Skip to content
Agentmart
Submit a tool

network-exposure-baseline

Allowlist/blocklist for network bind targets (compose ports, redis bind, postgres listen_addresses, systemd ListenStream); load before any port change.

Skill by Arcanada-one

IndexedMIT★ 16
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/arcanada-one-network-exposure-baseline

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Allowlist/blocklist for network bind targets (compose ports, redis bind, postgres listen_addresses, systemd ListenStream); load before any port change.

Use cases: backend

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/network-exposure-baseline

What's inside

Skill name
network-exposure-baseline
Description
Allowlist/blocklist for network bind targets (compose ports, redis bind, postgres listen_addresses, systemd ListenStream); load before any port change.
License
MIT
Instructions
328 lines in SKILL.md · view on GitHub

Files (1)

  • SKILL.md

SKILL.md (first lines)

# Network Exposure Baseline

## When To Use

Load THIS skill before any change that touches: `docker-compose` ports / expose; `redis.conf` bind / protected-mode; `postgresql.conf` listen_addresses; systemd `.socket` ListenStream; a bare network listener (a runtime bind argument such as `host=0.0.0.0`); firewall / UFW rules.

NOT for: purely internal refactoring with no networking change; purely application-logic work.

## Why It Matters (founding principle)

Public-by-default = breach-by-default.

Background: in a prior incident in the ecosystem, Redis 7.x listened on `0.0.0.0:6379` without auth and Postgres ran with `listen_addresses='*'` and the default password — both were reachable from the public internet, which led to an abuse report from the regulator. Root cause: docker compose default-binds to `0.0.0.0` and there was no CI/CD gate.

The goal of this baseline: make a restricted bind the default; public exposure is opt-in with a justification and a TTL.

## Tier Model (canonical)

| Tier | Bind targets | Justification | Example |
|---|---|---|---|
| Tier 0 | unix socket / no port published | not required | `/run/redis/redis.sock` |
| Tier 1 | `127.0.0.1`, `::1`, `::ffff:127.0.0.1` | not required | `127.0.0.1:5432` |
| Tier 2 | Tailscale CGNAT `100.64.0.0/10` + `::ffff:100.64.0.0/10` | not required (mesh-only by definition) | `100.65.1.5:5432` |
| Tier 3 | `0.0.0.0`, `::`, public IPs, mapped public IPv6, `*`, `listen_addresses='*'`, **any specific public IPv4**, **RFC1918 private (`10/8`, `172.16/12`, `192.168/16`)**, **link-local (`169.254/16`)** | REQUIRED — `x-exposure-justification` + `x-exposure-expires` (≤90 days) | `203.0.113.10:443`, `192.168.1.50:5432`, `0.0.0.0:443` + Cloudflare ACL |

> **Why specific public AND private IPv4 are both Tier 3.** A static linter cannot read the host routing table, so it cannot prove that a private RFC1918 (or link-local) bind is mesh-only the way a loopback bind is safe-by-construction — a private address bridged onto a public-routed interface is a real exposure. Block-by-default folds all of them into Tier 3: the justification forces a human to record *why* the bind is safe, and the TTL forces re-review. Only loopback and Tailscale CGNAT are pass-by-construction. A genuinely unparseable bind string (not a dotted-quad, not a recognised IPv6) remains `malformed` (exit 2) — distinct from a Tier-3 violation (exit 1), which is a valid bind missing its annotation.

## Decision Tree

Walk this tree for every bind target in the diff.

```mermaid
flowchart TD
    A([Start]) --> B{Port published?}
    B -->|No| C[Tier 0 PASS]
    B -->|Yes| D{Host IP extractable<br/>from bind string?}
    D -->|No / short-form| E[Implicit 0.0.0.0<br/>Tier 3 without justification]
    E --> F[FAIL]
    D -->|Yes| G{Classify IP}
    G -->|127.0.0.1 / ::1 /<br/>::ffff:127.0.0.1| H[Tier 1 PASS]
    G -->|100.64.0.0/10 /<br/>::ffff:100.64.0.0/10| I[Tier 2 PASS]
    G -->|0.0.0.0 / :: / [::] /<br/>* / listen_addresses=*| J[Tier 3]
    G -->|Specific public IPv4| J
    G -->|RFC1918 private /<br/>169.254 link-local| J
    G -->|Other mapped IPv6| J
    G -->|Other IPv6| J
    G -->|Not an IP at all| F
    J --> K{Justification + TTL valid?<br/>expires ≤90d, in future}
    K -->|Yes| L[Tier 3 PASS]
    K -->|No| F
```

## Allowlist (verbatim)

These targets are allowed with no extra justification.

- `127.0.0.1`
- `::1`
- `::ffff:127.0.0.1`
- `100.64.0.0/10` (Tailscale CGNAT)
- `::ffff:100.64.0.0/10` (mapped Tailscale)
- unix socket paths (anything starting with `/`, ending with `.sock`)

## Blocklist (default deny)

Anything not in the allowlist and without a valid justification is denied by default.

- `0.0.0.0`
- `::` / `[::]` / `0:0:0:0:0:0:0:0`
- `*`
- `listen_addresses = '*'`
- `bind 0.0.0.0`
- `ListenStream=0.0.0.0` / `[::]`
- short-form ports (for example `"5432:5432"`, `"6379"`) — implicit `0.0.0.0`
- Dockerfile `EXPOSE` without host context — emit warn, do not fail by default

## Justification format

Tier 3 requires an explicit exposure justification.

### Primary — YAML extension (docker-compose)

```yaml
services:
  api:
    ports:
      - "0.0.0.0:443:443"
    x-exposure-justification: "Public HTTPS endpoint behind edge ACL + rate-limit"
    x-exposure-expires: "YYYY-MM-DD"
```

### Fallback — inline comment (non-compose)

```
# exposure: justified expires=YYYY-MM-DD — short reason
bind 100.64.1.5
```

### Authorship-time rule

When writing any wildcard bind (`0.0.0.0:PORT`, `[::]:PORT`) into a config file, env template, or systemd/launchd unit, the inline `# exposure:` justification annotation MUST be added in the SAME edit that introduces the bind. Do not defer the annotation to a review or compliance stage — it is zero-logic, zero-risk, and always knowable at authorship time; deferring it just converts a free fix into a review finding.

### TTL rule

- Mandatory for Tier 3.
- The `expires` date MUST be in the future and ≤90 days from the file's last modification date.
- Missing or expired (when required) → FAIL.
- Rationale: waivers accumulate and are forgotten; the TTL forces a quarterly review.

## Compose variable interpolation (`${VAR}` in a port string)

A docker-compose port string is often a shell-style parameter expansion — e.g. a mesh bind `${TAILNET_IP:?tailnet ip required}:443:443`, or a dev-compose published port `${PORT:-3700}:3700`. The linter reads the string verbatim (it never shell-expands it) and resolves it with the **B3 hybrid** strategy, in order:

1. **env-resolve** — if the named environment variable is set, its value is classified. (Resolution is `printenv`-only; the value is never executed.)
2. **default-extract** — for the `${VAR:-D}` / `${VAR-D}` forms with the variable unset, the default literal `D` is classified. So `${BIND_HOST:-0.0.0.0}` with no env is a Tier-3 violation, and `${BIND_HOST:-127.0.0.1}` passes.
3. **unresolved residual** — for `${VAR:?msg}` or bare `${VAR}` in the **host** slot with the variable unset and no default, the linter first checks the variable *name* (see mesh var-name recognition below); if the name is not a tailnet name it emits a **WARN naming the variable + `file:line` and PASSES** (exit 0). It cannot know the runtime value in CI where the variable is intentionally unset, and false-failing every legitimate mesh bind would make the linter a CI nuisance.

**Interpolation applies to every port segment, not only the host**. Steps 1–2 resolve a `${VAR:-D}` / `${VAR}` in the published-port or target-port slot too: `127.0.0.1:${PORT:-3700}:3700` resolves to `127.0.0.1:3700:3700` (Tier 1 pass), and the short-form `${PORT:-3700}:3700` resolves to `3700:3700` — a genuine implicit-`0.0.0.0` short-form Tier-3 violation (no longer an "unrecognized port form"). The modifier colon of `${VAR:-D}` / `${VAR:?msg}` is not a segment separator; only the `:` between host / published / target ports splits segments.

From Arcanada-one/datarim, MIT.

More from Arcanada-one/datarim

Signals

Stars
16
Forks
1
Open issues
0
Last commit (this folder)
Oct 4, 2026 (26 days ago)
Last release
Oct 22, 2026 (7 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from Arcanada-one

Cite this page

network-exposure-baseline skill. Agent Mart. https://agentmart.dev/skills/arcanada-one-network-exposure-baseline (last updated 2026-10-24)