clawdie-iso/NETWORKING.md
Sam & Claude c47cd44251 Add .prettierrc + reformat all active markdown to match
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>
2026-05-20 11:16:42 +02:00

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 (see docs/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:

  1. Detects ext_if via route -n get default — no hardcoded interface names
  2. Creates agent bridge warden0 at 192.168.100.1/24 (matches the Clawdie-AI bridge naming convention)
  3. Writes /etc/pf.conf with block-all default, SSH protection, jail NAT
  4. Installs pf_reload rc.d service — see cold boot race below
  5. 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:

  1. pkg install tailscale
  2. sysrc tailscaled_enable="YES"
  3. tailscale up --authkey=<key> --hostname=<name>
  4. 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
  1. 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.