Codifies the markdown format Sam applied in bd94b87 / 30cf590 as a project rule rather than per-file judgment. Prettier 3 defaults with proseWrap=preserve (no prose reflow), printWidth=80, embedded code formatting off so we don't touch fenced shell/JSON blocks. .prettierignore scopes Prettier to active docs: - excludes tmp/, cache/, node_modules/, build artifacts - excludes CHANGELOG.md + RELEASE-NOTES-*.md (hand-formatted, rigid) - excludes .archive/ and .opencode/ (historical / tooling internal) - excludes bundled bootstrap.html Reformatted 16 active .md files: padded markdown tables, blank line before lists (CommonMark-strict), `*emph*` -> `_emph_`. No content changes — diffs are all whitespace/alignment/emphasis style. Verified: `npx prettier@3 --check '**/*.md'` reports all clean. Build: not run — docs + tooling config only. Tests: pass — prettier --check is green; git diff confirms no content deletions, only formatting. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.4 KiB
Clawdie-AI Networking & Firewall
Version: v0.5.0 Updated: 12.maj.2026
Overview
Every Clawdie-AI instance runs PF as its firewall from first boot. The firewall
is configured by shell-pf.sh during clawdie-firstboot and is not optional —
all instances ship protected.
Internet
│
▼
vtnet0 (or em0, igb0, etc. — auto-detected)
│
PF ← block all default, brute-force protection
│
├── port 22 → sshd (password + key auth, rate-limited)
├── port 80 → nginx (Let's Encrypt renewals)
├── port 443 → nginx (agent UI + HTTPS)
│
└── NAT → 192.168.0.0/16 → agent jails
└── warden0 bridge → 192.168.100.0/24
Glasspane Operator Feature
The "glasspane" provides operators a visual view into browser automation:
Operator (via Tailscale)
│
├── SSH (port 22) → tmux panes → terminal view
│
└── VNC (port 5900) → wayvnc → cage → browser → visual browser view
Security model:
- VNC (5900) blocked on public interface
- VNC only accessible via Tailscale interface
- SSH (22) also moves to Tailscale-only after Tailscale activation
- All protected behind PF with block-all default
This allows operators to watch the agent perform browser automation in real-time, debug issues visually, and verify behavior — all without exposing VNC to the public internet.
Architecture note: Autonomous browser execution is handled by the browser jail / task-clone path in Clawdie-AI (see
docs/internal/BROWSER-JAIL.md). Operator credential refresh will use host-side browser sessions via xpra over SSH (seedocs/internal/OPERATOR-BROWSER-ARCHITECTURE.md). The cage/wayvnc path above describes the current ISO-shipped visual monitoring capability, not the autonomous execution surface.
What firstboot sets up
shell-pf.sh runs during firstboot and:
- Detects ext_if via
route -n get default— no hardcoded interface names - Creates agent bridge
warden0at192.168.100.1/24(matches the Clawdie-AI bridge naming convention) - Writes
/etc/pf.confwith block-all default, SSH protection, jail NAT - Installs
pf_reloadrc.d service — see cold boot race below - Enables PF via rc.conf
Agent bridge naming
The root install bridge is always warden0. It is not derived from
ASSISTANT_NAME; renaming the assistant must not rename networking
infrastructure.
| Scope | Bridge | Default subnet |
|---|---|---|
| Root install | warden0 |
192.168.100.0/24 |
| Future extra bridge 1 | warden1 |
operator-assigned |
| Future extra bridge 2 | warden2 |
operator-assigned |
Multi-tenant: PF NAT covers the entire 192.168.0.0/16 supernet. Adding a
second bridge later requires no PF NAT change — just an explicit bridge name and
a new /24 from the pool.
Brute force SSH protection
PF automatically blocks IPs that exceed the rate limit:
max-src-conn 5 — max 5 simultaneous connections from one IP
max-src-conn-rate 3/60 — max 3 new connections per 60 seconds
overload <bruteforce> — offending IP added to the bruteforce table
flush global — existing connections from that IP killed
The <bruteforce> table persists across pfctl -f reloads (not across reboots).
To inspect blocked IPs:
pfctl -t bruteforce -T show
To unblock an IP (e.g., if you locked yourself out):
pfctl -t bruteforce -T delete 1.2.3.4
To flush all blocks:
pfctl -t bruteforce -T flush
The cold boot race condition
Problem: On FreeBSD, PF starts before tailscaled:
pf: REQUIRE: FILESYSTEMS netif routing ← very early
tailscaled: REQUIRE: NETWORKING ← much later
When PF loads rules at boot, tailscale0 does not exist yet. FreeBSD PF
resolves interface names to kernel interface indexes at rule load time. A
non-existent interface gets index -1. When Tailscale comes up later and
creates tailscale0 with a real index, PF's loaded rule still holds -1 —
the mismatch means SSH packets on tailscale0 never match the pass rule.
The default block all takes over. SSH is blocked even though Tailscale is up.
Fix: pf_reload rc.d service
shell-pf.sh installs /usr/local/etc/rc.d/pf_reload on every instance:
# PROVIDE: pf_reload
# REQUIRE: tailscaled ← runs after Tailscale is up
This service reloads /etc/pf.conf after tailscaled starts, giving PF a
chance to resolve tailscale0 to its real kernel index. SSH via Tailscale
then works correctly on every boot.
Pre-installed even without Tailscale. pf_reload is harmless if
tailscaled is not installed — the rc.d script simply has no trigger.
When the agent later installs Tailscale, the race fix is already in place.
Adding Tailscale (agent's job)
When the agent installs Tailscale on a deployed instance:
pkg install tailscalesysrc tailscaled_enable="YES"tailscale up --authkey=<key> --hostname=<name>- Edit
/etc/pf.conf— uncomment the Tailscale block:
tailscale_if="tailscale0"
pass in quick on $tailscale_if proto tcp to port 22 keep state # SSH via Tailscale
block in quick on $ext_if proto tcp to port 22 # Block public SSH
pass in quick on $tailscale_if proto tcp to port 5900 keep state # wayvnc glasspane
block in quick on $ext_if proto tcp to port 5900 # Block public VNC
pass in quick on $ext_if proto udp to port 41641 keep state # Tailscale WireGuard
pass in quick on $ext_if inet6 proto udp to port 41641 keep state # Tailscale WireGuard v6
- Reload PF:
pfctl -f /etc/pf.conf
After this, SSH and VNC on the public interface are blocked. Access is via Tailscale only.
pf_reload (already installed) handles the cold boot race automatically.
Troubleshooting SSH lockout
Symptom: SSH connections refused, can only access via provider console.
Diagnosis checklist:
# Is PF running?
pfctl -s info | head -3
# Are you blocked by bruteforce table?
pfctl -t bruteforce -T show
# Is Tailscale up (if using Tailscale SSH)?
tailscale status
# Does tailscale0 exist?
ifconfig tailscale0
# Test pf.conf loads without error
pfctl -nf /etc/pf.conf
Quick recovery (from provider console):
# Disable PF temporarily to get SSH access
service pf stop
# Diagnose, fix, then re-enable
service pf start
If locked out due to Tailscale race (cold boot):
# Re-enable PF after tailscale0 is confirmed up
tailscale status # confirm connected
pfctl -f /etc/pf.conf # reload — resolves tailscale0 index
Ports reference
| Port | Protocol | Purpose |
|---|---|---|
| 22 | TCP | SSH (rate-limited, brute-force protected) — moves to Tailscale-only |
| 80 | TCP | HTTP — Let's Encrypt certificate renewals |
| 443 | TCP | HTTPS — agent UI |
| 5900 | TCP | VNC — wayvnc glasspane (Tailscale-only) |
| 41641 | UDP | Tailscale WireGuard (if Tailscale installed) |
All other inbound traffic is blocked by default.