# ===================================================================== # vmd: the privileged side of the VM sandbox. # # Runs as owuivmd, NOT root. Its four privileged needs - start and stop # the VM unit, create a project dataset with a quota, start and stop # virtiofsd - go through /usr/local/sbin/owuivm-ctl, which is the single # entry in its sudoers file and accepts a fixed verb list. # # ITS NETWORK IS AN ALLOW-LIST, WHICH IS THE OPPOSITE OF THE VM'S. # # The sandbox unit uses deny-only, because the guest should reach the # whole internet except a few ranges. vmd is the other way round: it # needs exactly one destination, Gitea on the LAN, and nothing else - # not the internet, not Ollama, not Open WebUI, not the other hosts. # # systemd.resource-control(5) checks IPAddressAllow= FIRST and grants on # a match, so an allow-list is written as `deny any` plus the addresses # that are wanted. That is the documented recipe, and it is the exact # inverse of the mistake that was made in owuivm.service - where an # `IPAddressAllow=0.0.0.0/0` silently disabled the whole deny list. # ===================================================================== [Unit] Description=owuitools VM sandbox daemon After=network-online.target zfs-mount.service Wants=network-online.target # A daemon that can start a VM while the guard table is absent would be # a daemon that can start an unfiltered VM. Requires=owuivm-nft-guard.service After=owuivm-nft-guard.service [Service] Type=simple User=owuivmd Group=owuivmd SupplementaryGroups=owuivm owuitools-agent UMask=0007 Environment=PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 EnvironmentFile=-/etc/owuivm/vmd.env ExecStart=/usr/bin/python3 /usr/local/lib/owuivm/vmd.py # --- network: one destination ----------------------------------------- IPAddressDeny=any # BOTH of these, and the second is not redundant. # # Gitea is published on 192.168.1.10:7200, and allowing only that # address does not work: systemd's filter is a BPF program on the # cgroup's packets, and by the time it sees them Docker's DNAT has # already rewritten the destination to the container's own address. # Measured - with only the .10 rule, a connect to 192.168.1.10:7200 # times out; adding gitea's docker network makes it succeed immediately. # # The second entry is gitea_default, whose only member is the gitea # container. Naming the container's current address instead would be # narrower and would break the next time it is recreated. @GITEA_ALLOW@ # The tool gateway, for the agent's web_search and read_page. The whole # owuitools_core subnet rather than the gw's address, for the reason # above: it is pinned in docker-compose.yml, and naming one container's # address is a rule that breaks the next time it is recreated. # # Measured the hard way: this was missing, and `web_search` inside a # session came back "could not reach the search service: timed out" # while the same call from a shell as the same user worked perfectly - # because a shell is not in the unit's cgroup, and the filter is on the # cgroup. The same shape as every other time this has bitten. IPAddressAllow=@TOOLS_SUBNET@ RestrictAddressFamilies=AF_UNIX AF_INET # It answers on a unix socket and listens on nothing else. SocketBindDeny=any # --- filesystem -------------------------------------------------------- ProtectSystem=strict ProtectHome=yes PrivateTmp=yes # /var/lib/owuivm/warm holds the saved machines vmd writes and reads # back. Not the image directory next to it: vmd must never be able to # touch a base image, and this keeps that true while giving it # somewhere of its own. ReadWritePaths=/run/owuivm @POOL_ROOT@/vmprojects @POOL_ROOT@/vmrun /var/lib/owuivm/warm /var/lib/owuivm/sessions # /etc/sudoers.d/60-cis-logfile makes every sudo call write here. That is # an audit control and is not being disabled for this daemon's # convenience; ProtectSystem=strict just has to make room for it, or vmd # cannot invoke its own privileged helper at all. ReadWritePaths=/var/log/sudo.log # Explicitly NOT writable: /var/lib/owuivm/base. vmd reads the frozen # images to clone from them and must never be able to change one. ReadOnlyPaths=/var/lib/owuivm/base ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes ProtectControlGroups=yes ProtectClock=yes ProtectHostname=yes ProtectProc=invisible RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=no LockPersonality=yes # NoNewPrivileges cannot be set: sudo to owuivm-ctl is how vmd starts the # VM at all, and sudo is setuid. The compensating control is that the # sudoers entry names exactly one program, and that program takes a fixed # verb list with a pattern-checked argument. NoNewPrivileges=no SystemCallArchitectures=native SystemCallFilter=@system-service MemoryMax=512M TasksMax=128 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target