Files
computer-playbook/roles/svc-swarm-node/templates/nftables.conf.j2
kevinveenbirkenbachandClaude Opus 5.5 788eabffbf fix(svc-swarm-node): drop outside traffic to non-public routing-mesh ports
In swarm mode `docker stack deploy` ignores the host IP of a short port
mapping, so every `127.0.0.1:<port>:<port>` that container_ports renders for
a `ports.local` entry is published by the ingress routing mesh on every
node, via docker-proxy on 0.0.0.0 and [::] plus DNAT in nat PREROUTING.
Databases, LDAP and application backends answered from outside the cluster.

svc-swarm-node now loads an nftables table through svc-net-firewall. Its
prerouting filter chain (priority mangle, ahead of Docker's dstnat) drops
packets addressed to the node for any ingress-published port that no
application declares under `ports.public`. Loopback, docker0,
docker_gwbridge, the user bridges and the swarm nodes' own addresses stay
admitted. Host-side provisioning over 127.0.0.1, containers that reach a
host port through host-gateway, and node-to-node consumers such as the
registry-cache mirror keep working.

Both sets are read live on the manager (`docker service inspect` for
`.Endpoint.Ports`, `docker node inspect` for `.Status.Addr`), so ephemeral
ingress ports and nodes outside `--limit` are covered. The table is loaded
on a node before it joins, reloaded on every node after the join, and
reloaded after each stack deploy by the sys-svc-compose swarm handler.

Refs #1089

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 20:56:29 +02:00

33 lines
1.1 KiB
Django/Jinja

table inet {{ FIREWALL_TABLE }}
delete table inet {{ FIREWALL_TABLE }}
table inet {{ FIREWALL_TABLE }} {
{% for protocol in ['tcp', 'udp'] %}
{% set _ports = SWARM_INGRESS_GUARDED_PORTS | selectattr('protocol', 'equalto', protocol) | map(attribute='port') | list %}
set guarded_{{ protocol }} {
type inet_service
{% if _ports | length > 0 %}
elements = { {{ _ports | join(', ') }} }
{% endif %}
}
{% endfor %}
{% for family in ['ipv4', 'ipv6'] %}
{% set _addresses = SWARM_INGRESS_PEER_ADDRESSES[family] %}
set peers_{{ family }} {
type {{ family }}_addr
{% if _addresses | length > 0 %}
elements = { {{ _addresses | join(', ') }} }
{% endif %}
}
{% endfor %}
chain prerouting {
type filter hook prerouting priority -150; policy accept;
{% for family, match in [('ipv4', 'ip'), ('ipv6', 'ip6')] %}
{% for protocol in ['tcp', 'udp'] %}
iifname != { "lo", "docker0", "docker_gwbridge" } iifname != "br-*" {{ match }} saddr != @peers_{{ family }} fib daddr type local {{ protocol }} dport @guarded_{{ protocol }} drop
{% endfor %}
{% endfor %}
}
}