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 |
| 716 | 716 | assuming, enables lingering, and verifies rootless podman actually runs |
| 717 | 717 | as that user. It is idempotent. |
| 718 | 718 | |
| 719 | It also installs nftables. =make deploy-runner= ships |
| 720 | =deploy/gitbay-runner-egress.nft= to =/etc/gitbay-runner/egress.nft= |
| 721 | with =gitbay-runner-egress.service=, which loads it and which the |
| 722 | runner's unit requires; it checks the file with =nft -c=, reloads the |
| 723 | unit, and runs =deploy/runner-egress-check.sh= as =ci-runner= before |
| 724 | restarting the runner: =127.0.0.1:22= and the public 22 must answer, |
| 725 | 2222 must not. The table limits the runner's user to =127.0.0.1:22=, |
| 726 | DNS on loopback, and 22, 80 and 443 on the host's public address; the |
| 727 | Threat-Model page says why. A restart of =nftables.service= flushes it; |
| 728 | =systemctl reload gitbay-runner-egress= restores it. |
| 729 | |
| 719 | 730 | The drop-in sets =NoNewPrivileges=no=, without which rootless podman |
| 720 | 731 | cannot call =newuidmap= and the runner refuses to start. That is a |
| 721 | 732 | considered 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 @@ |
| 24 | 24 | | 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=) | |
| 25 | 25 | | 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=) | |
| 26 | 26 | | 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) | |
| 28 | 28 | | TB8 | Z1 → Z0 outbound | webhooks, mirrors, mail, push | address checks on user-supplied URLs; HMAC on webhooks; no redirects ([[file:03-Deployment.org][3]]) | |
| 29 | 29 | | TB9 | user content → browser | Markdown and Org bodies, READMEs, filenames | HTML sanitised (=ugcHTML=, =internal/httpd/web.go=, bluemonday); CSP =script-src 'none'= | |
| 30 | 30 | | 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: |
| 68 | 68 | | Build home | trusted: =<workdir>/trusted-home/<owner>/<name>=, one per repository, persistent; untrusted: =<workdir>/build-<id>-home=, removed with the build (=main.go=) | |
| 69 | 69 | | Secrets | env file 0600 outside the workspace, or =--env NAME= for multi-line values | |
| 70 | 70 | | 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) | |
| 72 | 72 | | Shutdown | SIGTERM stops claiming and drains in-flight builds; the unit uses =KillMode=mixed= | |
| 73 | 73 | |
| 74 | 74 | * Integrations |
.gitbay/wiki/Architecture/09-Controls.org
+1 −1
| @@ -88,7 +88,7 @@ chapter names of OWASP ASVS 4.0 where one fits. |
| 88 | 88 | | No secrets for untrusted builds | in place | =internal/control/build.go= | |
| 89 | 89 | | Runner limited to attached repositories | in place | =runnerMayBuild= (=build.go=) | |
| 90 | 90 | | 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) | |
| 92 | 92 | | Build results reused only across equal trust | in place | =SuccessBuildForTree=, =SuccessBuildFor= (=internal/store/builds.go=) | |
| 93 | 93 | |
| 94 | 94 | ** 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. |
| 64 | 64 | A restart ends every open follow with a message saying so, rather |
| 65 | 65 | than holding the drain; follow again once the daemon is back. |
| 66 | 66 | |
| 67 | * What a build can reach |
| 68 | |
| 69 | Builds have outbound internet access, trusted and untrusted alike. On |
| 70 | the runner's host they reach only the forge's public ports 22, 80 and |
| 71 | 443: not the host's loopback, not the operator's sshd. The forge is |
| 72 | reached at its public address, the one in =GITBAY_SSH=. See the |
| 73 | Threat-Model page, "What a build can reach", for how and why |
| 74 | (krz/gitbay#260). |
| 75 | |
| 67 | 76 | * The table |
| 68 | 77 | |
| 69 | 78 | One =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. |
| 193 | 193 | already has rather than naming anything on the internet. On an |
| 194 | 194 | instance with open registration that is the difference between a |
| 195 | 195 | 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. |
| 196 | 218 | |
| 197 | 219 | Under =-isolation none=, anything a step can do as the runner's user a |
| 198 | 220 | pushed =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; |
| 563 | 563 | a broken config surfaces as a failed =ci/config= status. Environment: |
| 564 | 564 | =GITBAY_REPO=, |
| 565 | 565 | =GITBAY_SHA=, =GITBAY_REF=, =GITBAY_JOB=, =CI=true=, and =GITBAY_SSH=, |
| 566 | | the instance's ssh destination as the build reaches it (=git@gitbay.org= |
| 567 | | from a runner elsewhere; inside a container on the server's own runner |
| 568 | | the host is at a private address the runner fills in). A job that |
| 566 | the instance's ssh destination as the build reaches it (=git@gitbay.org=, |
| 567 | the instance's public address, from a runner elsewhere and from a |
| 568 | container on the server's own runner alike; =git@host:port= on an |
| 569 | instance whose ssh is not on 22, so use it as =ssh://$GITBAY_SSH/owner/name.git= |
| 570 | or =ssh ssh://$GITBAY_SSH …=, which work in both forms). A job that |
| 569 | 571 | talks back to the instance — a release asset, a comment, a push to a |
| 570 | 572 | pages branch — uses =$GITBAY_SSH= with a key it holds as a secret; |
| 571 | 573 | the 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 |
| 133 | 133 | same image. =repo settings require-contexts= names status contexts |
| 134 | 134 | that must report green; setting any turns require-checks on, and one |
| 135 | 135 | 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) |
| 136 | 139 | |
| 137 | 140 | * v1.36.0 — 2026-09-23 |
| 138 | 141 | |