August 11, 2026

The network-exposure-baseline Skill — Restricted Bind as the Default

How the network-exposure-baseline skill classifies every port bind into four tiers, blocks public exposure without a justification, and integrates as a gate into /dr-plan, /dr-do, and /dr-archive.

Public-by-default is breach-by-default. In a prior incident in the Arcana ecosystem, Redis 7.x listened on 0.0.0.0:6379 without authentication and Postgres ran with listen_addresses='*' and the default password — both reachable from the public internet, which led to a regulator abuse report. The root cause was that docker compose binds to 0.0.0.0 by default and there was no CI gate. The network-exposure-baseline skill exists to prevent that class of mistake.

The skill must be loaded before any change that touches Docker Compose ports, Redis bind addresses, Postgres listen_addresses, systemd socket ListenStream values, or firewall rules. It does not apply to purely internal refactoring with no networking change.

The four tiers

Every bind target falls into one of four tiers. Tier 0 is a Unix socket or no published port — always allowed, no justification required. Tier 1 is loopback: 127.0.0.1, ::1, or their IPv6-mapped equivalents — always allowed. Tier 2 is the Tailscale CGNAT range (100.64.0.0/10) — mesh-only by definition, always allowed.

Tier 3 covers everything else: 0.0.0.0, ::, the wildcard *, specific public IPv4 addresses, RFC1918 private ranges, and link-local addresses. RFC1918 private is Tier 3 because a static linter cannot prove a private-range bind is safe — a private address bridged onto a public-routed interface is a real exposure. Loopback and Tailscale CGNAT are safe by construction; everything else needs a human justification.

Justification format and TTL

A Tier 3 bind requires two fields added to the service in docker-compose.yml: x-exposure-justification with a description of the actual mitigation, and x-exposure-expires with a date no more than 90 days in the future. The TTL is not optional. Without a forced expiry, waivers accumulate and are forgotten. A missing or expired TTL is a FAIL, not a warning.

For non-compose configs — Redis, Postgres, systemd sockets — the same information goes in an inline comment: # exposure: justified expires=YYYY-MM-DD — reason.

Short-form ports in docker-compose (for example "5432:5432" without a host IP) are an implicit 0.0.0.0 bind and fail by default. Docker Compose variable interpolation like ${TAILNET_IP}:443:443 is resolved from the environment before classification; if the variable is unset with no default, the linter emits a warning and passes — it cannot know the runtime value.

Pipeline integration

The gate runs at four pipeline stages. At /dr-prd, a Network Exposure Baseline section is mandatory in the PRD with the tier declaration and Tier 3 justifications. At /dr-plan, a warning is emitted if the plan touches networking surfaces without an explicit tier classification. At /dr-do, any new 0.0.0.0 bind or short-form port without justification blocks the step. At /dr-archive, every Tier 3 bind must have an unexpired justification.

The canonical verifier is dev-tools/network-exposure-check.sh. Any drift between the skill document and the script is a defect — both must be updated together.

Read what Datarim is for broader context, or see the related post on the infra-automation skill for server operation patterns that rely on correct bind configuration.