v0.6.3: russh 0.61 security bump (CVE batch) + bergshamra 0.5 + axum 0.8
Security-driven dependency release.
- russh =0.55.0 (pinned) -> 0.61.2: closes the advisory batch reachable
from our SFTP *client* path — unbounded/allocation-first packet
parsing (CVE-2026-48110, CVE-2026-46702, CVE-2026-46673, HIGH) plus
CVE-2026-48107 in client auth. A malicious or compromised SFTP server
an operator pointed us at could previously OOM the PXE server. Also
drops mlock on non-secret buffers (~21% SSH throughput upstream) —
directly in the remote-share ISO streaming path. ring backend kept;
zero code changes needed in sftp_share.rs.
- bergshamra 0.4 -> 0.5.1: the pin's blocking condition (stable
RustCrypto generation, pkcs8 0.11) is now met upstream, so the
=0.55.0 pin is deleted and its comment rewritten as history. 0.5 is
secure-by-default for DSig (flags we already set explicitly) and
fixes an XML-Enc DerivedKey fallthrough.
- axum 0.7 -> 0.8.9: route captures /:id -> {id} across the router and
the /api/docs listing; ConnectInfo optional extraction moves to the
Result form. Gains the HEAD content-length fix (iPXE/sanboot clients
probe with HEAD before Range requests) and puts us back on the
maintained line.
Validation: clippy clean, fmt clean, all 272 workspace tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5da05a519d
commit
7f25bb681c
+49
-47
@@ -71,7 +71,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/branding/pxe-logo", get(ui_pxe_logo))
|
||||
// iPXE script endpoints.
|
||||
.route("/boot.ipxe", get(boot_top_menu))
|
||||
.route("/boot/:filename", get(boot_sub))
|
||||
.route("/boot/{filename}", get(boot_sub))
|
||||
// v0.5.2: unattended answer-file *serving* — public (like /iso),
|
||||
// because the booting installer fetches these with no session.
|
||||
// `/unattended/:id` serves a Kickstart/Preseed with `{{HOSTNAME}}`
|
||||
@@ -80,12 +80,12 @@ pub fn build_router(state: AppState) -> Router {
|
||||
// autoinstall (`…/<ctx>/user-data` + `/meta-data`), where `<ctx>`
|
||||
// base64url-encodes the per-host hostname/ip/mac. Management
|
||||
// (upload/list/delete) lives under the gated `/api/unattended`.
|
||||
.route("/unattended/:id", get(serve_unattended))
|
||||
.route("/unattended/:id/:ctx/:sub", get(serve_unattended_seed))
|
||||
.route("/unattended/{id}", get(serve_unattended))
|
||||
.route("/unattended/{id}/{ctx}/{sub}", get(serve_unattended_seed))
|
||||
// Bundled binaries and raw ISO access.
|
||||
.route("/ipxe/:name", get(ipxe_binary))
|
||||
.route("/iso/:filename", get(iso_raw))
|
||||
.route("/iso/:id/*path", get(iso_file))
|
||||
.route("/ipxe/{name}", get(ipxe_binary))
|
||||
.route("/iso/{filename}", get(iso_raw))
|
||||
.route("/iso/{id}/{*path}", get(iso_file))
|
||||
// Container health/readiness probes. `/healthz` is always 200 OK
|
||||
// while the HTTP task is alive. `/readyz` additionally requires at
|
||||
// least one bundled iPXE binary (without one, no client can PXE).
|
||||
@@ -93,23 +93,23 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/readyz", get(readyz))
|
||||
// JSON API.
|
||||
.route("/api/isos", get(api_list_isos).post(api_upload_iso))
|
||||
.route("/api/isos/:id", delete(api_delete_iso))
|
||||
.route("/api/isos/{id}", delete(api_delete_iso))
|
||||
.route("/api/uploads", post(api_upload_begin))
|
||||
.route(
|
||||
"/api/uploads/:upload_id",
|
||||
"/api/uploads/{upload_id}",
|
||||
put(api_upload_chunk).delete(api_upload_abort),
|
||||
)
|
||||
// Per-ISO password prompt. PUT body `{ "password": "..." }`
|
||||
// sets, `{ "password": null }` (or DELETE) clears.
|
||||
.route(
|
||||
"/api/isos/:id/password",
|
||||
"/api/isos/{id}/password",
|
||||
axum::routing::put(api_set_iso_password).delete(api_clear_iso_password),
|
||||
)
|
||||
// v0.4.4: per-ISO menu category (Os / Tools). Drives whether the
|
||||
// image appears under Linux/Windows Installers (default) or in
|
||||
// the Tools submenu next to memtest / shell / NIC info.
|
||||
.route(
|
||||
"/api/isos/:id/category",
|
||||
"/api/isos/{id}/category",
|
||||
axum::routing::put(api_set_iso_category),
|
||||
)
|
||||
// v0.4.4: filesystem free-space telemetry for the ISO directory's
|
||||
@@ -120,7 +120,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
// logo). v0.5.2: split into three slots — `light` / `dark` /
|
||||
// `client`. Multipart upload to POST; DELETE clears one slot.
|
||||
.route(
|
||||
"/api/branding/logo/:slot",
|
||||
"/api/branding/logo/{slot}",
|
||||
post(api_branding_upload).delete(api_branding_clear),
|
||||
)
|
||||
// v0.5.2: unattended-install answer-file management (gated).
|
||||
@@ -130,7 +130,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/unattended",
|
||||
get(api_unattended_list).post(api_unattended_upload),
|
||||
)
|
||||
.route("/api/unattended/:id", delete(api_unattended_delete))
|
||||
.route("/api/unattended/{id}", delete(api_unattended_delete))
|
||||
// v0.4.4: self-rendered API reference, served as JSON so the UI
|
||||
// can format it consistently with the rest of the chrome. Lives
|
||||
// under the Settings tab — operators chasing an integration get
|
||||
@@ -161,12 +161,12 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/api/settings", get(api_get_settings).put(api_put_settings))
|
||||
.route("/api/queue", get(api_list_queue))
|
||||
.route("/api/queue/join", get(api_queue_join))
|
||||
.route("/api/queue/poll/:entry_id", get(api_queue_poll))
|
||||
.route("/api/queue/poll/{entry_id}", get(api_queue_poll))
|
||||
.route("/api/queue/assign", post(api_queue_assign))
|
||||
// v0.5.2: per-device deployment profile (auto hostname / IP /
|
||||
// unattended file) set from the queue "Profile" button.
|
||||
.route("/api/queue/:entry_id/profile", put(api_queue_set_profile))
|
||||
.route("/api/queue/:entry_id", delete(api_queue_release))
|
||||
.route("/api/queue/{entry_id}/profile", put(api_queue_set_profile))
|
||||
.route("/api/queue/{entry_id}", delete(api_queue_release))
|
||||
// v0.4.65: SMB share manager (userspace via smbclient). The
|
||||
// kernel-mount NFS routes that v0.4.64 shipped are gone — they
|
||||
// didn't work on hosts whose kernel lacked the nfs client
|
||||
@@ -177,8 +177,8 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/smb-shares",
|
||||
get(api_smb_shares_list).post(api_smb_shares_add),
|
||||
)
|
||||
.route("/api/smb-shares/:id", delete(api_smb_shares_remove))
|
||||
.route("/api/smb-shares/:id/scan", post(api_smb_shares_scan))
|
||||
.route("/api/smb-shares/{id}", delete(api_smb_shares_remove))
|
||||
.route("/api/smb-shares/{id}/scan", post(api_smb_shares_scan))
|
||||
// v0.4.67: NFSv3 share manager (pure-Rust in-process client).
|
||||
// Ships alongside SMB. Routes are parallel so the UI can
|
||||
// reuse the same form/error/hint rendering for both.
|
||||
@@ -186,8 +186,8 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/nfs-shares",
|
||||
get(api_nfs_shares_list).post(api_nfs_shares_add),
|
||||
)
|
||||
.route("/api/nfs-shares/:id", delete(api_nfs_shares_remove))
|
||||
.route("/api/nfs-shares/:id/scan", post(api_nfs_shares_scan))
|
||||
.route("/api/nfs-shares/{id}", delete(api_nfs_shares_remove))
|
||||
.route("/api/nfs-shares/{id}/scan", post(api_nfs_shares_scan))
|
||||
// v0.5.5: SFTP-over-SSH share manager (pure-Rust russh client).
|
||||
// Parallel to SMB/NFS so the UI reuses the same form/error/hint
|
||||
// rendering. Like NFS, SFTP-sourced ISOs support Range requests.
|
||||
@@ -195,8 +195,8 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/sftp-shares",
|
||||
get(api_sftp_shares_list).post(api_sftp_shares_add),
|
||||
)
|
||||
.route("/api/sftp-shares/:id", delete(api_sftp_shares_remove))
|
||||
.route("/api/sftp-shares/:id/scan", post(api_sftp_shares_scan))
|
||||
.route("/api/sftp-shares/{id}", delete(api_sftp_shares_remove))
|
||||
.route("/api/sftp-shares/{id}/scan", post(api_sftp_shares_scan))
|
||||
// Phase 4: Network info (read-only) + DNS edit.
|
||||
.route("/api/network", get(api_network).put(api_network_put))
|
||||
// Phase 4: live-log stream + recent buffer for the Terminal tab.
|
||||
@@ -208,10 +208,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
// Phase 5: per-MAC host bindings. Operator
|
||||
// pins a MAC to a boot entry; /boot.ipxe?mac=... chains directly.
|
||||
.route("/api/hosts", get(api_hosts_list).post(api_hosts_upsert))
|
||||
.route("/api/hosts/:mac", delete(api_hosts_remove))
|
||||
.route("/api/hosts/{mac}", delete(api_hosts_remove))
|
||||
// v0.5.0: Wake-on-LAN a bound host. Sends a magic packet to the
|
||||
// limited broadcast + the server's own subnet broadcast.
|
||||
.route("/api/hosts/:mac/wol", post(api_hosts_wol))
|
||||
.route("/api/hosts/{mac}/wol", post(api_hosts_wol))
|
||||
// Rolling "host log" of boot events: what image actually
|
||||
// started installing on what MAC/IP, and when. Persisted to disk.
|
||||
.route("/api/boot-log", get(api_boot_log))
|
||||
@@ -520,14 +520,15 @@ fn text_plain(body: String) -> Response {
|
||||
/// to the bound target instead of rendering the menu.
|
||||
async fn boot_top_menu(
|
||||
State(state): State<AppState>,
|
||||
peer: Option<ConnectInfo<SocketAddr>>,
|
||||
peer: Result<ConnectInfo<SocketAddr>, axum::extract::rejection::ExtensionRejection>,
|
||||
Query(p): Query<BootMenuParams>,
|
||||
) -> Response {
|
||||
// `ConnectInfo` is only populated when axum was started with
|
||||
// `into_make_service_with_connect_info` (production path). Tests
|
||||
// call the router via `oneshot`, which skips that wiring — we
|
||||
// tolerate it by treating the peer as unknown rather than 500ing.
|
||||
let peer_ip = peer.map(|c| c.0.ip());
|
||||
// (axum 0.8: `Result<T, Rejection>` is the optional-extractor form.)
|
||||
let peer_ip = peer.ok().map(|c| c.0.ip());
|
||||
state
|
||||
.metrics
|
||||
.record_http(openpxe_core::HttpRoute::BootScript);
|
||||
@@ -619,11 +620,12 @@ struct BootSubParams {
|
||||
|
||||
async fn boot_sub(
|
||||
State(state): State<AppState>,
|
||||
peer: Option<ConnectInfo<SocketAddr>>,
|
||||
peer: Result<ConnectInfo<SocketAddr>, axum::extract::rejection::ExtensionRejection>,
|
||||
AxumPath(filename): AxumPath<String>,
|
||||
Query(p): Query<BootSubParams>,
|
||||
) -> Response {
|
||||
let peer_ip = peer.map(|c| c.0.ip());
|
||||
// axum 0.8: `Result<T, Rejection>` is the optional-extractor form.
|
||||
let peer_ip = peer.ok().map(|c| c.0.ip());
|
||||
// `/boot/<name>.ipxe` where `<name>` is either one of our reserved
|
||||
// submenu names (prefixed `_`) or a boot entry id.
|
||||
let name = filename.strip_suffix(".ipxe").unwrap_or(&filename);
|
||||
@@ -1560,13 +1562,13 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "List ISOs (local + NFS) with size, family, boot entries, category."},
|
||||
{"method": "POST", "path": "/api/isos",
|
||||
"summary": "Legacy single-shot multipart upload. Prefer /api/uploads for big files."},
|
||||
{"method": "DELETE", "path": "/api/isos/:id",
|
||||
{"method": "DELETE", "path": "/api/isos/{id}",
|
||||
"summary": "Delete a local ISO and its sidecar metadata."},
|
||||
{"method": "PUT", "path": "/api/isos/:id/password",
|
||||
{"method": "PUT", "path": "/api/isos/{id}/password",
|
||||
"summary": "Set or update an ISO's boot password (bcrypt-hashed; plaintext never stored)."},
|
||||
{"method": "DELETE", "path": "/api/isos/:id/password",
|
||||
{"method": "DELETE", "path": "/api/isos/{id}/password",
|
||||
"summary": "Clear an ISO's boot password."},
|
||||
{"method": "PUT", "path": "/api/isos/:id/category",
|
||||
{"method": "PUT", "path": "/api/isos/{id}/category",
|
||||
"summary": "Set the menu category. Body: { \"category\": \"os\" | \"tools\" }."},
|
||||
],
|
||||
},
|
||||
@@ -1575,9 +1577,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"endpoints": [
|
||||
{"method": "POST", "path": "/api/uploads",
|
||||
"summary": "Begin a chunked upload session. Body: { \"filename\", \"size_bytes\" }."},
|
||||
{"method": "PUT", "path": "/api/uploads/:upload_id",
|
||||
{"method": "PUT", "path": "/api/uploads/{upload_id}",
|
||||
"summary": "Append a chunk. Headers: x-openpxe-upload-offset, x-openpxe-upload-complete."},
|
||||
{"method": "DELETE", "path": "/api/uploads/:upload_id",
|
||||
{"method": "DELETE", "path": "/api/uploads/{upload_id}",
|
||||
"summary": "Abort a chunked upload session and remove the .partial file."},
|
||||
],
|
||||
},
|
||||
@@ -1588,9 +1590,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "List configured SMB shares with connection state and iso counts."},
|
||||
{"method": "POST", "path": "/api/smb-shares",
|
||||
"summary": "Register an SMB share. Body: { server, share, guest, username?, password?, port? }."},
|
||||
{"method": "DELETE", "path": "/api/smb-shares/:id",
|
||||
{"method": "DELETE", "path": "/api/smb-shares/{id}",
|
||||
"summary": "Forget a share and drop its entries from the ISO store."},
|
||||
{"method": "POST", "path": "/api/smb-shares/:id/scan",
|
||||
{"method": "POST", "path": "/api/smb-shares/{id}/scan",
|
||||
"summary": "Re-list a share for new ISOs."},
|
||||
],
|
||||
},
|
||||
@@ -1601,9 +1603,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "List configured NFSv3 shares with connection state and iso counts."},
|
||||
{"method": "POST", "path": "/api/nfs-shares",
|
||||
"summary": "Register an NFSv3 share. Body: { server, export, port? }. Auth is AUTH_SYS only; access control is by client IP on the server side."},
|
||||
{"method": "DELETE", "path": "/api/nfs-shares/:id",
|
||||
{"method": "DELETE", "path": "/api/nfs-shares/{id}",
|
||||
"summary": "Forget a share and drop its entries from the ISO store."},
|
||||
{"method": "POST", "path": "/api/nfs-shares/:id/scan",
|
||||
{"method": "POST", "path": "/api/nfs-shares/{id}/scan",
|
||||
"summary": "Re-list a share for new ISOs."},
|
||||
],
|
||||
},
|
||||
@@ -1614,9 +1616,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "List configured SFTP-over-SSH shares with connection state and iso counts."},
|
||||
{"method": "POST", "path": "/api/sftp-shares",
|
||||
"summary": "Register an SFTP share. Body: { server, export, username, port?, password? | private_key? + passphrase? }. The server's SSH host key is pinned trust-on-first-use."},
|
||||
{"method": "DELETE", "path": "/api/sftp-shares/:id",
|
||||
{"method": "DELETE", "path": "/api/sftp-shares/{id}",
|
||||
"summary": "Forget a share, drop its entries from the ISO store, and scrub its credentials file."},
|
||||
{"method": "POST", "path": "/api/sftp-shares/:id/scan",
|
||||
{"method": "POST", "path": "/api/sftp-shares/{id}/scan",
|
||||
"summary": "Re-list a share for new ISOs."},
|
||||
],
|
||||
},
|
||||
@@ -1636,9 +1638,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "Current runtime settings (Windows toggle, timeout, dns hint, …)."},
|
||||
{"method": "PUT", "path": "/api/settings",
|
||||
"summary": "Replace runtime settings. Guards against enabling Windows when wimboot isn't bundled."},
|
||||
{"method": "POST", "path": "/api/branding/logo/:slot",
|
||||
{"method": "POST", "path": "/api/branding/logo/{slot}",
|
||||
"summary": "Upload a custom logo for a slot (light | dark | client). Multipart 'file', PNG/SVG/JPEG/WebP/GIF up to 2 MB. The client slot is raster-only."},
|
||||
{"method": "DELETE", "path": "/api/branding/logo/:slot",
|
||||
{"method": "DELETE", "path": "/api/branding/logo/{slot}",
|
||||
"summary": "Remove the custom logo for a slot and revert to the bundled mark."},
|
||||
{"method": "GET", "path": "/branding/pxe-logo",
|
||||
"summary": "Raster form of the operator's 'client' logo for the iPXE menu's `console --picture`. Default background when unset/SVG."},
|
||||
@@ -1679,9 +1681,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "List uploaded answer files (Kickstart / Preseed / Autoinstall / Windows answer file)."},
|
||||
{"method": "POST", "path": "/api/unattended",
|
||||
"summary": "Upload an answer file (multipart 'file', .ks/.cfg/.seed/.yaml/.yml/.xml/user-data, up to 1 MB)."},
|
||||
{"method": "DELETE", "path": "/api/unattended/:id",
|
||||
{"method": "DELETE", "path": "/api/unattended/{id}",
|
||||
"summary": "Delete an uploaded answer file."},
|
||||
{"method": "GET", "path": "/unattended/:id",
|
||||
{"method": "GET", "path": "/unattended/{id}",
|
||||
"summary": "Public: serve an answer file with {{HOSTNAME}}/{{IP}}/{{MAC}} substituted from the query string."},
|
||||
],
|
||||
},
|
||||
@@ -1692,9 +1694,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "List queue entries (waiting + assigned, with any deployment profile)."},
|
||||
{"method": "POST", "path": "/api/queue/assign",
|
||||
"summary": "Assign a target image to queued clients. Body: { target, entry_ids }."},
|
||||
{"method": "PUT", "path": "/api/queue/:entry_id/profile",
|
||||
{"method": "PUT", "path": "/api/queue/{entry_id}/profile",
|
||||
"summary": "Set a queued device's deployment profile. Body: { auto_hostname?, auto_ip?, unattended_file? }."},
|
||||
{"method": "DELETE", "path": "/api/queue/:entry_id",
|
||||
{"method": "DELETE", "path": "/api/queue/{entry_id}",
|
||||
"summary": "Release a queue entry without assigning."},
|
||||
],
|
||||
},
|
||||
@@ -1705,9 +1707,9 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "List per-MAC boot bindings."},
|
||||
{"method": "POST", "path": "/api/hosts",
|
||||
"summary": "Pin a MAC to a boot target. Body: { mac, target, label, auto_hostname?, auto_ip?, unattended_file? }."},
|
||||
{"method": "DELETE", "path": "/api/hosts/:mac",
|
||||
{"method": "DELETE", "path": "/api/hosts/{mac}",
|
||||
"summary": "Remove a binding."},
|
||||
{"method": "POST", "path": "/api/hosts/:mac/wol",
|
||||
{"method": "POST", "path": "/api/hosts/{mac}/wol",
|
||||
"summary": "Send a Wake-on-LAN magic packet to a bound MAC (limited + subnet broadcast)."},
|
||||
{"method": "GET", "path": "/api/boot-log",
|
||||
"summary": "Ring of recent boot events (timestamp, mac, ip, target)."},
|
||||
|
||||
@@ -1489,9 +1489,10 @@ async fn api_docs_lists_known_endpoints() {
|
||||
}
|
||||
for needle in [
|
||||
"/api/isos",
|
||||
"/api/isos/:id/category",
|
||||
// v0.6.3: docs use axum 0.8's `{param}` capture syntax.
|
||||
"/api/isos/{id}/category",
|
||||
"/api/storage/disk",
|
||||
"/api/branding/logo/:slot",
|
||||
"/api/branding/logo/{slot}",
|
||||
"/api/unattended",
|
||||
"/api/boot-log",
|
||||
"/metrics",
|
||||
|
||||
Reference in New Issue
Block a user