Commit 748122ef10

748122ef1028a1ae48e703996414cd25ae938508

parent: 7acfcebdf5

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 03:46 UTC

runner host: flood test asserts the table's effect; exact rollback of the builds table

Ref #260

Layout: unified · split

.gitbay/wiki/Admin.org +30 −8
@@ -976,7 +976,12 @@ private range, allow it in the file first or builds resolve nothing.
976A restart of =nftables.service= flushes both tables; 976A restart of =nftables.service= flushes both tables;
977=systemctl reload gitbay-runner-egress= restores both. 977=systemctl reload gitbay-runner-egress= restores both.
978 978
979Checking the tables on a running host, during a build: 979The egress unit's stop and the rollback below use =nft destroy=,
980which needs nftables 1.0.8 or later; check =nft --version= on a new
981host.
982
983Checking the tables on a running host, during a build (the flood test
984below waits a minute before its first probe, for this):
980 985
981#+begin_src sh 986#+begin_src sh
982nft list table inet gitbay_builds # counters on the reject rules 987nft list table inet gitbay_builds # counters on the reject rules
@@ -986,15 +991,32 @@ for p in $(pgrep -u ci-runner pasta); do cat /proc/$p/cgroup; done
986 991
987A pasta process anywhere else — the runner's own =runner= cgroup, a 992A pasta process anywhere else — the runner's own =runner= cgroup, a
988user slice — means the builds table does not see that build's traffic 993user slice — means the builds table does not see that build's traffic
989and only the first table applies. Then run 994and only the first table applies. Run
990=deploy/runner-auth-flood-test.sh= on the scratch repository (below), 995=deploy/runner-auth-flood-test.sh= on the scratch repository (below),
991once as a push and once with =--untrusted=; it prints what the build 996once as a push and once with =--untrusted=. It fails if the runner was
992reached and fails if the runner was locked out. 997locked out or if the build log lacks the lines only a working table
998produces. The authoritative proof that pasta's sockets are in the
999build's cgroup is the untrusted run: =169.254.1.2:22= and
1000=github.com:22= refused, all twelve logins refused, and the counters on
1001the =untrusted= chain's rejects rising. The first table lets
1002=ci-runner= reach both of those addresses.
1003
1004To take the builds table out, on the host:
1005
1006#+begin_src sh
1007sed -i '/^ExecStartPre=+.*builds/d' /etc/systemd/system/gitbay-runner.service.d/override.conf
1008rm /etc/gitbay-runner/builds.nft
1009systemctl daemon-reload
1010nft destroy table inet gitbay_builds
1011#+end_src
993 1012
994To take the builds table out: delete the three =ExecStartPre= lines 1013The runner needs no restart. It still places builds under
995that name =builds= from the drop-in, =systemctl daemon-reload=, and 1014=builds/trusted= and =builds/untrusted=, but with the file gone neither
996=nft destroy table inet gitbay_builds=. The runner needs no restart, 1015a runner start nor =systemctl reload gitbay-runner-egress= loads the
997and builds keep the first table. 1016table again, and builds keep the first table and =--no-map-gw=. A
1017runner that takes =-untrusted= or polls over loopback refuses to start
1018without build cgroups at all, since the table would match nothing. The
1019next =make deploy-runner= installs the file and the lines again.
998 1020
999The drop-in sets =NoNewPrivileges=no=, without which rootless podman 1021The drop-in sets =NoNewPrivileges=no=, without which rootless podman
1000cannot call =newuidmap= and the runner refuses to start. That is a 1022cannot call =newuidmap= and the runner refuses to start. That is a
deploy/gitbay-runner-egress.service +5 −4
@@ -15,9 +15,10 @@
15# flush ruleset removes it); delete would fail and leave the unit failed. 15# flush ruleset removes it); delete would fail and leave the unit failed.
16# 16#
17# The builds table (gitbay-runner-builds.nft) is loaded by the runner's 17# The builds table (gitbay-runner-builds.nft) is loaded by the runner's
18# own start, against its cgroups. Reload loads it again when those 18# own start, against its cgroups. Reload loads it again when the file is
19# cgroups exist, so after a restart of nftables.service one reload puts 19# installed and those cgroups exist, so after a restart of
20# both tables back. 20# nftables.service one reload puts both tables back; without the file
21# (taken out per the Admin page) the reload skips it and succeeds.
21[Unit] 22[Unit]
22Description=Host egress rule for CI builds 23Description=Host egress rule for CI builds
23After=nftables.service ufw.service 24After=nftables.service ufw.service
@@ -28,7 +29,7 @@ Type=oneshot
28RemainAfterExit=yes 29RemainAfterExit=yes
29ExecStart=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft 30ExecStart=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft
30ExecReload=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft 31ExecReload=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft
31ExecReload=/bin/sh -c 'if [ -d /sys/fs/cgroup/system.slice/gitbay-runner.service/builds/untrusted ]; then exec /usr/sbin/nft -f /etc/gitbay-runner/builds.nft; fi' 32ExecReload=/bin/sh -c 'if [ -f /etc/gitbay-runner/builds.nft ] && [ -d /sys/fs/cgroup/system.slice/gitbay-runner.service/builds/untrusted ]; then exec /usr/sbin/nft -f /etc/gitbay-runner/builds.nft; fi'
32ExecStop=/usr/sbin/nft destroy table inet gitbay_runner 33ExecStop=/usr/sbin/nft destroy table inet gitbay_runner
33ExecStop=/usr/sbin/nft destroy table inet gitbay_builds 34ExecStop=/usr/sbin/nft destroy table inet gitbay_builds
34 35
deploy/runner-auth-flood-test.sh +33 −9
@@ -10,14 +10,15 @@
10# deploy/runner-auth-flood-test.sh cmc/runner-scratch # trusted: a push to main 10# deploy/runner-auth-flood-test.sh cmc/runner-scratch # trusted: a push to main
11# deploy/runner-auth-flood-test.sh cmc/runner-scratch --untrusted # a merge request from a fork 11# deploy/runner-auth-flood-test.sh cmc/runner-scratch --untrusted # a merge request from a fork
12# 12#
13# The build's logins use a key registered with --ttl 1s and expired by 13# The build's logins use a git-scoped key registered with --ttl 1s and
14# the time the build runs. With registration open an unknown key is 14# expired by the time the build runs. With registration open an
15# admitted to run register and never counts against the SSH auth 15# unknown key is admitted to run register and never counts against the
16# limiter; an expired key counts (internal/sshd/sshd.go, authenticate). 16# SSH auth limiter; an expired key counts (internal/sshd/sshd.go, authenticate).
17# The key is removed from the account when the script exits. 17# The key is removed from the account when the script exits.
18# 18#
19# The step probes what the build reaches, then makes 12 logins without 19# The step waits a minute, so the operator can find pasta's cgroup
20# pause, so the limiter (ssh_auth_rate, 10 a minute per address) locks 20# (Admin page), probes what the build reaches, then makes 12 logins
21# without pause, so the limiter (ssh_auth_rate, 10 a minute per address) locks
21# the address those logins come from for most of the next minute. The 22# the address those logins come from for most of the next minute. The
22# runner reports the result right after the step; its report retries 23# runner reports the result right after the step; its report retries
23# for half a minute. 24# for half a minute.
@@ -34,6 +35,14 @@
34# names that address, the build succeeds and the runner keeps polling. 35# names that address, the build succeeds and the runner keeps polling.
35# Untrusted: every login is refused by the builds table before it 36# Untrusted: every login is refused by the builds table before it
36# reaches sshd, and no auth.* entry comes from the build at all. 37# reaches sshd, and no auth.* entry comes from the build at all.
38#
39# The script also requires lines in the build log, so a missing ssh or
40# bash in the image, or a table that matches nothing, fails rather than
41# passes. Trusted: "logins 12 denied" (every login reached sshd) and
42# 10.0.0.1:80 refused, not timed out. Untrusted: 169.254.1.2:22 and
43# github.com:22 refused and "12 refused". The untrusted lines are the
44# proof that pasta's sockets are in the build's cgroup: the uid table
45# lets ci-runner reach both.
37set -eu 46set -eu
38 47
39repo=${1:-} 48repo=${1:-}
@@ -48,13 +57,15 @@ tmp=$(mktemp -d)
48fp= 57fp=
49cleanup() { 58cleanup() {
50 if [ -n "$fp" ]; then gitbay keys remove "$fp" >/dev/null || echo "remove key $fp by hand" >&2; fi 59 if [ -n "$fp" ]; then gitbay keys remove "$fp" >/dev/null || echo "remove key $fp by hand" >&2; fi
60 fp=
51 rm -rf "$tmp" 61 rm -rf "$tmp"
52} 62}
53trap cleanup EXIT 63trap cleanup EXIT
64trap 'cleanup; exit 130' INT TERM
54 65
55echo "==> an expired key" 66echo "==> an expired key"
56ssh-keygen -q -t ed25519 -N '' -C auth-flood-260 -f "$tmp/key" 67ssh-keygen -q -t ed25519 -N '' -C auth-flood-260 -f "$tmp/key"
57gitbay keys add --label auth-flood-260 --ttl 1s <"$tmp/key.pub" >/dev/null 68gitbay keys add --scope git --label auth-flood-260 --ttl 1s <"$tmp/key.pub" >/dev/null
58fp=$(ssh-keygen -lf "$tmp/key.pub" | awk '{print $2}') 69fp=$(ssh-keygen -lf "$tmp/key.pub" | awk '{print $2}')
59sleep 2 70sleep 2
60 71
@@ -87,6 +98,7 @@ cat >.gitbay/flood.sh <<'EOF'
87#!/bin/sh 98#!/bin/sh
88# Written by deploy/runner-auth-flood-test.sh (#260). 99# Written by deploy/runner-auth-flood-test.sh (#260).
89set -u 100set -u
101sleep 60
90key=/tmp/flood.key 102key=/tmp/flood.key
91cp .gitbay/flood.key "$key" 103cp .gitbay/flood.key "$key"
92chmod 600 "$key" 104chmod 600 "$key"
@@ -104,7 +116,7 @@ probe() {
104 esac 116 esac
105} 117}
106getent hosts proxy.golang.org >/dev/null && echo "dns ok" || echo "dns failed" 118getent hosts proxy.golang.org >/dev/null && echo "dns ok" || echo "dns failed"
107for t in 127.0.0.1:22 127.0.0.1:2222 "$host:22" "$host:80" "$host:443" "$host:2222" \ 119for t in "$host:22" "$host:80" "$host:443" "$host:2222" \
108 10.0.0.1:80 192.168.0.1:80 proxy.golang.org:443 github.com:22; do 120 10.0.0.1:80 192.168.0.1:80 proxy.golang.org:443 github.com:22; do
109 probe "${t%:*}" "${t##*:}" 121 probe "${t%:*}" "${t##*:}"
110done 122done
@@ -159,7 +171,8 @@ second=$(seen)
159echo " $account last seen $first, then $second" 171echo " $account last seen $first, then $second"
160 172
161echo "==> build log" 173echo "==> build log"
162gitbay build log "$repo" "$n" | sed -n '/^dns /,$p' 174log=$(gitbay build log "$repo" "$n")
175printf '%s\n' "$log" | sed -n '/dns /,$p'
163 176
164echo "==> auth audit, last 15 minutes" 177echo "==> auth audit, last 15 minutes"
165gitbay audit --action auth. --since 15m --json | 178gitbay audit --action auth. --since 15m --json |
@@ -173,5 +186,16 @@ if gitbay audit --action auth.throttled --since 15m --json | jq -e '.data[] | se
173 echo "FAIL: 127.0.0.1, the runner's address, was throttled" 186 echo "FAIL: 127.0.0.1, the runner's address, was throttled"
174 fail=1 187 fail=1
175fi 188fi
189need() {
190 printf '%s\n' "$log" | grep -Eq "$1" || { echo "FAIL: the build log lacks \"$2\""; fail=1; }
191}
192if [ "$mode" = trusted ]; then
193 need 'logins +12 denied' "logins 12 denied"
194 need 'refused +10\.0\.0\.1:80( |$)' "refused 10.0.0.1:80"
195else
196 need 'refused +169\.254\.1\.2:22( |$)' "refused 169.254.1.2:22"
197 need 'refused +github\.com:22( |$)' "refused github.com:22"
198 need '12 refused' "12 refused"
199fi
176[ $fail = 0 ] && echo "PASS ($mode): the build's failed logins did not lock the runner out" 200[ $fail = 0 ] && echo "PASS ($mode): the build's failed logins did not lock the runner out"
177exit $fail 201exit $fail