Host Firewall
- POST /network/host-firewall/apply
Host firewall: apply posture (observe/enforce)
Flips this node’s firewall posture. Body: {mode (required: “observe”|”enforce”), ttl_seconds (optional int, enforce-only dead-man deadline, default 60, ignored for observe)}. enforce gates through PreEnforceCheck then arms a dead-man BEFORE applying → 200 {token,mode,ttl_seconds,node}; observe applies straight → 200 {mode}. node is this node’s hostname so the UI routes the follow-up /confirm and /revert (the token is node-local) to the same node. 409 = enforce refused: either the node is already enforcing (confirm/revert the pending change first) or the gate blocked (reach-uncovered or dry-run parse fail); nothing applied in either case. The mode-file-write 500 is the ONE error body that carries a token (kernel change + dead-man succeeded; only the on-disk record failed — the caller must still /confirm).
- Status Codes:
200 OK – enforce {token,mode,ttl_seconds,node} or observe {mode}
400 Bad Request – malformed body, or mode not observe|enforce
401 Unauthorized – missing/invalid auth token
409 Conflict – enforce refused: already enforcing, or gate blocked (reach-uncovered / dry-run parse fail); nothing applied
500 Internal Server Error – apply/arm/snapshot failed, or mode-file write failed (that body also carries token)
- GET /network/host-firewall/audit
Host firewall: audit trail
The last N entries of this node’s append-only firewall audit trail (rule upserts, apply flips), oldest-first. limit defaults to 100; a bad ?limit= is ignored. Read-only. Set X-Destination-Node to read a remote node’s trail; omit for local.
- Query Parameters:
limit (integer) – max entries to return (default 100)
- Status Codes:
200 OK – {entries:[{ts,action,actor,detail,rendered_hash}]}
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – read failed: <cause>
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- Response JSON Object:
entries[].action (string)
entries[].actor (string)
entries[].detail (string)
entries[].rendered_hash (string)
entries[].ts (string)
- POST /network/host-firewall/confirm
Host firewall: confirm (disarm the dead-man)
Disarms the commit-confirm dead-man for the token returned by /apply enforce — the proof the enforce apply did not strand the operator. Body: {token (required)}. Empty/missing token → 400; a Confirm failure (timer could not be stopped, e.g. unknown/expired token) → 500.
- Status Codes:
200 OK – {ok:true}
400 Bad Request – malformed body, or empty/missing token
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – confirm failed (timer could not be stopped)
- GET /network/host-firewall/drift
Host firewall: drift (live vs desired)
Whether the live owned table matches what the current role+mode would render, computed fresh on every read (never persisted). reason is one of “owned table absent from kernel (stomped or not yet applied)” / “live enforce state does not match desired mode” / “live rule count differs from desired render”, and is omitted when not drifted. desired_hash is a sha256 of the desired nft text for client-side change detection only.
- Status Codes:
200 OK – {drifted, role, mode, live_present, live_enforcing, live_rules, desired_enforcing, desired_rules, desired_hash, reason?}
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – detect failed: <cause>
- GET /network/host-firewall/manifest
Host firewall: effective manifest
The compiled effective allow-list for this node’s role: each allow’s service, proto, port (or port_lo/port_hi/all_ports), plane, reach, and whether it is a system-reserved (non-editable) entry. Read-only; the only failure is auth. Set X-Destination-Node to read a remote node’s manifest; omit for the local node.
- Status Codes:
200 OK – {role, allows:[{service,proto,port,port_lo,port_hi,all_ports,plane,reach,reserved}]}
401 Unauthorized – missing/invalid auth token
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- GET /network/host-firewall/nodes
Host firewall: node registry
The mgmt-side list of known nodes ({os_ip, hostname, fqdn}) from the BMC inventory — the targets the UI node-forwards firewall detail requests to via X-Destination-Node. Management-server aggregate: NOT node-forwarded. 503 when called on a node with no registry (not a management server).
- Status Codes:
200 OK – {nodes:[{os_ip,hostname,fqdn}]}
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – registry read failed: <cause>
503 Service Unavailable – node registry unavailable (not a management server)
- Response JSON Object:
nodes[].fqdn (string)
nodes[].hostname (string)
nodes[].os_ip (string)
- DELETE /network/host-firewall/planes
Host firewall: delete plane→bridge override
Removes one operator-authored plane→bridge override ({plane: mgmt|storage}; bridge ignored), reverting the plane to its compiled default. Gated: a non-managed plane is a 400 with the engine’s message and nothing is touched; a plane already on its compiled default (no override) is a 404. On success the override is cleared, audited best-effort, and re-applied to the wire immediately. Set X-Destination-Node to target a remote node; omit for local.
- Request JSON Object:
bridge (string)
plane (string)
- Status Codes:
200 OK – {ok:true, reverted_to:compiled default}
400 Bad Request – malformed body, or non-managed plane
401 Unauthorized – missing/invalid auth token
404 Not Found – plane has no override (already on compiled default)
500 Internal Server Error – persist failed
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- GET /network/host-firewall/planes
Host firewall: plane model
Managed plane→bridge bindings (mgmt→cloudbr0, storage→cloudbr1), the operator-chosen fabric underlay interface (”” when no fabric is configured), and the compiled reserved floor for this node’s role (mgmt [22,443,8097,9096,9191,9443] / agent [22,8097,9096]). Read-only; the only failure is auth.
- Status Codes:
200 OK – {role, planes:[{plane,bridge}], fabric_iface, reserved_floor}
401 Unauthorized – missing/invalid auth token
- POST /network/host-firewall/planes
Host firewall: upsert plane→bridge override
Upserts one operator-authored plane→bridge override ({plane: mgmt|storage, bridge}). Validated (managed plane set + non-empty, whitespace-free bridge) before persist: an unknown plane or bad bridge is a 400 with the engine’s message and nothing is written. On success the override is persisted atomically and the resolver applies it (override beats the compiled default) on the next render; audited best-effort. Set X-Destination-Node to target a remote node; omit for local.
- Request JSON Object:
bridge (string)
plane (string)
- Status Codes:
200 OK – {ok:true}
400 Bad Request – malformed body, or invalid plane/bridge (validation message)
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – persist failed
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- GET /network/host-firewall/reserved
Host firewall: reserved port floor
The compiled, non-editable control floor that enforce must guarantee, sorted+deduped: mgmt [22,443,8097,9096,9191,9443], agent [22,8097,9096]. Read-only; the only failure is auth.
- Status Codes:
200 OK – {role, reserved_floor}
401 Unauthorized – missing/invalid auth token
- POST /network/host-firewall/revert
Host firewall: revert to snapshot
Replays this node’s safe-mode snapshot for the token (rolls the owned table back to the pre-enforce state and disarms the dead-man). Body: {token (required)}. Empty/missing token → 400; a Revert failure (unknown/expired token or replay error) → 500. On success the on-disk posture is forced back to observe (the kernel table was reverted) and the revert is audited best-effort. Set X-Destination-Node to target a remote node; omit for local.
- Request JSON Object:
token (string)
- Status Codes:
200 OK – {ok:true}
400 Bad Request – malformed body, or empty/missing token
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – revert failed (unknown/expired token or replay error)
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- DELETE /network/host-firewall/rules
Host firewall: delete operator rule
Removes one operator-authored rule addressed by the same identity as the upsert ({service, proto: tcp|udp, one of port OR port_lo/port_hi, plane, decision}). The engine matches on service+plane+proto+port; if no rule matches → 404 and nothing is written. On success the rule is removed, audited best-effort, and re-applied to the wire immediately. Set X-Destination-Node to target a remote node; omit for local.
- Request JSON Object:
decision (string)
plane (string)
port (integer)
port_hi (integer)
port_lo (integer)
proto (string)
service (string)
- Status Codes:
200 OK – {ok:true}
400 Bad Request – malformed body
401 Unauthorized – missing/invalid auth token
404 Not Found – no operator rule matches the given key
500 Internal Server Error – persist failed
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- GET /network/host-firewall/rules
Host firewall: list operator rules
The operator-authored allow/deny rules persisted for this node (rules.json), each with the same {service, proto, port|port_lo/port_hi, plane, decision} key that PUT/DELETE /rules address, plus best-effort actor/ts provenance. This is the operator-editable subset — distinct from GET /manifest, which is the full compiled+reserved effective allow-list. Empty (rules:[]) when none exist. Read-only; fail-safe (a missing/corrupt store returns [] , never 500). Set X-Destination-Node to read a remote node; omit for local.
- Status Codes:
200 OK – {rules:[{service,proto,port,port_lo,port_hi,plane,decision,actor,ts}]}
401 Unauthorized – missing/invalid auth token
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- Response JSON Object:
rules[].actor (string)
rules[].decision (string)
rules[].plane (string)
rules[].port (integer)
rules[].port_hi (integer)
rules[].port_lo (integer)
rules[].proto (string)
rules[].service (string)
rules[].ts (string)
- PUT /network/host-firewall/rules
Host firewall: upsert operator rule
Upserts one operator-authored allow/deny rule ({service, proto: tcp|udp, one of port OR port_lo/port_hi, plane: mgmt|storage|public, decision: allow|deny}). Validated against this node’s role + reserved floor before persist: a bad shape or a reserved-port target is a 400 with the engine’s message. On success the rule is persisted (latest decision per plane+proto+port wins — service is a LABEL, not part of the identity, so re-deciding a port REPLACES the earlier rule and a rename does NOT create a second one) and audited best-effort. Set X-Destination-Node to target a remote node; omit for local.
- Request JSON Object:
decision (string)
plane (string)
port (integer)
port_hi (integer)
port_lo (integer)
proto (string)
service (string)
- Status Codes:
200 OK – {ok:true}
400 Bad Request – malformed body, or invalid/reserved rule (validation message)
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – persist failed
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- GET /network/host-firewall/state
Host firewall: live applied state
Live state of the owned karios_fw table, listeners not covered by the allow-list, and whether default-deny may be applied: {role, applied:{present,policy,enforcing,rules}, uncovered:[{proto,addr,port,v6}], readiness:{ready,blockers:[{kind,detail}]}}. policy is “” when the table is absent; enforcing is true when a scoped per-plane deny is present (default-deny is never the base chain policy). readiness.ready=false means the reconciler is applying observe even if the posture file says enforce, and blockers says why (kinds: render-invalid, reach-port, fabric-iface, plane-bridge, uncovered-port). uncovered and blockers are always arrays, never null. Set X-Destination-Node to read a remote node’s own state; omit for the local node.
- Status Codes:
200 OK – live state + uncovered listeners + enforce readiness
401 Unauthorized – missing/invalid auth token
500 Internal Server Error – detect failed: <cause>
- Request Headers:
X-Destination-Node – target node OS IP/FQDN; omit for local node
- Response JSON Object:
applied.enforcing (boolean)
applied.policy (string)
applied.present (boolean)
applied.rules (integer)
readiness.blockers[].detail (string)
readiness.blockers[].kind (string)
readiness.ready (boolean)
role (string)
uncovered[].addr (string)
uncovered[].port (integer)
uncovered[].proto (string)
uncovered[].v6 (boolean)
- POST /network/host-firewall/validate
Host firewall: pre-enforce validate (dry-run)
Runs the real PreEnforceCheck (nft -c dry-run + operator-reach coverage). Read-only, mutates nothing. A PASS returns {ok:true,role}; a BLOCKED check returns {ok:false,role,error} but STILL 200 (the check ran and produced a verdict — only auth failure is non-200). error is the nft parse failure or the uncovered reach ports.
- Status Codes:
200 OK – {ok:true,role} on pass, or {ok:false,role,error} when blocked (still 200)
401 Unauthorized – missing/invalid auth token