114 lines
4.8 KiB
Desktop File
114 lines
4.8 KiB
Desktop File
# =====================================================================
|
|
# 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
|