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:
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”.
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-applyflag 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 showfirst.
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.
HosterLibhasAddFirewallRuleWithRollbackandUpdateFirewallRuleWithRollback, 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.
AddFirewallRulecalls the same internal function with a TTL of0, and a TTL of zero is rejected as “rollback ttl must be positive” — so no token is ever issued. There is no--rollback-ttlflag and nohoster 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.
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
- Deploying Control Center — the DNAT rules there are the worked example this page generalises.
- Networks — the bridges these rules name, and where they come from.
- DNS and the WireGuard mesh — the other half of what a network does: names, and routing between hosts.
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.