Opening a firewall port¶
This page opens a port in a host's firewall. A port is open because the host's configuration lists it, so the change is made in a repo and carried to the host by a run. Use it when a container starts listening on a port of its host, or when a host outside the internal network needs to reach one port that is closed to it.
Status: written, not yet run.
No command on the host opens a port for good. A rule added with iptables is gone after the next deploy or restart. See The firewall for the rules every host starts with.
Prerequisites¶
- The control shell, with fleet-ansible, fleet-nixos, fleet-stacks, and the private repo checked out next to each other under
~/src. See The control shell. - The right to push to the fleet-stacks repo the fleet uses, for a new port. See Get a repo you can push to.
- The host is built, healthy, and has
FIREWALL: truein the inventory.
Placeholders¶
| Placeholder | Value |
|---|---|
<host> |
The host's name in the inventory, such as tf01 |
<address> |
The host's address, ansible_host in the inventory |
<admin> |
The admin account, abbr_name followed by admin, such as mmadmin |
<container> |
The container's folder under containers/ in fleet-stacks, such as mailrise |
<port> |
The port number |
1. Choose the case¶
| You want | Do |
|---|---|
| A port that a container publishes on its host, and that no rule lists yet | Step 2, then steps 4 to 6 |
| An address outside the internal subnet to reach a port that is open to the internal subnet only | Step 3, then steps 4 to 6 |
| A port for a service of the host itself, not a container | A module in fleet-nixos that sets networking.firewall.allowedTCPPorts. See Adding a module to the flake |
A service reached through Traefik needs no port of its own. Traefik's ports 80 and 443 are already open on every host that runs it, and Traefik reaches the container over the Docker network.
Background: where a host's rules come from
Each container in fleet-stacks declares the ports it needs in its setup.yaml. The script build.py rolls those up into each stack's own setup.yaml. nixos-sync.yml then copies the entries of every stack a host runs into the host's file, nixos/hosts/<host>.json. The flake's stacks module turns each entry into a firewall rule. The module names no stack and no port itself.
An entry marked any is open to every address. An entry marked internal is open to docker_stacks_internal_subnet and to nothing else. Docker hands a published port straight to its container, past the host's own firewall chain, so the module also closes each internal port to outside addresses in Docker's DOCKER-USER chain. See Modules and options.
2. Declare a container's port¶
If the port is already listed for the stack and only the allowed addresses are wrong, skip to step 3.
In ~/src/fleet-stacks, add the port under firewall in containers/<container>/setup.yaml. This is the entry the mailrise container declares for its SMTP port:
| Key | Holds |
|---|---|
port |
The port on the host, as the container's compose.yaml publishes it |
proto |
tcp or udp. A port used with both gets two entries |
allow_from |
internal for the internal subnet only, or any for every address |
comment |
What listens on the port. It travels with the rule into the host's file |
Use internal unless the port has to be reached from outside the fleet's internal network. The entry opens the firewall and does nothing else: the container's compose.yaml still has to publish the port under ports.
Generate the stacks' files, then commit and push:
python3 scripts/build.py
git add containers/ stacks/
git commit -m "Open <port> for <container>"
git push
The output of build.py ends with Build complete. The commit holds your edit and the regenerated setup.yaml and README.md of every stack that uses the container. nixos-sync.yml reads fleet-stacks from the pushed main, so a change that is not pushed reaches no host.
3. Admit another address to an internal port¶
If every address that needs the port is inside docker_stacks_internal_subnet, skip to step 4.
In ~/src/fleet-private/hosts.yml, add docker_stacks_port_sources to the host that owns the port. This example lets bh01, in the DMZ, reach Redis on tf01:
tf01:
ansible_host: 172.16.7.111
serverHostname: "tf01"
docker_stacks_port_sources:
- port: 6379
proto: tcp
sources: ["172.16.8.111/32"]
Each entry names one port and protocol that a stack on the host opens as internal, and the IPv4 addresses or subnets that may also connect. Every other internal port stays closed to those addresses. An entry that matches no such port stops the build with a message naming the port, so a typo does not pass unnoticed.
Admit the DMZ on tf01 is this step as the build guide runs it.
4. Generate the host's file¶
From ~/src/fleet-ansible:
ansible-playbook -i ../fleet-private/hosts.yml nixos-sync.yml
git -C ../fleet-private add hosts.yml nixos/
git -C ../fleet-private diff --cached
The diff of nixos/hosts/<host>.json shows the new entry under stacks.firewall, or under portSources. A new port shows in the file of every host that runs the stack.
Check what the host's firewall will hold before anything reaches the host. From ~/src/fleet-nixos:
nix eval --json .#nixosConfigurations.<host>.config.fleet.stacks.firewall \
--override-input fleet git+file://$HOME/src/fleet-private \
--no-write-lock-file
The output lists every port of the host's stacks, the new one among them. Then commit and push, from ~/src/fleet-ansible:
5. Run the host¶
In Semaphore, run the site Template with Target set to the host's name.
From ~/src/fleet-ansible, in a shell prepared for runs after the handover. See Running from a shell again.
The run ends with failed=0 and unreachable=0 for the host. Its NixOS stage switches the host to a system with the new rule, and its last stage redeploys the Stack if the stack's files changed. Run every host whose file changed in step 4, one per run.
6. Verify the rule¶
Log in to the host and read the rules for the port:
sudo iptables -S nixos-fw | grep -E -e '--dport <port> '
sudo iptables -S DOCKER-USER | grep -E -e '--ctorigdstport <port> '
| The port is | nixos-fw shows |
DOCKER-USER shows |
|---|---|---|
any |
One line that ends in -j nixos-fw-accept |
Nothing |
internal |
One line for the internal subnet that ends in -j nixos-fw-accept |
One line that ends in -j DROP for every address outside the subnet |
internal, with more sources |
One more accept line for each source | One line that ends in -j RETURN for each source, above the drop |
Then connect to the port from a machine that should reach it, and from one that should not.
What's next¶
To close a port again, remove its entry and follow the same steps. The next deploy builds the firewall without the rule.
Not yet confirmed¶
The entries, the generated file, and the rules the module writes were read from fleet-stacks, fleet-ansible, and the flake's modules/stacks.nix. No rule has been loaded on a host.
- The rules on a host as
iptables -Sprints them, for each of the three cases in step 6. A port markedanygoes through NixOS'sallowedTCPPortsorallowedUDPPorts, and its line is expected to accept from every address. - That a switch reloads the firewall with the new rule and leaves established connections alone.
- That the run's last stage redeploys a Stack whose compose file changed in the same push.
- A module that opens a port for a service of the host itself. No module in the flake does so through
allowedTCPPortstoday, apart from the stacks module.