Files
deerflow-factory/scripts/deerflow-firewall.sh
DATA 75315d958e Network isolation: egress firewall + named bridge
Adds the host-level egress firewall recommended by the upstream
DeerFlow team's "run in a VLAN" guidance, adapted to a Fritzbox-only
home network where LAN VLANs are not available.

- docker/docker-compose.override.yaml: pins the upstream deer-flow
  Docker network to a stable Linux bridge name br-deerflow so the
  firewall can address it without guessing Docker's auto-generated
  br-<hash>. Used as a -f overlay on top of the upstream compose file.

- scripts/deerflow-firewall.sh: idempotent up/down/status wrapper that
  installs DOCKER-USER iptables rules. Allowlist for 10.67.67.1 (Searx)
  and 10.67.67.2 (XTTS/Whisper/Ollama-local), hard block for
  192.168.3.0/24 (home LAN), 10.0.0.0/8, 172.16.0.0/12. Stateful return
  rule keeps inbound LAN access to published ports working.

- scripts/deerflow-firewall.nix: NixOS module snippet defining a
  systemd unit ordered After=docker.service so the rules survive
  dockerd restarts and follow its lifecycle. Copy into
  configuration.nix and nixos-rebuild switch.

- HARDENING.md: new section 2.7 "Network isolation (egress firewall)"
  with allow/block tables, bring-up steps, and smoke-test commands.

Guarantees: rules match on -i br-deerflow, so if the bridge does not
exist, the rules are no-ops and do not affect any other container
(paperclip, telebrowser, openclaw-gateway, ...). Stopping the
container leaves the rules in place but inert; stopping the systemd
unit removes them.
2026-04-12 14:56:26 +02:00

143 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# deerflow-firewall.sh — egress firewall for the hardened DeerFlow container.
#
# Restricts outbound traffic from the DeerFlow Docker bridge so the agent
# cannot reach LAN devices, only the Internet plus a small allowlist of
# Wireguard hosts (Searx + local model servers).
#
# Threat model: a prompt-injected agent inside the container should be
# unable to scan or attack other devices on the home LAN. Inbound LAN
# traffic to the container's published ports is unaffected because the
# rules are stateful (ESTABLISHED,RELATED returns first).
#
# Idempotent: running `up` repeatedly will only ever leave one copy of
# each rule in DOCKER-USER. Running `down` removes them all.
#
# Usage:
# deerflow-firewall.sh up # install rules
# deerflow-firewall.sh down # remove rules
# deerflow-firewall.sh status # show DOCKER-USER chain
#
# Requires: iptables, root privileges, the `DOCKER-USER` chain (created
# by the Docker daemon at startup).
set -euo pipefail
readonly BRIDGE="br-deerflow"
readonly CHAIN="DOCKER-USER"
# IPs that the container IS allowed to reach inside RFC1918:
readonly -a ALLOW_HOSTS=(
"10.67.67.1" # Searx (Wireguard)
"10.67.67.2" # XTTS / Whisper / Ollama-local (Wireguard)
)
# Subnets that the container is NOT allowed to reach:
readonly -a BLOCK_NETS=(
"192.168.3.0/24" # home LAN
"10.0.0.0/8" # rest of /8 except whitelisted /32s above
"172.16.0.0/12" # other Docker bridges + RFC1918 leftovers
)
if [[ $EUID -ne 0 ]]; then
echo "deerflow-firewall: must run as root" >&2
exit 1
fi
require_chain() {
if ! iptables -w -nL "$CHAIN" >/dev/null 2>&1; then
echo "deerflow-firewall: chain $CHAIN does not exist — is the Docker daemon running?" >&2
exit 2
fi
}
# Run iptables only if the same rule is not already present.
add_rule() {
local args=("$@")
if ! iptables -w -C "$CHAIN" "${args[@]}" 2>/dev/null; then
iptables -w -I "$CHAIN" "${args[@]}"
fi
}
# Run iptables -D until the rule is gone (in case it was added multiple times).
del_rule() {
local args=("$@")
while iptables -w -C "$CHAIN" "${args[@]}" 2>/dev/null; do
iptables -w -D "$CHAIN" "${args[@]}"
done
}
cmd_up() {
require_chain
# NOTE: -I prepends, so build the chain bottom-up.
# Final order from top of DOCKER-USER:
# 1. ESTABLISHED,RELATED -> RETURN (let inbound responses out)
# 2. -d 10.67.67.1 -> RETURN (Searx)
# 3. -d 10.67.67.2 -> RETURN (XTTS/Whisper/Ollama)
# 4. -d 192.168.3.0/24 -> REJECT (home LAN)
# 5. -d 10.0.0.0/8 -> REJECT
# 6. -d 172.16.0.0/12 -> REJECT
# Insert rules in reverse so the final order matches the spec.
for net in "${BLOCK_NETS[@]}"; do
add_rule -i "$BRIDGE" -d "$net" -j REJECT --reject-with icmp-net-prohibited
done
for host in "${ALLOW_HOSTS[@]}"; do
add_rule -i "$BRIDGE" -d "$host" -j RETURN
done
add_rule -i "$BRIDGE" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
if ! ip link show "$BRIDGE" >/dev/null 2>&1; then
echo "deerflow-firewall: WARNING: bridge $BRIDGE does not exist yet."
echo "deerflow-firewall: rules are installed and will activate as soon as you run"
echo " docker compose -f deer-flow/docker/docker-compose.yaml \\"
echo " -f docker/docker-compose.override.yaml up -d"
fi
echo "deerflow-firewall: up"
}
cmd_down() {
require_chain
for net in "${BLOCK_NETS[@]}"; do
del_rule -i "$BRIDGE" -d "$net" -j REJECT --reject-with icmp-net-prohibited
done
for host in "${ALLOW_HOSTS[@]}"; do
del_rule -i "$BRIDGE" -d "$host" -j RETURN
done
del_rule -i "$BRIDGE" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
echo "deerflow-firewall: down"
}
cmd_status() {
require_chain
echo "DOCKER-USER chain (relevant rules):"
iptables -w -nL "$CHAIN" --line-numbers | grep -E "$BRIDGE|^Chain|^num" || true
if ip link show "$BRIDGE" >/dev/null 2>&1; then
echo
echo "Bridge $BRIDGE: present"
else
echo
echo "Bridge $BRIDGE: NOT present (DeerFlow container not started)"
fi
}
case "${1:-}" in
up) cmd_up ;;
down) cmd_down ;;
status) cmd_status ;;
*)
echo "Usage: $0 {up|down|status}" >&2
exit 64
;;
esac