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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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