deploy/gitbay-runner-egress.nft
67 lines · 3299 bytes
1#!/usr/sbin/nft -f
2# Host egress for CI builds (#260). Loaded by gitbay-runner-egress.service,
3# which gitbay-runner.service requires, so the runner does not start
4# without it. `make deploy-runner` installs it as
5# /etc/gitbay-runner/egress.nft.
6#
7# Under rootless podman with pasta, a build's connections are made by
8# pasta on the host, from sockets owned by the runner's user, ci-runner.
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
11# reaches on this host, and the runner needs little: 127.0.0.1:22, to
12# poll, clone and stream logs. gitbay-runner-builds.nft tells them apart
13# by cgroup and narrows this per build, trusted or not.
14#
15# Every packet to one of the host's own addresses, loopback or public,
16# leaves through lo, so the output hook sees host-bound traffic as
17# oifname "lo". Traffic to other hosts is not matched: builds keep
18# outbound internet access, trusted or not (go mod download needs it).
19#
20# What ci-runner may reach on this host:
21# 127.0.0.1:22 the forge over loopback, for the runner. This
22# table cannot close it to builds. A build reaches
23# the host at 169.254.1.2, pasta's --map-guest-addr,
24# which pasta translates to the host's public
25# address; --no-map-gw (podman's default, which the
26# runner also states) adds no mapping to loopback.
27# Runbook R3 checks from inside a build whether
28# 127.0.0.1:22 answers.
29# loopback :53 the host's resolver, for when the host's nameserver
30# is a loopback address. That pasta forwards a
31# build's DNS there is to be confirmed from inside a
32# build by runbook R3, not assumed.
33# public 22/80/443 the forge, as anyone on the internet reaches it,
34# and as a build reaches it through 169.254.1.2.
35# Everything else is rejected: the admin sshd on 2222 on every address,
36# and any service bound to loopback. -isolation none builds run as the
37# same user and get the same rule.
38#
39# The account name is resolved when the file is loaded. A restart of
40# nftables.service (flush ruleset) removes this table; `systemctl
41# reload gitbay-runner-egress` puts it back.
42#
43# The first line creates the table if it is missing, so the delete never
44# fails; the file then replaces it in one transaction, and a reload never
45# leaves a moment without the rule. The uid match sits in the base
46# chain's one rule rather than in a `!=` accept, because a packet with no
47# socket (a reset the kernel sends) matches neither `==` nor `!=` on
48# skuid and would otherwise fall through to the reject.
49
50table inet gitbay_runner
51delete table inet gitbay_runner
52
53table inet gitbay_runner {
54 chain output {
55 type filter hook output priority filter; policy accept;
56 oifname "lo" meta skuid "ci-runner" jump host
57 }
58
59 chain host {
60 ip daddr 127.0.0.1 tcp dport 22 accept
61 ip daddr 127.0.0.0/8 meta l4proto { tcp, udp } th dport 53 accept
62 ip6 daddr ::1 meta l4proto { tcp, udp } th dport 53 accept
63 ip daddr != 127.0.0.0/8 tcp dport { 22, 80, 443 } accept
64 ip6 daddr != ::1 tcp dport { 22, 80, 443 } accept
65 counter reject
66 }
67}