Docs / Networking

Firewall

Declared rules, generated nftables. The four scopes, publishing a service with DNAT, and why a rule you add takes effect the moment you press enter.

You do not write nftables rules. You declare intent — this guest publishes that port, this subnet may reach that service — and Hoster generates, validates and applies the nftables configuration.

That matters for a reason beyond convenience: the generated configuration is rewritten from your declarations on every boot, so a node’s firewall is a function of what you declared rather than of whatever accumulated in a file.

The four scopes

Same verbs at every level. Pick the narrowest one that expresses what you mean.

Scope For
hoster firewall input traffic terminating on the host itself
hoster firewall vm <name> one virtual machine
hoster firewall microvm <name> one microVM
hoster firewall bridge <name> a bridge, a subnet, or a class of guests

Each of vm, microvm and bridge takes the same operations:

Output
list  add-ingress  add-egress  add-dnat  add-snat  add-custom  set-hairpin  remove

Host input rules are add, list and remove — there is no NAT for traffic that terminates on the host.

Looking before you leap

hoster firewall status     # enabled, active, rule count, when it was last generated
hoster firewall show       # the nftables config your declarations produce — a preview
hoster firewall apply      # regenerate, validate, apply

show prints the generated configuration without touching the running one. It is the dry run, and on a machine you reach over the network it is the habit worth having.

status reports whether the firewall is enabled, whether nftables is active, whether the table exists, how many rules there are, and whether persistence and the include are configured — which is how you tell “no rules” apart from “not switched on”.

Screenshot

Take a screenshot of `hoster firewall status` and the first page of `hoster firewall show` side by side on a node with a few published services.

Changes take effect immediately

Adding or removing a rule applies it. There is no staging step and no confirmation prompt: the command saves the configuration and applies the generated nftables in one go.

Older material mentions a --no-apply flag for batching changes. It does not exist — the variable is in the source but was never wired to a flag. Every add and remove applies. Plan accordingly, especially over SSH: hoster firewall show first.

There is no rollback from the CLI — and a network change does have one

This is the sharpest edge on this page, and it is worth stating plainly because the neighbouring subsystem behaves the opposite way.

A firewall rule you add from the CLI is permanent the moment it applies. If it locks you out, nothing brings it back — you are going to the console.

A network change is not. If hoster network create fails to reconcile, the previous configuration is restored, host networking is reverted onto it, and the dependent services are reloaded; if that recovery itself fails, the error says so explicitly. See what create actually does.

So the same instinct — “I will just try it and see” — is cheap on a network and expensive on a firewall rule.

The safety net exists, but only over the API. HosterLib has AddFirewallRuleWithRollback and UpdateFirewallRuleWithRollback, which take a TTL and return a rollback token and a deadline; the agent exposes them under /api/v1/host/firewall/rollback/. It is a dead-man’s switch: the rule applies immediately, and unless you confirm the token before the deadline, it is reverted automatically. Exactly what you want when changing rules on a machine you reach only over the link you are changing.

The CLI cannot use it. AddFirewallRule calls the same internal function with a TTL of 0, and a TTL of zero is rejected as “rollback ttl must be positive” — so no token is ever issued. There is no --rollback-ttl flag and no hoster firewall confirm.

One caveat that applies even over the API: pending rollbacks are held in memory, in a map with a timer. If the agent restarts before you confirm, the timer dies with it and the change simply stays. The dead-man’s switch does not survive a crash of the thing holding it.

Until the CLI grows the flag, the substitute is discipline: run hoster firewall show before applying, and keep a second session open — a serial console via hoster vm console for a guest, or a physical console for a node. hoster firewall disable flushes every rule and is the fastest revert, but it only helps while you can still reach the machine to run it, which is precisely what a bad rule takes away.

Filtering: ingress and egress

hoster firewall vm add-ingress web-01 \
  --protocol tcp --ports 443 \
  --description "HTTPS from anywhere"

hoster firewall vm add-egress web-01 \
  --protocol tcp --ports 80,443 --action accept \
  --description "outbound web only"
Flag Default Notes
--ports 443, 80,443, or a range like 8000-9000
--protocol tcp, udp, icmp
--action accept accept, drop, reject
--priority 100 lower runs first
--description worth filling in; it is what list shows you in six months
--no-counter off drop the packet counter for this rule

--description is the flag people skip and then regret. A rule list of ports and protocols with no reasons attached is a list nobody will dare delete from.

Multi-NIC guests

A guest with more than one interface is ambiguous — say which bridge the rule belongs to:

hoster firewall vm add-ingress db-01 --bridge hoster-internal --protocol tcp --ports 5432

Publishing a service with DNAT

This is the common case: something outside should reach a guest inside.

hoster firewall vm add-dnat web-01 \
  --protocol tcp \
  --external-address 192.0.2.10 \
  --external-port 8443 \
  --internal-port 443 \
  --description "HTTPS for web-01"

External port and internal port are separate, so several guests can publish the same internal port on different external ones.

The same rule, two ways

A DNAT can be expressed on the guest, or at bridge scope with a selector naming the guest. These are equivalent:

# on the guest
hoster firewall microvm add-dnat controlcenter \
  --protocol tcp --external-address 192.0.2.10 \
  --external-port 1443 --internal-port 443

# at bridge scope, selecting the guest
hoster firewall bridge add-dnat hoster-internal \
  --target-microvm controlcenter \
  --protocol tcp --external-address 192.0.2.10 \
  --external-port 1443 --internal-port 443

Prefer the guest-scoped form for one guest; reach for bridge scope when the rule is about a group. Bridge rules take exactly one selector:

Selector Selects
--target-vm one VM
--target-microvm one microVM
--target-tag every VM carrying a tag
--target-microvm-tag every microVM carrying a tag
--subnet a CIDR range

Tags are what make this worth using: publish a port for every guest tagged web once, and a guest deployed tomorrow with that tag is covered without touching the firewall.

Both forms name the guest rather than its address, so the rule survives the guest being re-addressed.

Screenshot

Take a screenshot of `hoster firewall vm list` for a guest with a couple of ingress rules and a DNAT, showing the rule ids and descriptions.

SNAT: choosing the way out

DNAT is how traffic gets in. SNAT decides which address it leaves from — for a host with several public addresses, or a guest that must be seen as a particular one:

hoster firewall vm add-snat vpn-01 \
  --protocol udp \
  --external-address 198.51.100.25 \
  --out-interface bond0 \
  --description "vpn-01 egress via bond0"

Hairpin

hoster firewall vm set-hairpin web-01 --enabled

Hairpin NAT lets a guest reach a published service by its external address, rather than only from outside. Without it, a guest that resolves your public name and connects to it gets no answer, while everything works fine from the internet — a confusing class of bug worth knowing the name of.

Host input rules

For traffic to the node itself, not to a guest:

hoster firewall input add --protocol tcp --ports 22 --subnet 192.0.2.0/24 \
  --description "SSH from the management LAN"
hoster firewall input list
hoster firewall input remove <rule-id>

Removing rules

Rules are removed by id, which list gives you:

hoster firewall vm list web-01
hoster firewall vm remove web-01 dnat <rule-id>

The rule type — ingress, egress, dnat, snat, custom — goes between the scope name and the id. Every list takes --json, which is the form to script against.

Custom rules, when the model does not fit

hoster firewall bridge add-custom hoster-internal \
  --chain forward \
  --raw '<raw nftables rule text>' \
  --description "why this exists"

--chain is one of prerouting, postrouting, input, output or forward. Raw rules are passed through to nftables, so they are yours to get right — validation will catch syntax, not intent. Use the modelled rule types where they fit; a raw rule is the one thing here that will not be reasoned about for you.

How rules survive a reboot

Nothing is stored in nftables itself. Your declarations are the source, and on every boot — before the firewall, DHCP and DNS services start — a one-shot service regenerates the configuration and writes it to /etc/nftables.d/vm_firewall.nft, effectively running hoster firewall show into the file nftables includes.

That is the same service described in Quick start, stage 2, and it is why the answer to “the firewall looks wrong” is usually hoster firewall show rather than reading nftables.

Switching it off

hoster firewall disable    # flush the rules
hoster firewall enable     # apply them again

disable flushes; your declarations are untouched and enable brings them back. It is a diagnostic, for answering “is the firewall the reason this does not work” — not a configuration change.

Next

Something unclear on this page?

Ask about this specific page and we will come back to you. Your question arrives with a link to it, so you do not have to describe where you were.