#!/bin/sh # ===================================================================== # Network namespace guard for owuitools-vmagent. # # WHY A SECOND GUARD AND NOT THE EXISTING ONE # # owuitools-netguard already exists and already does this for the api # container. The agent is not put behind it because their allowlists are # different in a way that matters: the agent needs Ollama, and Ollama has # no authentication and serves DELETE /api/delete. Adding it to the api # container's allowlist would mean a URL-validation bug in fetch_page # could delete the model the whole assistant runs on. # # So the agent gets its own namespace with a strictly smaller allowlist: # owuitools-vmgw, and nothing else. vmgw is the one that may talk to # Ollama and to Open WebUI, and it exposes exactly three paths. # # The agent's other channel - vmd - is a unix socket on a bind mount, so # it does not appear here at all. That is the point of using a socket: # a filesystem permission cannot be reached by a network bug. # # The result is a hard allowlist of ONE address and ONE port. # ===================================================================== set -eu # What this applied, kept with every other component's log. `exec` so the # redirection covers the verification output at the end too, which is the # part worth having: it is the difference between "the filter ran" and # "the filter is in force". LOG_DIR="${OWUITOOLS_LOG_DIR:-/logs}" if [ -d "$LOG_DIR" ] && [ -w "$LOG_DIR" ]; then exec > >(tee -a "$LOG_DIR/vmguard.log") 2>&1 fi # From docker-compose.yml, for the reason in netguard/firewall.sh: the # address is pinned there, and a second copy here is the one that gets # forgotten. It must stay equal to OWUITOOLS_VMGW_URL on the vmagent # container - that is the address the agent dials, and this is the only # address it is allowed to reach. GW_IP="${OWUITOOLS_VMGW_IP:-172.22.2.10}" # the only permitted destination GW_PORT="${OWUITOOLS_VMGW_PORT:-8000}" echo "Configuring owuitools vmguard..." # Fail closed before opening anything. iptables -w 10 -P INPUT DROP iptables -w 10 -P OUTPUT DROP iptables -w 10 -P FORWARD DROP # Filter table only. Docker's embedded DNS resolver installs NAT rules in # this namespace; flushing nat would break name resolution. iptables -w 10 -F INPUT iptables -w 10 -F OUTPUT iptables -w 10 -F FORWARD # Loopback: the embedded resolver at 127.0.0.11 and the agent's own # healthcheck. iptables -w 10 -A INPUT -i lo -j ACCEPT iptables -w 10 -A OUTPUT -o lo -j ACCEPT iptables -w 10 -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -w 10 -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # The whole allowlist. iptables -w 10 -A OUTPUT -d "$GW_IP/32" -p tcp --dport "$GW_PORT" \ -m conntrack --ctstate NEW -j ACCEPT # Nothing may start a conversation with the agent. It claims work from a # shared volume and answers there; it serves nothing. # Verify the rules that matter actually took, and that the policies are # still DROP. A firewall that silently failed to apply is worse than # none, because everything downstream carries on as though it were # enforced - the same assertion netguard/firewall.sh makes. iptables -w 10 -C OUTPUT -d "$GW_IP/32" -p tcp --dport "$GW_PORT" \ -m conntrack --ctstate NEW -j ACCEPT test "$(iptables -S INPUT | head -n 1)" = "-P INPUT DROP" test "$(iptables -S OUTPUT | head -n 1)" = "-P OUTPUT DROP" test "$(iptables -S FORWARD | head -n 1)" = "-P FORWARD DROP" touch /tmp/firewall-ready echo echo "owuitools vmguard ready." echo " outbound $GW_IP:$GW_PORT (vmgw) and nothing else" echo " inbound nothing" echo " the host, the LAN, Ollama and Open WebUI are NOT reachable directly" echo trap 'exit 0' TERM INT while :; do sleep 3600 & wait $! done