1# SPDX-License-Identifier: MIT
2#
3# systemd unit of the ermite runner. Install the static binary as
4# /usr/local/bin/ermite-runner, write the settings to /etc/ermite-runner/env
5# (mode 0600) and register the runner once; the README has the commands.
6#
7# The runner runs as root: it creates TAP devices and nftables rules (vm), loop
8# mounts and containers (container), and starts the jailer. Jobs never run as
9# root on the host: they run in a micro-VM or a container as user "runner".
10#
11# The sandboxing below is what can be added without breaking those jobs.
12# What is deliberately NOT set, and why, is listed at the end of the file and
13# in the README.
14[Unit]
15Description=ermite CI runner
16Documentation=https://git.bnema.dev/bnema/ermite-runner
17Wants=network-online.target
18After=network-online.target docker.service
19
20[Service]
21Type=simple
22EnvironmentFile=/etc/ermite-runner/env
23ExecStart=/usr/local/bin/ermite-runner run
24Restart=on-failure
25RestartSec=5
26
27# SIGTERM cancels the running jobs with runner_shutdown, reports them and removes
28# their sandboxes. With the defaults that takes at most 90 s (30 s for the
29# executor to stop its steps, 30 s to destroy the sandbox, 30 s to report; the
30# jobs do this in parallel). A test checks that this value leaves 15 s of margin
31# over host.Timing.ShutdownBudget.
32KillMode=mixed
33KillSignal=SIGTERM
34TimeoutStopSec=120
35
36# The state directory holds the runner token (0600), the VM images and the
37# per-job files; RuntimeDirectory is for the lock and sockets.
38StateDirectory=ermite-runner
39StateDirectoryMode=0700
40RuntimeDirectory=ermite-runner
41
42# Jobs are many processes.
43TasksMax=infinity
44LimitNOFILE=1048576
45
46# ---- sandboxing that leaves the unit in the host's mount namespace ----------
47# Only options that do not give the unit a mount namespace of its own: in
48# container mode the runner loop-mounts the scratch disk of a job, and the Docker
49# daemon, which lives in the host's namespace, must see that mount to bind it
50# into the container. See the end of the file.
51NoNewPrivileges=yes
52# The runner does not set the clock.
53ProtectClock=yes
54# No setuid/setgid files created by the runner or its children.
55RestrictSUIDSGID=yes
56LockPersonality=yes
57# The runner does not need real-time scheduling.
58RestrictRealtime=yes
59# Syscall architectures: native only.
60SystemCallArchitectures=native
61# Address families the runner and its children use: Unix sockets (Docker, the
62# executor stream), IPv4/IPv6 (the server, the DNS forwarder), netlink (ip, nft,
63# TAP devices), packet (none needed: left out), vsock (the executor stream in a
64# micro-VM).
65RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK
66
67[Install]
68WantedBy=multi-user.target
69
70# ---- deliberately absent -----------------------------------------------------
71#
72# PrivateTmp=, ProtectHome=, ProtectSystem=, ProtectKernelModules=,
73# ProtectKernelLogs=, ProtectKernelTunables=, ProtectControlGroups=,
74# PrivateDevices=, ReadOnlyPaths=, ReadWritePaths=, InaccessiblePaths=,
75# TemporaryFileSystem=, BindPaths=
76# Each of these puts the unit in a mount namespace of its own
77# (systemd sets one up for ProtectKernelModules and
78# ProtectKernelLogs too: it hides /usr/lib/modules and
79# /proc/kmsg with mounts). In container mode the runner
80# loop-mounts the scratch disk of a job below the state
81# directory, and the Docker daemon, in the host's namespace,
82# cannot see a mount made inside the unit's: the bind
83# sources of the container would be empty directories. The
84# micro-VM mode would tolerate them, but one unit serves both
85# modes and a second unit for VM-only hardening is not worth
86# it. test: internal/host TestUnitHasNoMountNamespaceOptions.
87#
88# Delegate=yes The runner does not manage cgroups of its own. The jailer
89# creates the cgroups of the VMs below /sys/fs/cgroup itself
90# (it is root and writes there directly), and Docker has its
91# own. Delegation would hand a subtree to this unit that
92# nothing uses.
93# PrivateNetwork=, RestrictNamespaces=,
94# CapabilityBoundingSet=, MemoryDenyWriteExecute=, SystemCallFilter=
95# The runner creates network namespaces and devices (TAP,
96# veth, bridges) and mounts file systems, enables
97# net.ipv4.ip_forward (the jailer also writes cgroups, /dev/kvm, /dev/net/tun and
98# /dev/loop* are needed), hands capabilities to the jailer and
99# to Docker, and starts Firecracker (which maps guest memory
100# writable and executable). Each of these would break a path
101# that cannot be tested without a rootful host; the
102# boundary of a job is the micro-VM or the container, not
103# this unit.
104# UMask=0077 The state directory is made 0700 by the runner itself, and the
105# drivers create directories that the job's user must be able
106# to enter (the scratch disk's work and tmp directories are
107# made with mode 0755): a restrictive umask would turn them
108# into root-only directories, which cannot be tested without
109# a rootful host.