# The Forgejo Actions runner of this repository's CI, on learn.halkyon.eu's server. Installed as # /etc/systemd/system/forgejo-runner.service; its configuration is config.yml, next to # this file. # # Jobs run directly on the host as the unprivileged user `runner` ("without any # isolation" from that user, as Forgejo's security guide puts it), and they run our # dependencies' build code. On the production server, so everything below keeps them # away from production and makes sure CI can't starve the instance. Docker is out of # reach entirely: `runner` isn't in the docker group, and the socket is hidden too. # # `runner` has user and group ID 61000, and must never share a user ID with a # container's processes (PostgreSQL and the server run as 999; Docker doesn't remap # them): processes of one user ID can read each other's environment and, through # /proc, their files. `useradd --system` picks 999 when it is free, so create it with # useradd --system --uid 61000 --user-group --create-home \ # --home-dir /var/lib/forgejo-runner --shell /usr/sbin/nologin runner # CI's first step checks this sandbox and fails if it leaks. [Unit] Description=Forgejo Actions runner (CI of halkyon_learn; jobs on the host) Documentation=https://forgejo.org/docs/latest/admin/actions/security/ After=network-online.target Wants=network-online.target [Service] User=runner Group=runner WorkingDirectory=/var/lib/forgejo-runner ExecStart=/usr/local/bin/forgejo-runner daemon --config /etc/forgejo-runner/config.yml # The runner's token, from a root-only file. LoadCredential=token:/etc/forgejo-runner/token Restart=on-failure RestartSec=30 # The runner waits up to 10 minutes for a running job (shutdown_timeout). TimeoutStopSec=11min # Away from production: /home (deploy's .env, the checkout, the database dumps) is # invisible, the rest of the system read-only except the runner's own directory, other # users' processes hidden, and Docker's container networks, where the database listens, # unreachable. ProtectHome=yes ProtectSystem=strict ReadWritePaths=/var/lib/forgejo-runner PrivateTmp=yes ProtectProc=invisible InaccessiblePaths=-/run/docker.sock -/run/containerd -/var/lib/docker IPAddressDeny=172.16.0.0/12 192.168.0.0/16 NoNewPrivileges=yes CapabilityBoundingSet= AmbientCapabilities= SystemCallArchitectures=native RestrictSUIDSGID=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes ProtectControlGroups=yes ProtectClock=yes ProtectHostname=yes LockPersonality=yes RestrictRealtime=yes RestrictNamespaces=yes RemoveIPC=yes # Netlink stays: Dart lists network interfaces through it. RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK # System calls no CI job needs fail with an error rather than killing the job. SystemCallFilter=~@clock @cpu-emulation @debug @module @mount @obsolete @raw-io @reboot @swap SystemCallErrorNumber=EPERM UMask=0077 # Never at the instance's expense: two of the four cores, at most 5 GB of the 8 (the # kernel reclaims from CI first above 4 GB), and the lowest priority for CPU and disk. # A cold `dart analyze` of the workspace with its lint plugin needs about 3.7 GB; with # 3 GB the kernel killed it, and OOMPolicy's default then stopped the whole unit. CPUQuota=200% MemoryHigh=4G MemoryMax=5G TasksMax=2048 Nice=10 IOSchedulingClass=best-effort IOSchedulingPriority=7 [Install] WantedBy=multi-user.target