2026-04-06 06:48:25 +02:00
|
|
|
# Clawdie-AI Networking & Firewall
|
|
|
|
|
|
|
|
|
|
**Version:** v0.5.0
|
|
|
|
|
**Updated:** 06.apr.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
|
|
|
|
|
└── clawdie0 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 → Chromium → visual browser view
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Security model:**
|
2026-05-12 08:36:14 +02:00
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
- 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.
|
|
|
|
|
|
2026-05-12 08:36:14 +02:00
|
|
|
> **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.
|
|
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## What firstboot sets up
|
|
|
|
|
|
|
|
|
|
`shell-pf.sh` runs during firstboot and:
|
|
|
|
|
|
|
|
|
|
1. **Detects ext_if** via `route -n get default` — no hardcoded interface names
|
2026-05-12 18:02:16 +02:00
|
|
|
2. **Creates agent bridge** — currently `${ASSISTANT_NAME}0` (e.g., `clawdie0`), **must be `warden0`** (see alignment note below)
|
2026-04-06 06:48:25 +02:00
|
|
|
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
|
|
|
|
|
|
2026-05-12 18:02:16 +02:00
|
|
|
> **CRITICAL ALIGNMENT BUG:** Clawdie-AI `src/jail-config.ts` expects the host
|
|
|
|
|
> bridge to be named `warden0`. `shell-pf.sh:32` currently creates
|
|
|
|
|
> `${ASSISTANT_NAME:-clawdie}0` instead. **Jails will fail to route on first
|
|
|
|
|
> boot** because the bridge name doesn't match what the runtime looks up.
|
|
|
|
|
>
|
|
|
|
|
> **Fix:** Change `shell-pf.sh:32` to `local BRIDGE="warden0"`. Single-tenant
|
|
|
|
|
> uses `warden0`. Multi-tenant adds `warden1`, etc. This matches `AGENTS.md`
|
|
|
|
|
> bridge naming convention and `shell-env.sh:198-200` which already writes
|
|
|
|
|
> `WARDEN_*` vars.
|
|
|
|
|
>
|
|
|
|
|
> The table below reflects current (broken) behavior, not the target.
|
2026-05-12 08:36:14 +02:00
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
The bridge interface is named after the agent: `${ASSISTANT_NAME}0`.
|
|
|
|
|
|
2026-05-12 08:36:14 +02:00
|
|
|
| Agent name | Bridge | Subnet |
|
|
|
|
|
| ---------- | ---------- | ------------------ |
|
|
|
|
|
| `clawdie` | `clawdie0` | `192.168.100.0/24` |
|
|
|
|
|
| `natasha` | `natasha0` | `192.168.101.0/24` |
|
|
|
|
|
| `clawd` | `clawd0` | `192.168.102.0/24` |
|
2026-04-06 06:48:25 +02:00
|
|
|
|
|
|
|
|
**Multi-tenant:** PF NAT covers the entire `192.168.0.0/16` supernet. Adding a
|
|
|
|
|
second agent to the same host requires no PF changes — just a new bridge and a
|
|
|
|
|
new `/24` from the pool. Set `AGENT_NET` in `build.cfg` per deployment.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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:**
|
2026-05-12 08:36:14 +02:00
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
```sh
|
|
|
|
|
pfctl -t bruteforce -T show
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**To unblock an IP (e.g., if you locked yourself out):**
|
2026-05-12 08:36:14 +02:00
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
```sh
|
|
|
|
|
pfctl -t bruteforce -T delete 1.2.3.4
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**To flush all blocks:**
|
2026-05-12 08:36:14 +02:00
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
```sh
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```pf
|
|
|
|
|
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
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
5. 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:**
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
# 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):**
|
2026-05-12 08:36:14 +02:00
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
```sh
|
|
|
|
|
# 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):**
|
2026-05-12 08:36:14 +02:00
|
|
|
|
2026-04-06 06:48:25 +02:00
|
|
|
```sh
|
|
|
|
|
# Re-enable PF after tailscale0 is confirmed up
|
|
|
|
|
tailscale status # confirm connected
|
|
|
|
|
pfctl -f /etc/pf.conf # reload — resolves tailscale0 index
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Ports reference
|
|
|
|
|
|
2026-05-12 08:36:14 +02:00
|
|
|
| Port | Protocol | Purpose |
|
|
|
|
|
| ----- | -------- | ------------------------------------------------------------------- |
|
2026-04-06 06:48:25 +02:00
|
|
|
| 22 | TCP | SSH (rate-limited, brute-force protected) — moves to Tailscale-only |
|
2026-05-12 08:36:14 +02:00
|
|
|
| 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) |
|
2026-04-06 06:48:25 +02:00
|
|
|
|
|
|
|
|
All other inbound traffic is blocked by default.
|