Commit 26e387e932

26e387e93251acedab909720ff021866a1416939

parent: 9467ed29d6

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:35 UTC

wiki: what a build can reach

Ref #260

Layout: unified · split

.gitbay/wiki/Admin.org +11
@@ -716,6 +716,17 @@ The script installs podman, delegates a subuid/subgid range to
716716assuming, enables lingering, and verifies rootless podman actually runs
717717as that user. It is idempotent.
718718
719It also installs nftables. =make deploy-runner= ships
720=deploy/gitbay-runner-egress.nft= to =/etc/gitbay-runner/egress.nft=
721with =gitbay-runner-egress.service=, which loads it and which the
722runner's unit requires; it checks the file with =nft -c=, reloads the
723unit, and runs =deploy/runner-egress-check.sh= as =ci-runner= before
724restarting the runner: =127.0.0.1:22= and the public 22 must answer,
7252222 must not. The table limits the runner's user to =127.0.0.1:22=,
726DNS on loopback, and 22, 80 and 443 on the host's public address; the
727Threat-Model page says why. A restart of =nftables.service= flushes it;
728=systemctl reload gitbay-runner-egress= restores it.
729
719730The drop-in sets =NoNewPrivileges=no=, without which rootless podman
720731cannot call =newuidmap= and the runner refuses to start. That is a
721732considered trade, explained in the file and in the Threat-Model; if you
.gitbay/wiki/Architecture/04-Trust-Boundaries.org +1 −1
@@ -24,7 +24,7 @@
2424| TB4 | Z1 → Z3 git | argv, repository path, stdin packs | argv built by code, never a shell; repository path from the database, not the request (=internal/gitutil=) |
2525| TB5 | Z3 → Z1 hook socket | ref updates, repository id, user id, key scope, push token, commit objects | the socket is mode 0600 and, on Linux, refuses a peer whose uid is not the daemon's; a request must carry the token sshd minted for its receive-pack (stored hashed in =push_tokens=) and name the same repository, account and scope. The daemon then decides with =policy.CheckPush= and =sig.VerifyCommit= (=internal/hookd/hookd.go=) |
2626| TB6 | Z4 ↔ Z1 runner channel | build claims (with secrets for trusted builds), logs, results | runner-scoped SSH key; claims limited to attached repositories; secrets only when the build is trusted (=internal/control/build.go=) |
27| TB7 | Z5 → Z4 container | build steps, workspace, build home | rootless podman, operator-provisioned image, cgroup limits; a trusted build's home is its repository's, an untrusted build's is discarded with it; the network is open (#260) |
27| TB7 | Z5 → Z4 container | build steps, workspace, build home | rootless podman, operator-provisioned image, cgroup limits; a trusted build's home is its repository's, an untrusted build's is discarded with it; outbound is open; on the host only the forge's public ports (#260) |
2828| TB8 | Z1 → Z0 outbound | webhooks, mirrors, mail, push | address checks on user-supplied URLs; HMAC on webhooks; no redirects ([[file:03-Deployment.org][3]]) |
2929| TB9 | user content → browser | Markdown and Org bodies, READMEs, filenames | HTML sanitised (=ugcHTML=, =internal/httpd/web.go=, bluemonday); CSP =script-src 'none'= |
3030| TB10| Z6 → everything | host shell | operator SSH on 2222, keys only, fail2ban; append-only offsite backup credentials |
.gitbay/wiki/Architecture/07-CI-and-Supply-Chain.org +1 −1
@@ -68,7 +68,7 @@ Who may do what:
6868| Build home | trusted: =<workdir>/trusted-home/<owner>/<name>=, one per repository, persistent; untrusted: =<workdir>/build-<id>-home=, removed with the build (=main.go=) |
6969| Secrets | env file 0600 outside the workspace, or =--env NAME= for multi-line values |
7070| Resources | per-build cgroup with =memory.max= and =cpu.max= written by the runner; unit-level =MemoryMax=6G=, =CPUQuota=300%= |
71| Network | podman default (pasta); outbound unrestricted (#260) |
71| Network | pasta; outbound open; a loopback runner's builds run with =--no-map-gw= (=main.go=); on the host only public 22/80/443 (=gitbay-runner-egress.nft=, #260) |
7272| Shutdown | SIGTERM stops claiming and drains in-flight builds; the unit uses =KillMode=mixed= |
7373
7474* Integrations
.gitbay/wiki/Architecture/09-Controls.org +1 −1
@@ -88,7 +88,7 @@ chapter names of OWASP ASVS 4.0 where one fits.
8888| No secrets for untrusted builds | in place | =internal/control/build.go= |
8989| Runner limited to attached repositories | in place | =runnerMayBuild= (=build.go=) |
9090| Build images fixed by the operator | in place | =--pull=never= |
91| Build network egress restricted | gap | #260 |
91| Build network egress restricted | partial | host: loopback closed, public 22/80/443 only (=gitbay-runner-egress.nft=); internet outbound open by decision (#260) |
9292| Build results reused only across equal trust | in place | =SuccessBuildForTree=, =SuccessBuildFor= (=internal/store/builds.go=) |
9393
9494** Availability and operations
.gitbay/wiki/CI.org +9
@@ -64,6 +64,15 @@ seconds: a follower who loses it is told the repository is not found.
6464A restart ends every open follow with a message saying so, rather
6565than holding the drain; follow again once the daemon is back.
6666
67* What a build can reach
68
69Builds have outbound internet access, trusted and untrusted alike. On
70the runner's host they reach only the forge's public ports 22, 80 and
71443: not the host's loopback, not the operator's sshd. The forge is
72reached at its public address, the one in =GITBAY_SSH=. See the
73Threat-Model page, "What a build can reach", for how and why
74(krz/gitbay#260).
75
6776* The table
6877
6978One =ci.yml= with five jobs, each isolating one rule:
.gitbay/wiki/Threat-Model.org +22
@@ -193,6 +193,28 @@ runner, polling over SSH, clones the commit and runs its steps.
193193 already has rather than naming anything on the internet. On an
194194 instance with open registration that is the difference between a
195195 curated set and arbitrary code from a registry nobody vetted.
196- *What a build can reach.* Outbound internet, trusted or not: a fork's
197 merge request to a Go repository has to fetch its modules. On the
198 runner's host, only the forge's public ports 22, 80 and 443, exactly
199 as anyone on the internet reaches them. Two layers keep it there. A
200 runner that polls the daemon over loopback starts its containers with
201 pasta's gateway mapping off, so a build does not reach the host's
202 loopback, and =GITBAY_SSH= names the public address; that keeps the
203 runner's source address, =127.0.0.1=, one no build connects from. And
204 an nftables table (=deploy/gitbay-runner-egress.nft=) rejects every
205 connection the runner's user makes to the host's own addresses except
206 =127.0.0.1:22=, DNS on loopback, and 22, 80 and 443 on the public
207 address: the operator's sshd on 2222 and anything bound to loopback
208 are closed to builds. Under rootless podman a build's connections are
209 made by pasta as the runner's user, so the table cannot tell a build
210 from its runner and leaves =127.0.0.1:22= open; the gateway mapping
211 is what closes it to builds. The runner does not start without the
212 table. The SSH auth limiter counts failures per source address and,
213 once an address is over the limit, refuses every key from it until
214 the window passes, the runner's included; with registration open or
215 by invite an unknown key never counts (krz/gitbay#260). Under
216 =-isolation none= a build runs on the host and shares its loopback;
217 the table still applies, since it runs as the same user.
196218
197219Under =-isolation none=, anything a step can do as the runner's user a
198220pushed =ci.yml= can do. Under podman a step is confined to its
.gitbay/wiki/Users.org +5 −3
@@ -563,9 +563,11 @@ with =sh -c= on the instance's runner, stopping at the first failure;
563563a broken config surfaces as a failed =ci/config= status. Environment:
564564=GITBAY_REPO=,
565565=GITBAY_SHA=, =GITBAY_REF=, =GITBAY_JOB=, =CI=true=, and =GITBAY_SSH=,
566the instance's ssh destination as the build reaches it (=git@gitbay.org=
567from a runner elsewhere; inside a container on the server's own runner
568the host is at a private address the runner fills in). A job that
566the instance's ssh destination as the build reaches it (=git@gitbay.org=,
567the instance's public address, from a runner elsewhere and from a
568container on the server's own runner alike; =git@host:port= on an
569instance whose ssh is not on 22, so use it as =ssh://$GITBAY_SSH/owner/name.git=
570or =ssh ssh://$GITBAY_SSH …=, which work in both forms). A job that
569571talks back to the instance — a release asset, a comment, a push to a
570572pages branch — uses =$GITBAY_SSH= with a key it holds as a secret;
571573the build's container has no key of its own. Two things about that
CHANGELOG.org +3
@@ -133,6 +133,9 @@ for the eighteen commands whose CLI path differs from the registry's
133133 same image. =repo settings require-contexts= names status contexts
134134 that must report green; setting any turns require-checks on, and one
135135 not yet reported counts as pending. (#258)
136- Builds lose host loopback and reach only the forge's public 22, 80
137 and 443 on the host. Deploy gitbayd, then the runner, after
138 validating on a scratch repository per the CI page. (#260)
136139
137140* v1.36.0 — 2026-09-23
138141