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.
976976A restart of =nftables.service= flushes both tables;
977977=systemctl reload gitbay-runner-egress= restores both.
978978
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):
980985
981986#+begin_src sh
982987nft 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
986991
987992A pasta process anywhere else — the runner's own =runner= cgroup, a
988993user 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
990995=deploy/runner-auth-flood-test.sh= on the scratch repository (below),
991once as a push and once with =--untrusted=; it prints what the build
992reached and fails if the runner was locked out.
996once as a push and once with =--untrusted=. It fails if the runner was
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
9931012
994To take the builds table out: delete the three =ExecStartPre= lines
995that name =builds= from the drop-in, =systemctl daemon-reload=, and
996=nft destroy table inet gitbay_builds=. The runner needs no restart,
997and builds keep the first table.
1013The runner needs no restart. It still places builds under
1014=builds/trusted= and =builds/untrusted=, but with the file gone neither
1015a runner start nor =systemctl reload gitbay-runner-egress= loads the
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.
9981020
9991021The drop-in sets =NoNewPrivileges=no=, without which rootless podman
10001022cannot call =newuidmap= and the runner refuses to start. That is a
deploy/gitbay-runner-egress.service +5 −4
@@ -15,9 +15,10 @@
1515# flush ruleset removes it); delete would fail and leave the unit failed.
1616#
1717# 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
19# cgroups exist, so after a restart of nftables.service one reload puts
20# both tables back.
18# own start, against its cgroups. Reload loads it again when the file is
19# installed and those cgroups exist, so after a restart of
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.
2122[Unit]
2223Description=Host egress rule for CI builds
2324After=nftables.service ufw.service
@@ -28,7 +29,7 @@ Type=oneshot
2829RemainAfterExit=yes
2930ExecStart=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft
3031ExecReload=/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'
3233ExecStop=/usr/sbin/nft destroy table inet gitbay_runner
3334ExecStop=/usr/sbin/nft destroy table inet gitbay_builds
3435
deploy/runner-auth-flood-test.sh +33 −9
@@ -10,14 +10,15 @@
1010# deploy/runner-auth-flood-test.sh cmc/runner-scratch # trusted: a push to main
1111# deploy/runner-auth-flood-test.sh cmc/runner-scratch --untrusted # a merge request from a fork
1212#
13# The build's logins use a key registered with --ttl 1s and expired by
14# the time the build runs. With registration open an unknown key is
15# admitted to run register and never counts against the SSH auth
16# limiter; an expired key counts (internal/sshd/sshd.go, authenticate).
13# The build's logins use a git-scoped key registered with --ttl 1s and
14# expired by the time the build runs. With registration open an
15# unknown key is admitted to run register and never counts against the
16# SSH auth limiter; an expired key counts (internal/sshd/sshd.go, authenticate).
1717# The key is removed from the account when the script exits.
1818#
19# The step probes what the build reaches, then makes 12 logins without
20# pause, so the limiter (ssh_auth_rate, 10 a minute per address) locks
19# The step waits a minute, so the operator can find pasta's cgroup
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
2122# the address those logins come from for most of the next minute. The
2223# runner reports the result right after the step; its report retries
2324# for half a minute.
@@ -34,6 +35,14 @@
3435# names that address, the build succeeds and the runner keeps polling.
3536# Untrusted: every login is refused by the builds table before it
3637# 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.
3746set -eu
3847
3948repo=${1:-}
@@ -48,13 +57,15 @@ tmp=$(mktemp -d)
4857fp=
4958cleanup() {
5059 if [ -n "$fp" ]; then gitbay keys remove "$fp" >/dev/null || echo "remove key $fp by hand" >&2; fi
60 fp=
5161 rm -rf "$tmp"
5262}
5363trap cleanup EXIT
64trap 'cleanup; exit 130' INT TERM
5465
5566echo "==> an expired key"
5667ssh-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
5869fp=$(ssh-keygen -lf "$tmp/key.pub" | awk '{print $2}')
5970sleep 2
6071
@@ -87,6 +98,7 @@ cat >.gitbay/flood.sh <<'EOF'
8798#!/bin/sh
8899# Written by deploy/runner-auth-flood-test.sh (#260).
89100set -u
101sleep 60
90102key=/tmp/flood.key
91103cp .gitbay/flood.key "$key"
92104chmod 600 "$key"
@@ -104,7 +116,7 @@ probe() {
104116 esac
105117}
106118getent 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" \
108120 10.0.0.1:80 192.168.0.1:80 proxy.golang.org:443 github.com:22; do
109121 probe "${t%:*}" "${t##*:}"
110122done
@@ -159,7 +171,8 @@ second=$(seen)
159171echo " $account last seen $first, then $second"
160172
161173echo "==> 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'
163176
164177echo "==> auth audit, last 15 minutes"
165178gitbay audit --action auth. --since 15m --json |
@@ -173,5 +186,16 @@ if gitbay audit --action auth.throttled --since 15m --json | jq -e '.data[] | se
173186 echo "FAIL: 127.0.0.1, the runner's address, was throttled"
174187 fail=1
175188fi
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
176200[ $fail = 0 ] && echo "PASS ($mode): the build's failed logins did not lock the runner out"
177201exit $fail