clawdie-iso/firstboot/shell-pf.sh

146 lines
5.4 KiB
Bash

#!/bin/sh
# Clawdie Shell — PF Firewall Module
# Purpose: Configure PF with block-all default, SSH protection, jail NAT, glasspane VNC
# POSIX-compliant (no bash-isms)
set -eu
# Configuration (can be overridden for testing)
RC_CONF="${RC_CONF:-/etc/rc.conf}"
LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
PF_CONF="${PF_CONF:-/etc/pf.conf}"
PF_RELOAD_RCD="${PF_RELOAD_RCD:-/usr/local/etc/rc.d/pf_reload}"
# Inputs (caller sets these)
# ASSISTANT_NAME - used for jail identity (not bridge naming; bridge is always warden0)
# AGENT_NET - jail subnet (default: 192.168.100.0/24)
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
clawdie_shell_pf() {
local LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
local PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
local RC_CONF="${RC_CONF:-/etc/rc.conf}"
local AGENT_NET="${AGENT_NET:-192.168.100.0/24}"
log_msg "[pf] Setting up firewall..."
# Bridge is always warden0 (matches AGENTS.md bridge naming convention)
local BRIDGE="warden0"
local NET_ADDR="${AGENT_NET%/*}"
local NET_PREFIX="${AGENT_NET#*/}"
local BRIDGE_IP
BRIDGE_IP="$(echo "$NET_ADDR" | awk -F. '{print $1"."$2"."$3".1"}')"
# Detect external interface via default route
local EXT_IF
EXT_IF=$(route -n get default 2>/dev/null | awk '/interface:/ {print $2}')
if [ -z "$EXT_IF" ]; then
log_msg "[pf] ERROR: Could not detect external interface"
return 1
fi
log_msg "[pf] ext_if=${EXT_IF}, bridge=${BRIDGE}, net=${AGENT_NET}"
# Configure agent bridge interface
# Bridge warden0 — multi-tenant: each agent gets its own bridge + /24 (warden0, warden1, ...)
printf 'cloned_interfaces="bridge0"\n' >> "$RC_CONF"
printf 'ifconfig_bridge0_name="%s"\n' \
"$BRIDGE" >> "$RC_CONF"
printf 'ifconfig_%s="inet %s/%s up"\n' \
"$BRIDGE" "$BRIDGE_IP" "$NET_PREFIX" >> "$RC_CONF"
printf 'gateway_enable="YES"\n' >> "$RC_CONF"
# Write pf.conf
# NAT supernet 192.168.0.0/16 covers all agent subnets — adding a second
# agent later requires no PF changes, just a new bridge + /24
local ssh_public_rule
local tailscale_block=""
local tailscale_wireguard_rules=""
if [ "${FEATURE_TAILSCALE:-NO}" = "YES" ] && [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
ssh_public_rule="block in quick on \$ext_if proto tcp to port 22"
tailscale_block="# SSH is tailnet-only when installer auths Tailscale during first boot"
tailscale_wireguard_rules='pass in quick on $ext_if proto udp to port 41641 keep state
pass in quick on $ext_if inet6 proto udp to port 41641 keep state'
else
ssh_public_rule="pass in quick on \$ext_if proto tcp to port 22 keep state (max-src-conn 5, max-src-conn-rate 3/60, overload <bruteforce> flush global)"
fi
cat > "$PF_CONF" << EOF
# Clawdie-AI firewall — generated by clawdie-firstboot $(date '+%d.%b.%Y' | tr 'A-Z' 'a-z')
# See NETWORKING.md for architecture notes and Tailscale integration path
ext_if="${EXT_IF}"
tailscale_if="tailscale0"
# Multi-tenant: covers all agent subnets — no PF change needed when adding agents
agent_nets="192.168.0.0/16"
# Brute force protection table — blocked after 3 attempts / 60s
table <bruteforce> persist
# NAT for agent jails
nat on \$ext_if from \$agent_nets to any -> (\$ext_if)
# Default: block everything
block all
# Allow established outbound
pass out all keep state
# Agent jail egress
pass quick inet from \$agent_nets to any keep state
# SSH — rate-limited, brute force offenders auto-blocked
block in quick from <bruteforce>
${ssh_public_rule}
# Web (HTTP for Let's Encrypt renewals, HTTPS for agent UI)
pass in quick on \$ext_if proto tcp to port { 80, 443 } keep state
# Tailscale remote access (SSH tunnel to loopback-bound controlplane)
pass in quick on \$tailscale_if proto tcp to port 22 keep state
${tailscale_block}
${tailscale_wireguard_rules}
EOF
# Install pf_reload rc.d service
# Fixes the PF/Tailscale cold boot race — see NETWORKING.md: "The cold boot race"
# Pre-installed even without Tailscale: harmless now, essential when agent adds it
cat > "$PF_RELOAD_RCD" << 'RCEOF'
#!/bin/sh
# PROVIDE: pf_reload
# REQUIRE: tailscaled
# KEYWORD: shutdown
. /etc/rc.subr
name="pf_reload"
rcvar="pf_reload_enable"
start_cmd="pf_reload_start"
stop_cmd=":"
pf_reload_start() {
pfctl -f /etc/pf.conf
}
load_rc_config "$name"
run_rc_command "$1"
RCEOF
chmod 755 "$PF_RELOAD_RCD"
# Enable PF and pf_reload
printf 'pf_enable="YES"\n' >> "$RC_CONF"
printf 'pf_reload_enable="YES"\n' >> "$RC_CONF"
log_msg "[pf] Firewall configured: ext_if=${EXT_IF}, bridge=${BRIDGE}"
if [ "${FEATURE_TAILSCALE:-NO}" = "YES" ] && [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
log_msg "[pf] SSH restricted to tailscale0; public port 22 blocked"
else
log_msg "[pf] Public SSH remains enabled on ${EXT_IF}"
fi
echo "[PF] COMPLETE" >> "$PROGRESS_FILE"
}
# ============================================================================
# LOGGING HELPER
# ============================================================================
log_msg() {
echo "$(date '+%H:%M:%S') $1" | tee -a "$LOG_FILE" 2>/dev/null || true
}
# Only run if executed directly (not during test)
if [ "${SHELL_PF_TEST:-0}" -eq 0 ] && [ "${0##*/}" = "shell-pf.sh" ]; then
clawdie_shell_pf
fi