Skip to main content
Docker publishes a container port by adding NAT rules that send traffic straight to the container. That traffic never reaches the INPUT chain, so ufw and a “firewall is active” check say nothing about it. A search engine or database container published on 0.0.0.0 is reachable by anyone, even on a host with ufw on and a deny default. The exposure audit lists every running container’s published ports on each node and how reachable each one is. It is read-only. Nothing changes until you choose to restrict a port.

What each state means

The audit never calls a port closed on a guess. A rule with an interface match, an ipset or a custom chain jump is reported as Unknown, not Restricted.

Severity

Severity depends on what is exposed, not only that something is.
  • High: a database, key-value store, search engine, admin console or the Docker API, published publicly without an opt-in.
  • Medium: the same kinds when you opted in (a database with public access enabled), or an unrecognised service from an unmanaged container.
  • Low or info: a web port. Apps behind the ingress are expected to be reachable, ideally only through it.
The owner is shown too: a managed database, a managed app, or an unmanaged container. A managed database bound publicly without public access enabled is a drift worth fixing.

Where it shows up

  • Settings, Firewall: the Published ports table, with an expandable plain explanation per row.
  • Doctor and setup: an exposure check group in the Network and ingress category, one finding per high or medium port.
  • CLI: levelrail-cli firewall exposure.
  • MCP: get_firewall_exposure, read-only. No MCP tool can apply or remove a rule.

Check from outside

--probe (or “Check from outside” in the dashboard) dials each internet-facing TCP port on the node’s public address. For the control plane’s own host that dial comes from the host itself, and many routers block a server from reaching its own public address (hairpin NAT). So a failed dial is reported as “could not confirm”, never as closed, and a successful one is reported as “answered from this side”, which does not prove a cloud firewall in front of the host agrees.

Restrict a port

Restricting a port installs an allow-list: the sources you name can connect, everyone else is dropped.
--local-containers additionally allows Docker’s default address pool (172.16.0.0/12). If you use a custom pool, list its CIDR explicitly.

What gets written

For each restriction the control plane inserts, at the top of DOCKER-USER, one RETURN rule per allowed source followed by one DROP rule. Each rule is tagged with a comment <brand prefix>exposure:<proto>/<port> (for example levelrail:exposure:tcp/8108). They match the original destination port with conntrack --ctorigdstport, because DOCKER-USER sees traffic after NAT, when the destination port is already the container’s port. Only the original direction is matched, so replies are not dropped.

Safety

  • Dry run first: the preview shows the exact iptables commands and a sentence stating what traffic will be dropped. Applying requires explicit confirmation.
  • Lockout guard: a restriction is refused for the management API, the agent listener, ingress ports 80 and 443, and SSH. Known nodes that are not in your allow-list are listed as warnings.
  • Only its own rules: removal and re-assertion touch only rules carrying the tag. Rules you wrote are never changed or deleted.
  • Reversible: one click or unrestrict removes the rules and forgets the restriction.
  • Audited: apply and remove go through the audit log with the port in the request path, and need the root ability. The audit and preview are plain reads.
  • Off by default: nothing is applied unless you ask.

Persistence

Rules in DOCKER-USER live in the kernel only. The restriction is stored in the control plane database and a reconciler re-applies it on every pass, so it comes back after a reboot or a Docker restart while the control plane runs on that host. If you stop the control plane, rules stay until the next reboot.

Limits

  • Rules can be applied only on the control plane’s own host. For a remote node, copy the dry-run commands and run them there. An agent RPC for this is planned.
  • Only IPv4 sources and the iptables backend are supported. Docker’s experimental nftables mode has no DOCKER-USER chain and is reported as unreadable.
  • Reading the chain needs root, the same as turning on ufw. Without it, wildcard-bound ports are reported as Unknown.
  • On macOS or Docker Desktop the host firewall is not visible, so publicly bound ports are Unknown.
Last modified on October 12, 2026