Ports and firewall¶
Everything SatL listens on, who is expected to reach it, and the pf(4) contract it needs on each node.
This page is meant to be handed to whoever runs the network.
Summary¶
| Port | Proto | Bound by | Reached by | Authentication |
|---|---|---|---|---|
| 2377 | TCP | every manager | other nodes in the cluster | mutual TLS against the cluster root |
| 2378 | TCP | every manager | a node that is joining, once | none — see below |
| 4789 | UDP | every node with an overlay task | other nodes in the cluster | none (VXLAN) |
| 4790–4999 | UDP | every node with an encrypted overlay task | other nodes in the cluster | IPsec ESP — see below |
| — | ESP (IP proto 50) | same | same | the ESP flow itself |
/var/run/satl.sock |
unix | every node | local operators and tooling | filesystem permissions (0660) |
Published container ports are separate and are whatever your services ask for, plus the dynamic ingress range 30000–32767 for ports the allocator assigns.
The last two rows exist only once a network is created with --opt encrypted; a cluster with no encrypted networks needs neither of them.
Nothing in this list needs to be reachable from the internet. 2377, 2378, 4789 and the encrypted-overlay range belong on the cluster's private network; the API socket is a unix socket and is not on the network at all.
2377/TCP — the internal mTLS listener¶
One TCP listener carries every internal service:
| Service | What crosses it |
|---|---|
| Raft | log replication, votes, snapshots between managers |
| Control | follower → leader forwarding of store mutations |
| Dispatcher | the worker↔manager session: heartbeats, assignments, task status |
| NodeCA | certificate signing for nodes that already have an identity |
| Health | liveness probes between managers |
Properties a network admin needs:
- Mutual TLS, always. Both ends present a certificate issued by the cluster's own root CA, and both verify the other against the cluster trust bundle. A peer whose certificate names a different cluster, or chains to a root that has been rotated away, is refused at the handshake.
- Workers open the connection; managers never dial workers. The dispatcher session is established outbound by the worker and maintained by it. That makes the firewall rule one-directional: nodes need to reach managers on 2377, not the other way round.
- Managers do dial each other on 2377 for Raft, so between managers the rule is symmetric.
- The address is
listen_addrinsatld.toml(default0.0.0.0:2377), and what peers are told to dial isadvertise_addr. Setadvertise_addrexplicitly on a multi-homed node — unset, it is derived from the interface carrying the default route, which on a cloud instance is usually the public NIC rather than the private network you meant.
2378/TCP — the NodeCA bootstrap¶
satl swarm join host:2377 derives this port itself; it is always
listen_addr's port plus one.
Why an unauthenticated port is not a hole. A node joining for the first time has no certificate — that is the entire point of joining — so it cannot present one on the mTLS port, and rustls builds a single mandatory client verifier for a server, admitting no per-service exception. Hence a second listener.
What makes it safe is the join token:
- The token is
SATL-1-<digest>-<secret>, and<digest>is a hash of the cluster's whole root CA trust bundle. - The joiner connects to 2378 and downloads that bundle.
- It hashes what it received and refuses to proceed unless the hash matches the digest in its token, with a message saying a man in the middle may have replaced or appended a root certificate.
- Only then does it submit a signing request, authenticated by the token's secret, and receive a certificate. Every subsequent connection it makes is mTLS on 2377.
So the untrusted channel carries exactly one thing — a public trust bundle — and that thing is pinned out of band by a token you copied from a manager. An attacker who can intercept 2378 cannot substitute their own CA, and one who cannot read the token cannot obtain a certificate.
Two consequences worth writing on the firewall ticket:
- 2378 only matters while a node joins. It is harmless to leave open on the private network and equally harmless to open only during a join.
- A join token is a credential. Treat it like a password: it is void the moment the root CA is rotated, and it should never appear in an argv.
4789/UDP — VXLAN¶
- The overlay data plane. One UDP socket per node, shared by every unencrypted overlay network on it: several VXLAN interfaces with the same local address and different VNIs all use it, each keeping an independent forwarding table. (Encrypted networks bind their own ports — see below.)
- Unicast only. SatL programs the forwarding tables from its own cluster state and never uses multicast, so no multicast routing or IGMP snooping is required of the fabric.
- Cleartext, unless the network opts in.
An ordinary overlay carries the container traffic as it is on this port.
A network created with
--opt encryptednever uses 4789 at all: its VTEPs bind a dedicated port from 4790–4999 and its datagrams cross the underlay as ESP — see below and Networks. - The MTU matters more than usual. VXLAN over IPv4 costs 50 bytes; SatL sets the overlay MTU to the measured underlay MTU minus 50 (minus 84 on an encrypted network, where ESP takes another 34). A path that drops IP fragments turns a wrong MTU from a throughput problem into a hang — see the overlay troubleshooting page.
- The module is not in the GENERIC kernel.
satldrunskldload -n if_vxlanitself, butif_vxlan_load="YES"in/boot/loader.confmakes a failure surface at boot rather than on the first overlay network.
4790–4999/UDP and ESP — encrypted overlays¶
This range is silent until someone runs satl network create -d overlay --opt encrypted; the how and why of the feature itself is on Networks.
What the network side of it looks like:
- One port per encrypted network, allocator-assigned from 4790–4999, not the shared 4789. The port is the only per-network selector the kernel's IPsec policy database can match on FreeBSD, so it is what keeps two encrypted networks' keyrings apart. 210 ports is generous against the number of encrypted networks a cluster will have.
- On the wire it is ESP, not VXLAN. The VXLAN datagram is wrapped in ESP transport mode (AES-128-GCM), so a capture between two nodes shows IP protocol 50. A firewall between nodes must pass ESP in addition to 4789/udp if encrypted networks are in use — and only then. No legitimate UDP ever crosses on 4790–4999: the sender's security policy encrypts or drops, and each node's own guard drops cleartext arriving on those ports.
- Cleartext on those ports is dropped, by pf.
The SPD on its own does not fail closed inbound on FreeBSD, so
satldloads asatl/guardanchor — block the range on the underlay, pass decapsulated packets onenc0— on the first encrypted network a node hosts, and flushes it when the last one leaves.pfctl -a satl/guard -srshows the live rules and their counters. Makingenc0presentation work needsnet.enc.in.ipsec_filter_mask=2, whichsatldsets node-wide, once, and deliberately never restores. - ESP costs 34 bytes per packet (measured), which is why an encrypted overlay's MTU is underlay − 84 — 1416 on a 1500 underlay — rather than the cleartext underlay − 50.
The API socket¶
/var/run/satl.sockby default (socket_pathinsatld.toml), mode0660, owned by the user and groupsatldruns as — root, soroot:wheelon a stock FreeBSD host.- There is no TCP listener for the Docker API, and no configuration key to ask for one. Remote access means SSH, or forwarding the socket yourself.
- Anyone who can reach this socket can run containers as root on the node. Treat membership of its group as equivalent to root.
The pf contract¶
SatL owns the satl/* anchors and never touches rules outside them.
The daemon refuses, in code, to load into any anchor outside satl/satl/*.
It regenerates the whole anchor ruleset on every change — there are no incremental edits — and re-asserts it periodically, so an anchor flushed by hand comes back within a minute.
An operator declares the anchors once, in /etc/pf.conf.
Translation anchors must come before filter rules, as pf requires:
A host with no firewall policy of its own needs nothing more than those three
lines plus a pass:
Enable it:
What SatL puts in the anchors¶
| Anchor | Rule | Purpose |
|---|---|---|
satl/nat |
nat on <egress> inet from <subnet> to any -> (<egress>) |
container egress. The parentheses make pf re-evaluate the interface's address, so the rule survives a DHCP renewal or an interface that comes up later |
satl/rdr |
rdr pass inet proto {tcp\|udp} from any to any port <host> -> <task ip> port <container> |
one per published port on this node. Several tasks of one service on one node share one rule with a round-robin address pool |
satl/guard |
block in log quick on <underlay> proto udp from any to any port 4790:4999, plus pass in quick on enc0 ... no state |
drops cleartext aimed at the encrypted-overlay ports; ESP-decapsulated packets arrive on enc0 and pass. Loaded on the first encrypted network the node hosts, flushed when the last one leaves |
Read them back with:
An empty anchor reports does not exist — that is not an error.
pf_mode¶
| Value | Behaviour |
|---|---|
enforce |
generate, syntax-check and load the anchors. Needed for published ports and for container egress. Requires pf enabled on the host |
check (default) |
generate and syntax-check only. Ports are allocated and displayed; no redirect is installed and nothing answers |
disabled |
generate nothing, for hosts where pf is unavailable |
Host prerequisites for container traffic¶
Container traffic is routed between the bridge and the egress interface, so the host must forward:
Without it, the NAT rule matches nothing useful and containers have no outbound connectivity — while inbound redirects still answer, which makes the symptom confusing.
satld checks the sysctl at startup and warns when it is off.
NAT also needs to know which interface to translate out of.
satld takes the interface of the host's default route unless egress_if is set in satld.toml.
Set it explicitly on a multi-homed node.
A worked firewall policy¶
For a three-node cluster on a private network 10.2.0.0/16, with services
published on the public interface:
| From | To | Port | Why |
|---|---|---|---|
| every node's private address | every manager's private address | 2377/tcp | Raft, dispatcher, control, NodeCA |
| a joining node's private address | one manager's private address | 2378/tcp | first-contact bootstrap (only while joining) |
| every node's private address | every node's private address | 4789/udp | VXLAN overlay |
| every node's private address | every node's private address | ESP (proto 50) | only with --opt encrypted networks — see above |
| the internet, or your load balancer | every node's public address | the published ports | your services |
| your workstation | every node | 22/tcp | operations |
Nothing else. In particular, the Docker API is not on this list, because it is not on the network.