Skip to content

Firewall

vxnsin edited this page Sep 7, 2026 · 3 revisions

Firewall

warden holds the machine's packet filter in whatever that machine actually uses, and every change it makes can be undone by doing nothing.

warden firewall allow ssh --from 10.0.0.0/8
warden firewall allow 8080
warden firewall apply

Rules are written down first and applied when you say so. warden firewall export prints what the machine would be told, and changes nothing. --for nftables writes for a firewall this machine does not have, which is how a ruleset gets read on a laptop before it reaches the server it is meant for.

Which firewall

kind Used on How a whole ruleset is applied
nftables Linux, by default nft -f — one transaction
iptables Linux, asked for by name iptables-restore — one transaction
pf macOS, FreeBSD, OpenBSD, NetBSD pfctl -f — one ruleset at a time
windows Windows netsh, rule by rule, with a snapshot around it

warden picks by what the machine is. WARDEN_FIREWALL_BACKEND or --for names one instead — an older Linux box still running iptables-legacy wants iptables.

Three of the four swap a whole ruleset in one step. Windows Defender Firewall has no such thing, so that backend takes its own snapshot before it starts and puts it back if any rule fails: a half-applied policy is never left standing.

Windows also filters statefully whichever way it is told, so it needs no established-traffic rule — allowing outbound is what lets the answers back. The other three are told explicitly, and warden never writes a ruleset without it:

ct state established,related accept
iif lo accept

Without those two lines a default-drop policy also drops the ssh session that applied it.

Nothing that cannot be undone

$ warden firewall apply
12 rules applied
rolling back in 60s unless you run `warden firewall confirm`

The order matters and is not negotiable:

  1. Snapshot first. Taken after the change, it would describe the change rather than what to go back to.
  2. Arm the rollback second. Armed after applying, there would be a window where a lost session means a lost machine.
  3. Apply third. An apply that fails disarms, because nothing was applied and nothing should be waiting to be undone.

The watchdog runs detached — its own session on POSIX, DETACHED_PROCESS on Windows — so it outlives the ssh session that armed it. That is the whole point: a rule that locks you out costs a minute, not a drive.

Command
warden firewall confirm Keep it, and call off the rollback
warden firewall restore Put it back now, without waiting
warden firewall status Whether a rollback is waiting, and for how long

--rollback 0 turns the window off, for a machine you are sitting at. WARDEN_FIREWALL_ROLLBACK sets the default, which is 60 seconds.

Taking over from ufw or firewalld

Two firewalls managing one machine is one too many, so taking over means reading the other one first and turning it off last.

$ warden firewall adopt
ufw is holding 5 rules
NAME              DIR  ACTION  WHAT           FROM        ORIGIN   UNTIL
allow-22          in   allow   tcp/22         any         adopted  -
allow-8000-8100…  in   allow   tcp/8000-8100  10.0.0.0/8  adopted  -
deny-3389         in   deny    tcp/3389       any         adopted  -
could not read: [ 6] Anywhere on eth0  ALLOW FWD  10.5.0.0/16
those would be lost. Write them by hand first, or say no.
Take over from ufw? [y/N]:
  • ufw is read from ufw status numbered, which is the output it documents. A bare 3389 means both protocols to ufw, so it becomes two rules rather than half an answer. ufw lists v4 and v6 apart; warden's table is inet, which is both, so the twin is not a second rule.
  • firewalld is read from firewall-cmd --list-all: services resolved against warden's own catalogue, ports and ranges as written, and the zone's source becoming the rule's source.

Anything that cannot be translated is named before anything is applied. A rule quietly dropped in this step is a door quietly left open, or quietly shut.

The other firewall stays enabled while the rollback window is open. Its rules are not loaded — warden's ruleset replaced them in one step — but the service is untouched, so rolling back returns the machine exactly as it was. warden firewall confirm is what finally disables it, and says so:

kept
  ufw --force disable
ufw is off

Named services

warden firewall allow ssh
warden firewall allow postgres --from 10.0.0.0/8
warden firewall deny smb

ssh, sftp, ftp, http, https, dns, dhcp, ntp, smtp, imap, rdp, vnc, smb, postgres, mysql, redis, mongodb, wireguard, mdns. A name nobody knows suggests the nearest one rather than just refusing.

A bare port number works too, and --proto udp when it is not tcp.

Rules that belong to a service

This is the part no other firewall tool can do, and the reason the registry and the firewall are one program: warden knows which port a service holds, for how long, and whether its holder is still there.

$ warden firewall open shop-api
NAME            DIR  ACTION  WHAT      FROM        ORIGIN    UNTIL
allow-shop-api  in   allow   tcp/8300  10.0.0.0/8  registry  58s

The rule takes the port the registry actually handed out, and the lease with it. When the registration lapses or is released, the rule goes:

$ warden firewall list
closed allow-shop-api - its service is gone
no rules yet

warden firewall dev-mode --for 2 opens the whole pool from one network for two hours, for the afternoon somebody else needs to reach what you are running. It closes itself, and warden doctor says it is open for as long as it is.

What a rule from the registry can never do

The registry listens on loopback and asks for no token there. If registering a service could open the machine to the network, then anything able to register could open the machine to the network — which is how UPnP became a byword. So a rule whose origin is the registry passes through one place, and that place decides:

  1. Registering opens nothing. A person types warden firewall open.
  2. The pool is the ceiling. Only ports between pool_start and pool_end. 22, 3389 and 445 are outside it and stay unreachable — to a mistake, and to a registry somebody else is driving.
  3. Loopback services have nothing to open, and are told so.
  4. Off until switched on. firewall_from_registry is false.
  5. Where from is decided once, by the operator, in firewall_allow_from. Not by the service.
  6. The lease is the rule's lease. A rule may not outlive it.
  7. Every rule says where it came frommanual, adopted, catalogue, registry — in the listing, the history, the event stream and the webhook.
  8. A development window is bounded and temporary, never a standing hole.

Bounds 2 and 6 carry the weight: one limits what can ever be opened, the other how long. Both are checked in one place, and there is a test for each of the eight that tries to get through it.

From another machine

Everything above is a person at a keyboard. The thing that most wants to open a port is somewhere else: a deploy that has just registered a service.

from warden import WardenClient

with WardenClient("http://build-01:7010", token="...") as client:
    rule = client.firewall_open("shop-api", source="10.0.0.0/8")
    client.firewall_apply(rollback=60)   # undoes itself unless confirmed
    client.firewall_confirm()
warden firewall status --on http://build-01:7010
warden firewall list   --on http://build-01:7010
warden firewall open shop-api --on http://build-01:7010

The switch

Reading is what a token already allows. Changing needs one setting on the machine being asked, and it is off out of the box:

warden settings set allow_remote_firewall true

Without it, every change comes back 403:

changing the firewall over the API is switched off -
set allow_remote_firewall on this warden to allow it

It is deliberately separate from firewall_from_registry. One decides what a rule may be, the other decides who may ask for one. A machine that never wants to be asked says so once, rather than relying on the other being off.

warden doctor fails — not warns — a warden that will change its own firewall on request, listens beyond loopback and asks for no token. That combination is a way through a firewall rather than a firewall.

It is not a way round the bounds

A rule asked for over the network is built by the same code as one asked for at the keyboard, so it meets the same eight bounds:

Asked for Answer
a source outside firewall_allow_from 403, naming what is allowed
a service bound to 127.0.0.1 403, "there is nothing to open"
a port outside the pool nothing can hold it, so nothing can open it
a comment with a newline in it 422, before it is stored
nothing named, and two networks declared 403 — warden does not pick for you

Endpoints

Method Path Needs Purpose
GET /v1/firewall token Backend, whether it is available, rule counts, what is waiting to roll back
GET /v1/firewall/rules token Every rule, ?origin= to filter
POST /v1/firewall/open token + switch The port a registered service holds
DELETE /v1/firewall/rules/{name} token + switch Take one back out
POST /v1/firewall/apply token + switch Make them true, ?rollback= in seconds
POST /v1/firewall/confirm token + switch Keep what was applied
POST /v1/firewall/restore token + switch Put a snapshot back, ?snapshot=

The rollback matters more here than at a keyboard: the caller is somewhere else, and a rule that shuts the door shuts it on them. apply arms it, a detached watchdog counts it down, and nothing is kept until confirm.

The whole fleet, from the hub

One firewall at a time is what nobody does at forty machines. The hub sees all of them and can drive all of them:

warden firewall status --all            # one line per node
warden firewall list --all              # every rule anywhere, with its node
warden firewall open shop-api --all     # on every node that holds it
warden firewall apply --fleet --rollback 120
warden firewall confirm --fleet
$ warden firewall status --all
NODE      BACKEND   RULES  REGISTRY  REMOTE  ROLLING BACK
build-01  nftables  12     3         yes     2m
hub       nftables  8      0         yes     -
web-02    iptables  0      0         no      -
db-03 (http://db-03:7010) could not be reached

A node that did not answer is named rather than quietly left out — the same rule the rest of the fleet views follow.

Applying across a fleet

$ warden firewall apply --fleet --rollback 120
NODE      RESULT   DETAIL
build-01  applied  12 rules, rolling back at 14:22:31
db-03     refused  changing the firewall over the API is switched off
hub       applied  8 rules, rolling back at 14:22:31
web-02    applied  4 rules, rolling back at 14:22:31

3 of 4 applied
`warden firewall confirm --fleet` keeps them; anything not confirmed puts itself back

Every node does it to its own firewall: its own snapshot, its own ruleset, its own watchdog counting down on its own machine. Nobody waits for anybody, and a node that refuses does not stop the rest.

A fleet-wide apply will not give up its rollback. --rollback 0 is refused, and it is the only place in warden where leaving the window out is not allowed. One wrong rule across forty machines is forty machines you cannot reach, and the watchdog on each of them is the only thing that opens them again without somebody driving there.

Confirming is a separate act. The hub never confirms on its own, however cleanly every node reported — the point of the window is that a person checks they can still reach the machines. Anything left unconfirmed comes back by itself.

Opening a service everywhere it lives

$ warden firewall open shop-api --all
NODE      RESULT   DETAIL
build-01  opened   allow-shop-api - 8000 from 10.0.0.0/8
hub       skipped  does not hold shop-api
web-02    opened   allow-shop-api - 8143 from 10.0.0.0/8

A name means a different port on every machine, and nothing at all on most of them. The hub asks who holds it first; a node that never registered it is skipped and said so rather than treated as a failure. Each node still resolves its own port and its own lease, and each rule still meets every bound.

One node at a time

warden firewall open shop-api --node build-01
warden firewall delete allow-shop-api --node build-01
warden firewall apply --node build-01 --rollback 120
warden firewall confirm --node build-01

--node goes through the hub. --on http://host:7010 goes straight to one warden without a hub in the middle. Both end up asking the same machine, and that machine decides.

What the hub cannot do

  • It cannot overrule allow_remote_firewall. A node with it off refuses, and the refusal is printed in that node's own words.
  • It cannot send a rule. open names a service; which port that is, and for how long, is the node's own answer.
  • It cannot get round the eight bounds. They are checked where the rule is built, which is on the node.
  • It cannot confirm for you. See above.

When a service dies while the ruleset is applied

warden closes the rule in its own book straight away, so it can never be applied again. The kernel still holds it until the next warden firewall apply, and warden doctor says so. Re-applying a firewall without being asked is not something warden does quietly.

What this needs

Root, or an elevated prompt on Windows. Every backend says which it is rather than failing obscurely:

nft refused: Operation not permitted - a firewall needs root
netsh refused: a firewall needs an elevated prompt on Windows

Clone this wiki locally