deploy: pin podman's run root; end the pause process with the service !310

merged merged by cmc on 2026-09-07 01:52 UTC · krz/gitbay:runner-podman-storage into main

4 files changed, +80 −13

Layout: unified · split

.gitbay/wiki/Admin.org +17
@@ -469,6 +469,23 @@ cannot call =newuidmap= and the runner refuses to start. That is a
469considered trade, explained in the file and in the Threat-Model; if you 469considered trade, explained in the file and in the Threat-Model; if you
470run with =-isolation none=, set it back to =yes=. 470run with =-isolation none=, set it back to =yes=.
471 471
472*Validate podman mode on a scratch repository before pointing the runner
473at real ones.* Every deploy that switched the whole instance to
474containers and failed took CI down with it. Instead: create a throwaway
475repository the runner account can read (public, or granted read — a
476private one is "not found" to the runner and the build stays pending),
477give it one job that names the CI image, and deploy the runner with
478=-repos= naming only that repository. The production unit, with its real
479hardening, then claims nothing else; other repositories' builds queue
480until =-repos= is switched back, which is a pause, not an outage.
481
482#+begin_src sh
483gitbay repo create cmc/ci-smoke # then push a .gitbay/ci.yml naming the image
484sed -i 's#-repos krz/gitbay #-repos cmc/ci-smoke #' /etc/systemd/system/gitbay-runner.service.d/override.conf
485systemctl daemon-reload && systemctl restart gitbay-runner
486gitbay build log cmc/ci-smoke 1 # green: switch -repos back, redeploy
487#+end_src
488
472*Do not deploy an isolating runner to a host that has not been 489*Do not deploy an isolating runner to a host that has not been
473prepared.* The runner is specified to refuse to start without a working 490prepared.* The runner is specified to refuse to start without a working
474podman rather than fall back to running builds unsandboxed — a fallback 491podman rather than fall back to running builds unsandboxed — a fallback
.gitbay/wiki/Threat-Model.org +7 −3
@@ -145,9 +145,13 @@ runner, polling over SSH, clones the commit and runs its steps.
145 repository is trusted; there is no automatic fallback to it — a runner 145 repository is trusted; there is no automatic fallback to it — a runner
146 configured for podman that cannot find one refuses to start, because 146 configured for podman that cannot find one refuses to start, because
147 dropping isolation silently is worse than a stopped runner. The 147 dropping isolation silently is worse than a stopped runner. The
148 systemd drop-in still adds =ProtectSystem=full= and the kernel and 148 systemd drop-in still adds =ProtectSystem=full= and the cgroup
149 cgroup protections, and =-repos= still limits a runner to named 149 protections, and =-repos= still limits a runner to named
150 repositories. =NoNewPrivileges= is *off*: rootless podman sets up its 150 repositories. =ProtectKernelTunables= is *off*: it overmounts =/proc=
151 in the unit's namespace and the kernel then refuses a proc mount in
152 any child user namespace, which every rootless container needs; the
153 container masks the same paths for the build itself.
154 =NoNewPrivileges= is *off*: rootless podman sets up its
151 namespace with the setuid =newuidmap=, which that flag blocks, so the 155 namespace with the setuid =newuidmap=, which that flag blocks, so the
152 choice is between it and containers at all. Containers are the stronger 156 choice is between it and containers at all. Containers are the stronger
153 boundary — the flag constrained a process that was already running 157 boundary — the flag constrained a process that was already running
deploy/gitbay-runner.override.conf +18 −6
@@ -27,11 +27,15 @@
27# and the sandboxing that has to match them live in one file: -isolation 27# and the sandboxing that has to match them live in one file: -isolation
28# podman needs NoNewPrivileges=no below, and -image needs an image the 28# podman needs NoNewPrivileges=no below, and -image needs an image the
29# host has been given (deploy/runner-podman-setup.sh, Containerfile.ci). 29# host has been given (deploy/runner-podman-setup.sh, Containerfile.ci).
30# Deliberately no XDG_RUNTIME_DIR. podman records its run root in its 30# podman's run root is pinned under the runner's home by storage.conf
31# database at first use, so setting one later fails with "database 31# (runner-podman-setup.sh), not taken from XDG_RUNTIME_DIR or /tmp: this
32# configuration mismatch"; the runner's storage was initialised without 32# unit has PrivateTmp, so a /tmp run root is a per-instance tmpfs.
33# it and works. Change it only together with `podman system reset` and a 33#
34# rebuild of the images (#144). 34# The cgroupfs manager puts podman's pause process under the user slice,
35# outside this unit's cgroup, so a stop does not end it and the next
36# start joins its namespaces — including a /tmp that no longer exists.
37# End it with the service.
38ExecStopPost=-/usr/bin/pkill -u ci-runner -x catatonit
35ExecStart= 39ExecStart=
36ExecStart=/usr/local/bin/gitbay-runner -remote git@127.0.0.1 -workdir /var/lib/gitbay-runner/work -poll 5s -timeout 45m -repos krz/gitbay -isolation podman -image localhost/gitbay-ci:1 40ExecStart=/usr/local/bin/gitbay-runner -remote git@127.0.0.1 -workdir /var/lib/gitbay-runner/work -poll 5s -timeout 45m -repos krz/gitbay -isolation podman -image localhost/gitbay-ci:1
37Nice=10 41Nice=10
@@ -56,7 +60,15 @@ IOWeight=30
56# yes. Set it back if you run that way. 60# yes. Set it back if you run that way.
57NoNewPrivileges=no 61NoNewPrivileges=no
58ProtectSystem=full 62ProtectSystem=full
59ProtectKernelTunables=yes 63# ProtectKernelTunables is off, for the same reason NoNewPrivileges is
64# (#144). It overmounts /proc/sys and friends in this unit's namespace,
65# and the kernel then refuses a fresh proc mount in any child user
66# namespace ("mount too revealing"): crun fails with "mount `proc` to
67# `proc`: Operation not permitted". There is no podman setting for it.
68# What the flag protected — /proc/sys from a build running on the host
69# as this user — the container now covers: a build gets its own proc,
70# with those paths masked by the runtime. Under -isolation none, set it
71# back to yes.
60ProtectControlGroups=yes 72ProtectControlGroups=yes
61RestrictSUIDSGID=yes 73RestrictSUIDSGID=yes
62Delegate=yes 74Delegate=yes
deploy/runner-podman-setup.sh +38 −4
@@ -52,13 +52,47 @@ if [ "$max_ns" -lt 1 ]; then
52fi 52fi
53echo " max_user_namespaces=$max_ns" 53echo " max_user_namespaces=$max_ns"
54 54
55# Lingering keeps the user's systemd session — and so podman's storage 55# podman's storage paths are pinned in storage.conf, both graphroot and
56# and any running container — alive when nobody is logged in. 56# runroot, under the runner's home. Left to podman, the run root is
57# $XDG_RUNTIME_DIR or /tmp/storage-run-<uid>; the service runs with
58# PrivateTmp, so that is a per-instance tmpfs, and podman's pause process
59# (which the cgroupfs manager places outside the service cgroup) can
60# outlive a restart holding a dead /tmp — after which every podman
61# command, in any context, fails with "mkdir ...: no such file or
62# directory". A run root under the home directory is valid in every
63# namespace and needs neither lingering nor /tmp.
64#
65# podman records the run root at first use. Changing it later needs
66# `podman system reset --force` as the runner user and a rebuild of the
67# images; this script does not do that for you.
68home=$(getent passwd "$RUNNER_USER" | cut -d: -f6)
69conf="$home/.config/containers/storage.conf"
70echo "==> storage config in $conf"
71install -d -o "$RUNNER_USER" -g "$RUNNER_USER" -m 700 "$home/.config/containers"
72printf '[storage]\ndriver = "overlay"\ngraphroot = "%s/.local/share/containers/storage"\nrunroot = "%s/.local/share/containers/run"\n' "$home" "$home" >"$conf"
73chown "$RUNNER_USER:$RUNNER_USER" "$conf"
74echo " written"
75
76# podman sets net.ipv4.ping_group_range in every container by default,
77# for unprivileged ping. The service runs with ProtectKernelTunables, so
78# /proc/sys is read-only and crun fails to start the container with
79# "open /proc/sys/net/ipv4/ping_group_range: Read-only file system". A
80# build has no use for ping; drop the default rather than the hardening.
81cconf="$home/.config/containers/containers.conf"
82echo "==> container defaults in $cconf"
83printf '[containers]\ndefault_sysctls = []\n' >"$cconf"
84chown "$RUNNER_USER:$RUNNER_USER" "$cconf"
85echo " written"
86
87# Lingering keeps the user's systemd session alive when nobody is logged
88# in, which podman's pause process relies on.
57echo "==> lingering for $RUNNER_USER" 89echo "==> lingering for $RUNNER_USER"
58loginctl enable-linger "$RUNNER_USER" 90loginctl enable-linger "$RUNNER_USER"
59 91
60echo "==> verifying rootless podman as $RUNNER_USER" 92echo "==> verifying rootless podman as $RUNNER_USER"
61su - "$RUNNER_USER" -s /bin/sh -c 'podman info --format "{{.Host.Security.Rootless}}"' 93# The verification fails rather than passing with || true: a host that
94# reports ready and is not is the outage this script exists to prevent.
95su - "$RUNNER_USER" -s /bin/sh -c "podman info --format 'rootless={{.Host.Security.Rootless}} runroot={{.Store.RunRoot}}'"
62 96
63echo 97echo
64echo "host is ready; now: make deploy-runner" 98echo "host is ready. Build the CI image (deploy/Containerfile.ci), then: make deploy-runner"