Commit 4be4bd5917
Verified · cmc
Layout: unified · split
Makefile +5
| @@ -77,9 +77,13 @@ deploy-runner: preflight | |||
| 77 | ./deploy/copy.sh $(HOST) $(PORT) deploy/gitbay-runner-prune.timer /etc/systemd/system/gitbay-runner-prune.timer | 77 | ./deploy/copy.sh $(HOST) $(PORT) deploy/gitbay-runner-prune.timer /etc/systemd/system/gitbay-runner-prune.timer |
| 78 | ./deploy/copy.sh $(HOST) $(PORT) deploy/gitbay-runner-egress.nft /etc/gitbay-runner/egress.nft | 78 | ./deploy/copy.sh $(HOST) $(PORT) deploy/gitbay-runner-egress.nft /etc/gitbay-runner/egress.nft |
| 79 | ./deploy/copy.sh $(HOST) $(PORT) deploy/gitbay-runner-egress.service /etc/systemd/system/gitbay-runner-egress.service | 79 | ./deploy/copy.sh $(HOST) $(PORT) deploy/gitbay-runner-egress.service /etc/systemd/system/gitbay-runner-egress.service |
| 80 | ./deploy/copy.sh $(HOST) $(PORT) deploy/gitbay-runner-builds.nft /etc/gitbay-runner/builds.nft | ||
| 80 | @echo "==> loading the egress rule" | 81 | @echo "==> loading the egress rule" |
| 82 | @# builds.nft names the runner's class cgroups, which may not exist yet; | ||
| 83 | @# its syntax is checked against system.slice, which always does. | ||
| 81 | ssh -p $(PORT) root@$(HOST) 'set -eu; \ | 84 | ssh -p $(PORT) root@$(HOST) 'set -eu; \ |
| 82 | nft -c -f /etc/gitbay-runner/egress.nft; \ | 85 | nft -c -f /etc/gitbay-runner/egress.nft; \ |
| 86 | sed "s|level 4 \"system.slice/gitbay-runner.service/builds/[a-z]*\"|level 1 \"system.slice\"|" /etc/gitbay-runner/builds.nft | nft -c -f /dev/stdin; \ | ||
| 83 | systemctl daemon-reload; \ | 87 | systemctl daemon-reload; \ |
| 84 | systemctl enable gitbay-runner-egress.service; \ | 88 | systemctl enable gitbay-runner-egress.service; \ |
| 85 | systemctl reload-or-restart gitbay-runner-egress.service' | 89 | systemctl reload-or-restart gitbay-runner-egress.service' |
| @@ -89,5 +93,6 @@ deploy-runner: preflight | |||
| 89 | mv /usr/local/bin/gitbay-runner.new /usr/local/bin/gitbay-runner; \ | 93 | mv /usr/local/bin/gitbay-runner.new /usr/local/bin/gitbay-runner; \ |
| 90 | systemctl enable --now gitbay-runner-prune.timer; \ | 94 | systemctl enable --now gitbay-runner-prune.timer; \ |
| 91 | systemctl restart gitbay-runner; \ | 95 | systemctl restart gitbay-runner; \ |
| 96 | nft list table inet gitbay_builds >/dev/null; \ | ||
| 92 | systemctl --no-pager --lines=3 status gitbay-runner; \ | 97 | systemctl --no-pager --lines=3 status gitbay-runner; \ |
| 93 | systemctl --no-pager list-timers gitbay-runner-prune.timer' | 98 | systemctl --no-pager list-timers gitbay-runner-prune.timer' |
deploy/gitbay-runner-builds.nft added +94
| @@ -0,0 +1,94 @@ | |||
| 1 | #!/usr/sbin/nft -f | ||
| 2 | # Egress for CI builds by trust (#260). Installed as | ||
| 3 | # /etc/gitbay-runner/builds.nft by `make deploy-runner` and loaded by the | ||
| 4 | # runner unit's ExecStartPre (gitbay-runner.override.conf), after the | ||
| 5 | # cgroups it names exist. | ||
| 6 | # | ||
| 7 | # gitbay-runner-egress.nft cannot tell a build from its runner: both run | ||
| 8 | # as ci-runner. This table can. The runner starts every podman process | ||
| 9 | # for a build inside builds/trusted/build-<id> or | ||
| 10 | # builds/untrusted/build-<id> under its service cgroup, and pasta, | ||
| 11 | # which podman starts, inherits that cgroup; so every socket pasta opens | ||
| 12 | # for a build carries it. The runner itself, its clone and its log | ||
| 13 | # stream run in <service>/runner and never match here. | ||
| 14 | # | ||
| 15 | # nftables resolves a cgroup path to the cgroup's id when the table | ||
| 16 | # loads. The runner's service cgroup is new on every start, so the drop-in | ||
| 17 | # creates builds/trusted and builds/untrusted and loads this file on | ||
| 18 | # every start, and the runner never recreates them. A table loaded | ||
| 19 | # against an earlier start's cgroups matches nothing, and builds then | ||
| 20 | # get only the uid table. | ||
| 21 | # | ||
| 22 | # The uid table still applies to builds; a packet must pass both. This | ||
| 23 | # table only takes away. Both hook output with policy accept, so their | ||
| 24 | # relative order does not matter. | ||
| 25 | # | ||
| 26 | # Trusted builds (a branch of the repository, with its secrets): | ||
| 27 | # internet open, any port. | ||
| 28 | # host, public 22, 80 and 443: the forge at GITBAY_SSH | ||
| 29 | # (169.254.1.2, which pasta translates to the public | ||
| 30 | # address). hutch and orgo publish over 22. | ||
| 31 | # host, loopback 53 only: the host's resolver, where pasta forwards | ||
| 32 | # a build's DNS when the host's nameserver is a | ||
| 33 | # loopback address. | ||
| 34 | # private ranges closed (RFC 1918, CGNAT, link-local, ULA). | ||
| 35 | # Untrusted builds (a fork's merge request head, no secrets): | ||
| 36 | # internet 80 and 443 over TCP, and 53: enough to fetch | ||
| 37 | # modules and packages, not to send mail or reach | ||
| 38 | # ssh elsewhere. | ||
| 39 | # host loopback 53 only. No forge: an untrusted build has | ||
| 40 | # no key to use there, and a failing login from it | ||
| 41 | # would count against the host's public address. | ||
| 42 | # private ranges closed. | ||
| 43 | # -isolation none builds run in the runner's own cgroup and get only the | ||
| 44 | # uid table; such a runner must not take -untrusted. | ||
| 45 | # | ||
| 46 | # A host whose /etc/resolv.conf names a nameserver in a private range | ||
| 47 | # needs that address let through here, or builds resolve nothing. | ||
| 48 | # | ||
| 49 | # The first line creates the table if it is missing, so the delete never | ||
| 50 | # fails; the file then replaces it in one transaction. | ||
| 51 | |||
| 52 | table inet gitbay_builds | ||
| 53 | delete table inet gitbay_builds | ||
| 54 | |||
| 55 | table inet gitbay_builds { | ||
| 56 | set private4 { | ||
| 57 | type ipv4_addr | ||
| 58 | flags interval | ||
| 59 | elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16 } | ||
| 60 | } | ||
| 61 | |||
| 62 | set private6 { | ||
| 63 | type ipv6_addr | ||
| 64 | flags interval | ||
| 65 | elements = { fc00::/7, fe80::/10 } | ||
| 66 | } | ||
| 67 | |||
| 68 | chain output { | ||
| 69 | type filter hook output priority filter; policy accept; | ||
| 70 | socket cgroupv2 level 4 "system.slice/gitbay-runner.service/builds/trusted" jump trusted | ||
| 71 | socket cgroupv2 level 4 "system.slice/gitbay-runner.service/builds/untrusted" jump untrusted | ||
| 72 | } | ||
| 73 | |||
| 74 | chain trusted { | ||
| 75 | oifname "lo" ip daddr 127.0.0.0/8 meta l4proto { tcp, udp } th dport 53 return | ||
| 76 | oifname "lo" ip6 daddr ::1 meta l4proto { tcp, udp } th dport 53 return | ||
| 77 | oifname "lo" ip daddr != 127.0.0.0/8 tcp dport { 22, 80, 443 } return | ||
| 78 | oifname "lo" ip6 daddr != ::1 tcp dport { 22, 80, 443 } return | ||
| 79 | oifname "lo" counter reject | ||
| 80 | ip daddr @private4 counter reject | ||
| 81 | ip6 daddr @private6 counter reject | ||
| 82 | } | ||
| 83 | |||
| 84 | chain untrusted { | ||
| 85 | oifname "lo" ip daddr 127.0.0.0/8 meta l4proto { tcp, udp } th dport 53 return | ||
| 86 | oifname "lo" ip6 daddr ::1 meta l4proto { tcp, udp } th dport 53 return | ||
| 87 | oifname "lo" counter reject | ||
| 88 | ip daddr @private4 counter reject | ||
| 89 | ip6 daddr @private6 counter reject | ||
| 90 | meta l4proto { tcp, udp } th dport 53 return | ||
| 91 | tcp dport { 80, 443 } return | ||
| 92 | counter reject | ||
| 93 | } | ||
| 94 | } | ||
deploy/gitbay-runner-egress.nft +2 −1
| @@ -9,7 +9,8 @@ | |||
| 9 | # nftables sees them exactly as it sees the runner's own ssh, so this | 9 | # nftables sees them exactly as it sees the runner's own ssh, so this |
| 10 | # table cannot tell a build from its runner. It limits what that user | 10 | # table cannot tell a build from its runner. It limits what that user |
| 11 | # reaches on this host, and the runner needs little: 127.0.0.1:22, to | 11 | # reaches on this host, and the runner needs little: 127.0.0.1:22, to |
| 12 | # poll, clone and stream logs. | 12 | # poll, clone and stream logs. gitbay-runner-builds.nft tells them apart |
| 13 | # by cgroup and narrows this per build, trusted or not. | ||
| 13 | # | 14 | # |
| 14 | # Every packet to one of the host's own addresses, loopback or public, | 15 | # Every packet to one of the host's own addresses, loopback or public, |
| 15 | # leaves through lo, so the output hook sees host-bound traffic as | 16 | # leaves through lo, so the output hook sees host-bound traffic as |
deploy/gitbay-runner-egress.service +7
| @@ -13,6 +13,11 @@ | |||
| 13 | # | 13 | # |
| 14 | # Stop uses destroy, which succeeds when the table is already gone (a | 14 | # Stop uses destroy, which succeeds when the table is already gone (a |
| 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 | # | ||
| 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 | ||
| 19 | # cgroups exist, so after a restart of nftables.service one reload puts | ||
| 20 | # both tables back. | ||
| 16 | [Unit] | 21 | [Unit] |
| 17 | Description=Host egress rule for CI builds | 22 | Description=Host egress rule for CI builds |
| 18 | After=nftables.service ufw.service | 23 | After=nftables.service ufw.service |
| @@ -23,7 +28,9 @@ Type=oneshot | |||
| 23 | RemainAfterExit=yes | 28 | RemainAfterExit=yes |
| 24 | ExecStart=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft | 29 | ExecStart=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft |
| 25 | ExecReload=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft | 30 | ExecReload=/usr/sbin/nft -f /etc/gitbay-runner/egress.nft |
| 31 | ExecReload=/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' | ||
| 26 | ExecStop=/usr/sbin/nft destroy table inet gitbay_runner | 32 | ExecStop=/usr/sbin/nft destroy table inet gitbay_runner |
| 33 | ExecStop=/usr/sbin/nft destroy table inet gitbay_builds | ||
| 27 | 34 | ||
| 28 | [Install] | 35 | [Install] |
| 29 | WantedBy=multi-user.target | 36 | WantedBy=multi-user.target |
deploy/gitbay-runner.override.conf +13 −1
| @@ -25,7 +25,8 @@ | |||
| 25 | [Unit] | 25 | [Unit] |
| 26 | # The host egress rule (#260, gitbay-runner-egress.nft) limits what this | 26 | # The host egress rule (#260, gitbay-runner-egress.nft) limits what this |
| 27 | # unit's user reaches on the host: 127.0.0.1:22 for the runner, the | 27 | # unit's user reaches on the host: 127.0.0.1:22 for the runner, the |
| 28 | # forge's public 22, 80 and 443 for builds, nothing else. Required, so | 28 | # forge's public 22, 80 and 443 for builds, nothing else. The builds |
| 29 | # table (ExecStartPre below) narrows that per build. Required, so | ||
| 29 | # the runner does not start without it: a table that failed to load must | 30 | # the runner does not start without it: a table that failed to load must |
| 30 | # not mean builds reach the admin sshd. | 31 | # not mean builds reach the admin sshd. |
| 31 | Requires=gitbay-runner-egress.service | 32 | Requires=gitbay-runner-egress.service |
| @@ -71,6 +72,17 @@ OOMPolicy=continue | |||
| 71 | # start joins its namespaces — including a /tmp that no longer exists. | 72 | # start joins its namespaces — including a /tmp that no longer exists. |
| 72 | # End it with the service. | 73 | # End it with the service. |
| 73 | ExecStopPost=-/usr/bin/pkill -u ci-runner -x catatonit | 74 | ExecStopPost=-/usr/bin/pkill -u ci-runner -x catatonit |
| 75 | # The builds table (#260, gitbay-runner-builds.nft) matches a build's | ||
| 76 | # traffic by the cgroup the runner starts it in, and nftables turns a | ||
| 77 | # cgroup path into the cgroup's id when the table loads. This unit's | ||
| 78 | # cgroup is new on every start, so the two class cgroups are created | ||
| 79 | # here, handed to the runner's user, and the table loaded against them, | ||
| 80 | # before the runner starts. As root (+), so the mkdir does not depend on | ||
| 81 | # when systemd hands the delegated cgroup to the unit's user. A table | ||
| 82 | # that fails to load stops the start, as the uid table's Requires= does. | ||
| 83 | ExecStartPre=+/usr/bin/mkdir -p /sys/fs/cgroup/system.slice/gitbay-runner.service/builds/trusted /sys/fs/cgroup/system.slice/gitbay-runner.service/builds/untrusted | ||
| 84 | ExecStartPre=+/usr/bin/chown -R ci-runner:ci-runner /sys/fs/cgroup/system.slice/gitbay-runner.service/builds | ||
| 85 | ExecStartPre=+/usr/sbin/nft -f /etc/gitbay-runner/builds.nft | ||
| 74 | # On stop the runner drains: it claims nothing more and finishes the | 86 | # On stop the runner drains: it claims nothing more and finishes the |
| 75 | # build in flight, then exits. Give it long enough — the per-build limit | 87 | # build in flight, then exits. Give it long enough — the per-build limit |
| 76 | # is 45m plus half a minute of report retries — before systemd kills it. | 88 | # is 45m plus half a minute of report retries — before systemd kills it. |