LibrePortal
Sponsor
Install

Extra firewall rules

Keep your own firewall rules, such as SSH only over WireGuard, through rebuilds, updates, backups and restores. For administrators and for people who write software that runs on a LibrePortal server.

On this page
  1. Adding a rule
  2. Writing a rule
  3. What order the rules work in
  4. What LibrePortal will not let you do
  5. Changing many rules at once
  6. Backups and restores
  7. For people who write software for LibrePortal servers

LibrePortal looks after the server's firewall. Every install, update and libreportal firewall refresh rebuilds it from what your apps need. That is what keeps it correct, and it is also why a rule you added by hand with ufw is never quite safe: you are asking the owner of the firewall to leave your rule alone.

Extra rules are the other way round. You hand a rule to LibrePortal and it keeps it for you. It checks the rule cannot lock you out, puts it in the firewall, puts it back after every rebuild, and carries it through backups and restores to a new server.

Some things people use them for: SSH only over a WireGuard interface, an app's admin ports only on the VPN, a limit on how fast one address may connect.

Adding a rule#

In the WebUI, open Admin → Tools → Firewall. Type the rule, an Owner (a short name for whoever the rule belongs to, admin is fine for your own) and, if you like, a Label that tells you what it is for, then press Add rule. The page groups the rules by owner, shows each as In force or Skipped, and says why when one was skipped.

From a terminal it is the same thing:

sudo -u libreportal libreportal firewall rule add --owner admin --label "SSH (VPN only)" 'allow tcp 22 iif wg0'
sudo -u libreportal libreportal firewall rule list
sudo -u libreportal libreportal firewall rule remove --owner admin 'allow tcp 22 iif wg0'

A change shows in the Tasks list as Firewall - Add Extra Rule and takes effect straight away. Leave the rule out of remove and every rule of that owner goes.

Writing a rule#

A rule is one line: what to do, which traffic, and optionally where it comes from or arrives.

allow tcp 22 iif wg0
allow tcp 8080,8081 from 10.0.0.0/24
allow udp 53 to 10.44.0.1 iif wg-hub
allow tcp 22 from 10.44.0.0/24 iif wg-hub
deny tcp 22
limit tcp 22
allow any iif wg-hub
route allow iif wg-hub oif wg-hub
  • allow, deny or limit. limit is the firewall's own rate limit: an address that opens six or more connections in thirty seconds is turned away for a while.
  • tcp, udp or any. Leave it out before from, to or iif and it means any.
  • Ports are one port, a comma list (8080,8081) or a range (1714:1764), up to fifteen entries.
  • iif is the interface the traffic arrives on (wg0). from and to are IPv4 addresses or networks. IPv6 addresses are not accepted yet. You can use from and iif together.
  • A rule with no ports, such as allow any iif wg-hub, needs iif, from or to, so it cannot open everything by accident.
  • route is for traffic the server passes on between interfaces. Turning on forwarding in the kernel is still your job.

Anything else in the line is refused with a sentence saying what is wrong, so a typo never turns into a different rule.

What order the rules work in#

The firewall reads its rules from the top and the first one that matches wins. Your rules go in together as one block, in the order you added them, above LibrePortal's own rules. That is on purpose: a deny of yours beats the open rules LibrePortal and the installer add, and an allow of yours can open a port to a VPN that LibrePortal otherwise keeps shut.

So for SSH only over WireGuard, add the allow first and the deny second:

sudo -u libreportal libreportal firewall rule add --owner admin 'allow tcp 22 iif wg0'
sudo -u libreportal libreportal firewall rule add --owner admin 'deny tcp 22'

Your block sits just under the rules for LibrePortal's own private network, which only ever match that tunnel. A blanket deny tcp 22 therefore also closes SSH over LibrePortal's private network unless you allow that interface (wg-lpadmin) before it.

Why the install's SSH rule goes away

The firewall treats an allow and a deny for exactly the same thing as one rule, so deny tcp 22 could never sit beside the plain "allow SSH from anywhere" the installer adds. While one of your rules holds the SSH port, LibrePortal takes that open rule out and does not put it back on updates or rebuilds. Remove your deny and it returns.

What LibrePortal will not let you do#

Rules are checked when you add them, and a refused rule says so and changes nothing.

  • Cutting yourself off. A deny that nothing narrows, on the SSH port or the WebUI port, is refused unless an allow for that port stands in front of it on an interface that is up, or from a network you name. A deny that is limited with from or iif only warns. If you are connected over SSH from somewhere none of your allows covers, you get a warning too. Taking away the allow that a deny depends on is refused for the same reason. There is no way round this one: add the allow first.
  • Closing LibrePortal's own doors. A deny that covers the web ports (80 and 443, when Traefik is installed) or the VPN port is refused unless you add --force, which prints a warning. Another program's ports, a WireGuard hub's say, are yours to decide and are not refused.
  • Bypassing the per-person rules. A rule for every port on LibrePortal's own interfaces (wg-lpadmin, Docker's bridges), or any routed rule on one, is refused unless you add --force.

At a rebuild nothing is judged again. A rule the firewall cannot take is skipped, marked Skipped with the firewall's own words, and the rest of the firewall, every LibrePortal rule and every other extra rule, still goes in. The Tasks list and the log show it too.

Changing many rules at once#

Each rule add and rule remove puts the whole block into the firewall, which can take a while when there are many. For dozens of changes use a batch: one list, one reload, all or nothing.

libreportal firewall batch apply rules.txt
libreportal firewall batch apply --from-stdin < rules.txt
libreportal firewall batch apply rules.txt --confirm-within 5m
libreportal firewall batch confirm        # keep it
libreportal firewall batch rollback       # undo it now
libreportal firewall batch status

The file has one change per line, written exactly as for rule add and rule remove. Blank lines and lines starting with # are ignored, and quotes work as in the shell (nothing is ever expanded).

# staff ranks
add --owner myvpn --label 'Staff to SSH' 'allow tcp 22 iif wg0'
add --owner myvpn 'allow tcp 8080,8081 from 10.0.0.0/24'
remove --owner myvpn 'allow tcp 9000 iif wg0'
remove --owner oldtool

Every line is checked first, in order, with the same checks as the single commands, against the rules as the earlier lines leave them (so an allow followed by a deny on the same port is fine). If any line is refused you are told which one and why, and nothing changes. Put the adds before the removes when a deny depends on an allow you are replacing. After that, one reload puts the lot in; if the firewall turns a rule away, everything goes back as it was.

A safety net. --confirm-within 5m (seconds, minutes or hours, 20 seconds to 24 hours) starts a timer on the server before anything changes. If libreportal firewall batch confirm has not been run when it ends, the previous rules are put back, even if your connection dropped. The five minutes count from when the new rules are live. Only one batch can wait for its confirmation at a time. A server that reboots in that window loses the timer: batch status then shows the change as overdue, and batch rollback undoes it.

For programs. Add --json to apply, confirm, rollback, status or firewall reload for one JSON document on standard output, for example {"schema":"firewall-batch/1","ok":true,"changed":true,"lines":50,"added":48,"removed":2,"already_there":0,"reloads":1,"pending_confirm":true,"confirm_deadline":"2026-10-30T14:05:00Z","confirm_within_seconds":300}. A refused batch gives "ok":false, "changed":false, the line and the reason in error, and exit status 1. features in rule list --json includes batch, no-reload and confirm on servers that have them.

Stage now, reload later. rule add and rule remove also take --no-reload: the change is saved and shows as pending, and libreportal firewall reload puts everything into the firewall at once. A rule the firewall will not take is only found at that reload, so prefer a batch when you need all or nothing.

Backups and restores#

Your rules are part of the system backup. When you restore onto a new server they come back in their old order and go back into the firewall. A deny for SSH or the WebUI whose allow interface is not up on the new machine yet is held back and marked Skipped, so a restore cannot lock you out of the server you are restoring to. Run sudo -u libreportal libreportal firewall reload once the interface is up.

For people who write software for LibrePortal servers#

Do not write firewall files, and do not call ufw directly for rules you want to keep. Ask LibrePortal:

sudo -u libreportal libreportal firewall rule add --owner myapp --label "My app admin" 'allow tcp 9443 iif wg-hub'
  • Take over a rule that already exists. If someone wrote the rule with ufw by hand, rule add is refused, because ufw keeps one copy of a rule. rule adopt takes the hand-made rule out and keeps it as yours, so it now survives rebuilds. It works only on a rule that matches exactly, and never on one LibrePortal made itself. If your rule is then refused, the old one is put back as it was.
sudo -u libreportal libreportal firewall rule adopt --owner myapp 'allow tcp 9443 iif wg-hub'
  • Run it on every start. Adding a rule that is already there changes nothing and succeeds.
  • Check the exit code. A refused rule exits non-zero and says why on the screen.
  • Use one owner name for everything your software adds, and rule remove --owner myapp when it is uninstalled.
  • Read rules back as JSON, which also tells you the feature is there:
sudo -u libreportal libreportal firewall rule list --json --owner myapp
{"format_version": 1, "failed": 0, "owners": ["myapp"],
 "features": ["route", "to", "any", "label", "adopt", "hooks"],
 "rules": [{"id": 4, "owner": "myapp", "label": "My app admin",
            "rule": "allow tcp 9443 iif wg-hub", "state": "applied", "reason": "",
            "ufw_rule": "ufw allow in on wg-hub to any port 9443 proto tcp"}]}

state is applied, failed (with the reason) or pending. The rule is the tidied text, with the ports sorted and any written out, so compare on that. features says what this server understands, so check it before relying on route, hooks and the rest. New fields may appear; format_version only changes if one is taken away.

  • The firewall comment on each rule is LibrePortal-extra:<owner>, followed by :<label> when you gave one. Do not edit those rules with ufw; a rebuild puts them back as LibrePortal holds them.
  • Rules in your own nftables table are safe. A rebuild never flushes the ruleset, resets ufw or runs iptables -F. It only ever removes rules carrying LibrePortal's own comments.
  • Do something after a rebuild. The folder /etc/libreportal/hooks/firewall-post.d/ exists on every install (created on install and on each update, with a README). Put an executable in it. It runs as root after every rebuild and after firewall reload, in name order, for at most 30 seconds. It is only run if it and the folder are owned by root and nobody else can write to them, and a failing hook is logged without stopping the others. Take the executable bit away to switch a hook off. libreportal firewall hooks list --json shows each hook and how it did last time, and libreportal firewall hooks run runs them now.

Rooted Docker

Extra rules filter traffic to the server itself, and routed traffic with route. On a server running rooted Docker, ports an app publishes are reached through Docker's own forwarding rules, which a plain deny does not reach. LibrePortal manages those with ufw-docker already; if you need to close one, make the app's port private.

Next steps