146 lines
5.4 KiB
Bash
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
|