#!/usr/sbin/nft -f # Egress for CI builds by trust (#260). Installed as # /etc/gitbay-runner/builds.nft by `make deploy-runner` and loaded by the # runner unit's ExecStartPre (gitbay-runner.override.conf), after the # cgroups it names exist. # # gitbay-runner-egress.nft cannot tell a build from its runner: both run # as ci-runner. This table can. The runner starts every podman process # for a build inside builds/trusted/build- or # builds/untrusted/build- under its service cgroup, and pasta, # which podman starts, inherits that cgroup; so every socket pasta opens # for a build carries it. The runner itself, its clone and its log # stream run in /runner and never match here. # # nftables resolves a cgroup path to the cgroup's id when the table # loads. The runner's service cgroup is new on every start, so the drop-in # creates builds/trusted and builds/untrusted and loads this file on # every start, and the runner never recreates them. A table loaded # against an earlier start's cgroups matches nothing, and builds then # get only the uid table. # # The uid table still applies to builds; a packet must pass both. This # table only takes away. Both hook output with policy accept, so their # relative order does not matter. # # Trusted builds (a branch of the repository, with its secrets): # internet open, any port. # host, public 22, 80 and 443: the forge at GITBAY_SSH # (169.254.1.2, which pasta translates to the public # address). hutch and orgo publish over 22. # host, loopback 53 only: the host's resolver, where pasta forwards # a build's DNS when the host's nameserver is a # loopback address. # private ranges closed (RFC 1918, CGNAT, link-local, ULA). # Untrusted builds (a fork's merge request head, no secrets): # internet 80 and 443 over TCP, and 53: enough to fetch # modules and packages, not to send mail or reach # ssh elsewhere. # host loopback 53 only. No forge: an untrusted build has # no key to use there, and a failing login from it # would count against the host's public address. # private ranges closed. # -isolation none builds run in the runner's own cgroup and get only the # uid table; such a runner must not take -untrusted. # # A host whose /etc/resolv.conf names a nameserver in a private range # needs that address let through here, or builds resolve nothing. # # The first line creates the table if it is missing, so the delete never # fails; the file then replaces it in one transaction. table inet gitbay_builds delete table inet gitbay_builds table inet gitbay_builds { set private4 { type ipv4_addr flags interval 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 } } set private6 { type ipv6_addr flags interval elements = { fc00::/7, fe80::/10 } } chain output { type filter hook output priority filter; policy accept; socket cgroupv2 level 4 "system.slice/gitbay-runner.service/builds/trusted" jump trusted socket cgroupv2 level 4 "system.slice/gitbay-runner.service/builds/untrusted" jump untrusted } chain trusted { oifname "lo" ip daddr 127.0.0.0/8 meta l4proto { tcp, udp } th dport 53 return oifname "lo" ip6 daddr ::1 meta l4proto { tcp, udp } th dport 53 return oifname "lo" ip daddr != 127.0.0.0/8 tcp dport { 22, 80, 443 } return oifname "lo" ip6 daddr != ::1 tcp dport { 22, 80, 443 } return oifname "lo" counter reject ip daddr @private4 counter reject ip6 daddr @private6 counter reject } chain untrusted { oifname "lo" ip daddr 127.0.0.0/8 meta l4proto { tcp, udp } th dport 53 return oifname "lo" ip6 daddr ::1 meta l4proto { tcp, udp } th dport 53 return oifname "lo" counter reject ip daddr @private4 counter reject ip6 daddr @private6 counter reject meta l4proto { tcp, udp } th dport 53 return tcp dport { 80, 443 } return counter reject } }