Skip to main content
Every managed Node runs Uncomplicated Firewall (UFW). Orbit owns the rules it needs for public SSH recovery, WireGuard member trust, and each role. Those rules carry an orbit: prefix. The rule orbit:wireguard-members admits every WireGuard member on the Node’s WireGuard address, as private network trust describes. The firewall family adds operator rules beside them, each identified by a stable name on one Node. The Gateway applies each rule over SSH, verifies it in the UFW status output, and records the result. The Node firewall catalog is the single source of Orbit-managed rules, so role provisioning and firewall commands use the same projection. Doctor checks the firewall family and reports a rule that is missing or different from its record without changing the machine.

Commands

Read the managed and live rules

The Gateway serves three read-only lists. The CLI has no command for them. The web app’s Node page and the MCP tools use them. The first role convergence removes orbit:public-ssh-recovery. So a Node with an active role does not list public port 22 as intended. The managed list comes from the same catalog that convergence applies. In the live list, each live rule has a match value. exact means it equals a managed or operator rule. drift means its comment names such a rule but the rule differs. unmanaged means its comment names no managed or operator rule, or it has no comment. A managed or operator rule that is absent from UFW is listed as missing. An inactive, absent, or unreachable UFW returns no live rows. The web app lists the live rules, shows drift and unmanaged rules in red, and flags missing rules. When it cannot read live UFW, it shows the operator rules and the managed catalog instead.

Rules and names

Every command accepts --json and requires --node with a numeric Node ID. A rule name is 1 through 63 characters of lowercase letters, digits, dots, underscores, and hyphens, starting and ending with a letter or digit; it cannot contain a colon, so an operator name never collides with an orbit: rule. The Gateway tags each UFW rule with the comment orbit:node:<node-id>:firewall:<name> and uses that comment for every later change. All targets and rule values are explicit; these commands do not prompt for missing arguments or options. Human requests show an indeterminate progress tree while the Gateway works, then the verified result and request ID. Piped output has no animation or escape codes. JSON contains only the existing response or error.

orbit firewall:allow

Allow traffic to one destination port or port range, optionally from one source.
Repeating the same command with the same values converges the existing rule and returns it. The same name with different values returns firewall.name_taken; remove the rule and create it again to change it. Human output reports Firewall rule [name] is active. with the request ID.

orbit firewall:deny

Deny traffic to one destination port or port range, optionally from one source. It accepts the same argument and options as firewall:allow.
The Gateway refuses a deny rule whose port contains the Node’s public SSH port for any source that would cover it with firewall.public_ssh_deny_forbidden, because that port is the recovery path after a failed provisioning. A deny rule and an allow rule with the same source, protocol, and port cannot coexist on one Node; the second one returns firewall.action_conflict.

orbit firewall:list

List the named rules recorded for one Node with their action, source, port, protocol, and lifecycle status.
Human output uses a read-only table with NAME, ACTION, SOURCE, PORT, PROTOCOL, and STATUS columns. Values wrap inside their cells; below the minimum width, labeled records retain every field. An empty result says No firewall rules.. A rule’s status is provisioning, active, failed, or removing. A failed rule keeps failed_step and error_code so the same command can be repeated after the cause is fixed. Orbit-owned orbit: rules are not part of this list.

orbit firewall:remove

Remove one named rule from UFW and delete its record. Before prompting, the CLI resolves the rule with a read-only lookup, then asks Remove firewall rule [name] from Node #ID? with No selected. Supply --yes for automation, including JSON; JSON never implies consent.
The Gateway marks the rule removing, deletes every UFW rule that carries its comment, verifies that none remains, and then deletes the record. A failure leaves the rule failed with the step that stopped; repeat the command to retry. Declining, Ctrl-C, or end of input before removal exits 1 with input.cancelled. Noninteractive removal without --yes exits 1 with input.confirmation_required and an instruction to supply --yes. Refused confirmations send no removal request. Existing lookup, authorization, and safety errors retain their codes.

Error codes

The CLI validates the name, source, protocol, and port before it sends a request. The Gateway returns the remaining codes.

Why it works this way

The managed list is the set that convergence keeps, not every rule Orbit can write. A Node with roles has closed public SSH, so listing it as intended would advertise a path that is gone. Hiding it only in the web app is rejected, because every other client would still read the wrong intent. Doctor alone is not enough, because operators look at the Node page, and Doctor is a separate run. The live read never changes UFW.
  • doctor inspects the firewall family with --family=firewall.
  • node removal refuses a Node that still owns named firewall rules.