Commit 7b644421d3

7b644421d364ffa5f0374ce9ce9dee8e419a6a29

parent: 94f55ffbcd

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-28 06:36 UTC

wiki: trusted and untrusted build homes

Closes #255

Layout: unified · split

.gitbay/wiki/Admin.org +8 −3
@@ -681,9 +681,14 @@ allocates without bound, and it sits above the e2e suite's 5GB peak
681rather than at a fair share. =OOMPolicy=continue= keeps systemd from 681rather than at a fair share. =OOMPolicy=continue= keeps systemd from
682stopping the runner when a build is OOM-killed. 682stopping the runner when a build is OOM-killed.
683 683
684Each repository gets its own build home under the runner's workdir, 684A trusted build's home is its repository's, under
685mounted into its containers as =HOME=. Caches persist between builds of 685=<workdir>/trusted-home/<owner>/<name>=, mounted into its containers as
686one repository and are never read by another's. 686=HOME=: caches persist between trusted builds of one repository and are
687never read by another's. An untrusted build — a merge request head from
688a fork — gets =<workdir>/build-<id>-home=, new and empty, removed when
689the build ends. Homes under =<workdir>/home= are from runners before
690krz/gitbay#255, which shared them with untrusted builds; nothing reads
691them any more, and they can be deleted.
687 692
688*Images are provisioned, never pulled by a build.* The runner passes 693*Images are provisioned, never pulled by a build.* The runner passes
689=--pull=never=. Two reasons, and the second is the better one: the 694=--pull=never=. Two reasons, and the second is the better one: the
.gitbay/wiki/Architecture/04-Trust-Boundaries.org +1 −1
@@ -24,7 +24,7 @@
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=) | 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| 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=) | 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| 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=) | 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; the build home is shared per repository and the network is open (#255, #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; the network is open (#260) |
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]]) | 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| TB9 | user content → browser | Markdown and Org bodies, READMEs, filenames | HTML sanitised (=ugcHTML=, =internal/httpd/web.go=, bluemonday); CSP =script-src 'none'= | 29| TB9 | user content → browser | Markdown and Org bodies, READMEs, filenames | HTML sanitised (=ugcHTML=, =internal/httpd/web.go=, bluemonday); CSP =script-src 'none'= |
30| TB10| Z6 → everything | host shell | operator SSH on 2222, keys only, fail2ban; append-only offsite backup credentials | 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 +4 −3
@@ -31,8 +31,9 @@ commit instead of failing silently.
31 runner key claims only for repositories it is attached to with 31 runner key claims only for repositories it is attached to with
32 =repo runner add=. Untrusted builds are claimable only by a runner 32 =repo runner add=. Untrusted builds are claimable only by a runner
33 started with =-untrusted= (=internal/store/builds.go=). The 33 started with =-untrusted= (=internal/store/builds.go=). The
34 claim returns id, repository, job, commit, ref, steps, image and — 34 claim returns id, repository, job, commit, ref, steps, image, the
35 for trusted builds only — the repository's secrets (=build.go=). 35 build's trust, and — for trusted builds only — the repository's secrets
36 (=build.go=).
363. *Run.* The runner clones over SSH into =build-<id>=, starts a 373. *Run.* The runner clones over SSH into =build-<id>=, starts a
37 container and runs each step with =podman exec … sh -c <step>= 38 container and runs each step with =podman exec … sh -c <step>=
38 (=cmd/gitbay-runner/isolate.go=). 39 (=cmd/gitbay-runner/isolate.go=).
@@ -64,7 +65,7 @@ Who may do what:
64| Container runtime | rootless podman under the =ci-runner= user and its subordinate uid range | 65| Container runtime | rootless podman under the =ci-runner= user and its subordinate uid range |
65| Image | =--pull=never=; images are built by the operator (=deploy/Containerfile.ci=) and referenced by tag | 66| Image | =--pull=never=; images are built by the operator (=deploy/Containerfile.ci=) and referenced by tag |
66| Workspace | =<workdir>/build-<id>=, removed after the build; workdir must be 0700 and owned by the runner (=main.go=) | 67| Workspace | =<workdir>/build-<id>=, removed after the build; workdir must be 0700 and owned by the runner (=main.go=) |
67| Build home | =<workdir>/home/<owner>/<name>=, one per repository, mounted read-write, shared by trusted and untrusted builds of that repository (#255) | 68| Build home | trusted: =<workdir>/trusted-home/<owner>/<name>=, one per repository, persistent; untrusted: =<workdir>/build-<id>-home=, removed with the build (=main.go=) |
68| Secrets | env file 0600 outside the workspace, or =--env NAME= for multi-line values | 69| Secrets | env file 0600 outside the workspace, or =--env NAME= for multi-line values |
69| Resources | per-build cgroup with =memory.max= and =cpu.max= written by the runner; unit-level =MemoryMax=6G=, =CPUQuota=300%= | 70| Resources | per-build cgroup with =memory.max= and =cpu.max= written by the runner; unit-level =MemoryMax=6G=, =CPUQuota=300%= |
70| Network | podman default (pasta); outbound unrestricted (#260) | 71| Network | podman default (pasta); outbound unrestricted (#260) |
.gitbay/wiki/Architecture/09-Controls.org +1 −1
@@ -84,7 +84,7 @@ chapter names of OWASP ASVS 4.0 where one fits.
84 84
85| Control | Status | Evidence | 85| Control | Status | Evidence |
86|---------------------------------------------+----------+------------------------------------------------------------------| 86|---------------------------------------------+----------+------------------------------------------------------------------|
87| Untrusted code runs isolated | partial | rootless podman, cgroup limits; shared build home per repository (#255) | 87| Untrusted code runs isolated | in place | rootless podman, cgroup limits; untrusted builds get a disposable home (=cmd/gitbay-runner/main.go=) |
88| No secrets for untrusted builds | in place | =internal/control/build.go= | 88| No secrets for untrusted builds | in place | =internal/control/build.go= |
89| Runner limited to attached repositories | in place | =runnerMayBuild= (=build.go=) | 89| Runner limited to attached repositories | in place | =runnerMayBuild= (=build.go=) |
90| Build images fixed by the operator | in place | =--pull=never= | 90| Build images fixed by the operator | in place | =--pull=never= |
.gitbay/wiki/Architecture/10-Known-Gaps.org −1
@@ -10,7 +10,6 @@ what the 2026-09-27 review found; remove a row when its issue closes.
10 10
11| Issue | Area | Gap | Severity | 11| Issue | Area | Gap | Severity |
12|-------+------------------+-----------------------------------------------------------------------+----------| 12|-------+------------------+-----------------------------------------------------------------------+----------|
13| #255 | CI isolation | Untrusted and trusted builds of a repository share a writable build home | high |
14| #258 | CI integrity | Any writer can post a =ci/*= status; tree reuse ignores trust and image | high | 13| #258 | CI integrity | Any writer can post a =ci/*= status; tree reuse ignores trust and image | high |
15| #259 | Recovery | No restore has been exercised; verification does not check git connectivity | high | 14| #259 | Recovery | No restore has been exercised; verification does not check git connectivity | high |
16| #260 | CI network | Builds share the runner's source address; no egress policy | medium | 15| #260 | CI network | Builds share the runner's source address; no egress policy | medium |
.gitbay/wiki/Threat-Model.org +13 −11
@@ -161,10 +161,12 @@ runner, polling over SSH, clones the commit and runs its steps.
161 secrets, and nothing the operator set on the service. =HOME= is a build 161 secrets, and nothing the operator set on the service. =HOME= is a build
162 home under the runner's =-workdir=, not the runner's own home, so a 162 home under the runner's =-workdir=, not the runner's own home, so a
163 build cannot read the =.netrc=, =.npmrc= or =.gitconfig= where tools 163 build cannot read the =.netrc=, =.npmrc= or =.gitconfig= where tools
164 keep credentials. That home is shared by every build on the runner — 164 keep credentials. A trusted build's home belongs to its repository
165 one build can poison a cache another reads, which is no more than 165 and persists, so caches survive; an untrusted build's home is new,
166 anything a step can already do as this user, and is what isolation 166 empty and removed when the build ends, so nothing a fork's build
167 (krz/gitbay#144) is for. 167 writes is read by a later build (krz/gitbay#255). The claim names a
168 build's trust explicitly, and a runner that finds no trust flag treats
169 the build as untrusted.
168- *Where it runs.* Steps run in a rootless podman container, one per 170- *Where it runs.* Steps run in a rootless podman container, one per
169 job, with the workspace bind mounted and nothing else. The clone 171 job, with the workspace bind mounted and nothing else. The clone
170 happens outside it with the runner's key, so the container never sees 172 happens outside it with the runner's key, so the container never sees
@@ -194,13 +196,13 @@ runner, polling over SSH, clones the commit and runs its steps.
194 196
195Under =-isolation none=, anything a step can do as the runner's user a 197Under =-isolation none=, anything a step can do as the runner's user a
196pushed =ci.yml= can do. Under podman a step is confined to its 198pushed =ci.yml= can do. Under podman a step is confined to its
197container, the bind-mounted workspace and the repository's own build 199container, the bind-mounted workspace and its build home: a trusted
198home, so what a build leaves in a cache is read only by later builds of 200build's cache is read only by later trusted builds of the same
199the same repository. Treat the runner host as executing untrusted code 201repository, and an untrusted build's home is discarded with it. Treat
200all the same: keep it off the daemon's host where the database lives, 202the runner host as executing untrusted code all the same: keep it off
201or scope it to repositories whose writers you trust. gitbay.org does 203the daemon's host where the database lives, or scope it to repositories
202the latter — its runner builds only the repositories the operator 204whose writers you trust. gitbay.org does the latter — its runner builds
203names. 205only the repositories the operator names.
204 206
205* What has not been audited 207* What has not been audited
206 208