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
681681rather than at a fair share. =OOMPolicy=continue= keeps systemd from
682682stopping the runner when a build is OOM-killed.
683683
684Each repository gets its own build home under the runner's workdir,
685mounted into its containers as =HOME=. Caches persist between builds of
686one repository and are never read by another's.
684A trusted build's home is its repository's, under
685=<workdir>/trusted-home/<owner>/<name>=, mounted into its containers as
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.
687692
688693*Images are provisioned, never pulled by a build.* The runner passes
689694=--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 @@
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; 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) |
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 +4 −3
@@ -31,8 +31,9 @@ commit instead of failing silently.
3131 runner key claims only for repositories it is attached to with
3232 =repo runner add=. Untrusted builds are claimable only by a runner
3333 started with =-untrusted= (=internal/store/builds.go=). The
34 claim returns id, repository, job, commit, ref, steps, image and —
35 for trusted builds only — the repository's secrets (=build.go=).
34 claim returns id, repository, job, commit, ref, steps, image, the
35 build's trust, and — for trusted builds only — the repository's secrets
36 (=build.go=).
36373. *Run.* The runner clones over SSH into =build-<id>=, starts a
3738 container and runs each step with =podman exec … sh -c <step>=
3839 (=cmd/gitbay-runner/isolate.go=).
@@ -64,7 +65,7 @@ Who may do what:
6465| Container runtime | rootless podman under the =ci-runner= user and its subordinate uid range |
6566| Image | =--pull=never=; images are built by the operator (=deploy/Containerfile.ci=) and referenced by tag |
6667| 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=) |
6869| Secrets | env file 0600 outside the workspace, or =--env NAME= for multi-line values |
6970| Resources | per-build cgroup with =memory.max= and =cpu.max= written by the runner; unit-level =MemoryMax=6G=, =CPUQuota=300%= |
7071| 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.
8484
8585| Control | Status | Evidence |
8686|---------------------------------------------+----------+------------------------------------------------------------------|
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=) |
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= |
.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.
1010
1111| Issue | Area | Gap | Severity |
1212|-------+------------------+-----------------------------------------------------------------------+----------|
13| #255 | CI isolation | Untrusted and trusted builds of a repository share a writable build home | high |
1413| #258 | CI integrity | Any writer can post a =ci/*= status; tree reuse ignores trust and image | high |
1514| #259 | Recovery | No restore has been exercised; verification does not check git connectivity | high |
1615| #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.
161161 secrets, and nothing the operator set on the service. =HOME= is a build
162162 home under the runner's =-workdir=, not the runner's own home, so a
163163 build cannot read the =.netrc=, =.npmrc= or =.gitconfig= where tools
164 keep credentials. That home is shared by every build on the runner —
165 one build can poison a cache another reads, which is no more than
166 anything a step can already do as this user, and is what isolation
167 (krz/gitbay#144) is for.
164 keep credentials. A trusted build's home belongs to its repository
165 and persists, so caches survive; an untrusted build's home is new,
166 empty and removed when the build ends, so nothing a fork's build
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.
168170- *Where it runs.* Steps run in a rootless podman container, one per
169171 job, with the workspace bind mounted and nothing else. The clone
170172 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.
194196
195197Under =-isolation none=, anything a step can do as the runner's user a
196198pushed =ci.yml= can do. Under podman a step is confined to its
197container, the bind-mounted workspace and the repository's own build
198home, so what a build leaves in a cache is read only by later builds of
199the same repository. Treat the runner host as executing untrusted code
200all the same: keep it off the daemon's host where the database lives,
201or scope it to repositories whose writers you trust. gitbay.org does
202the latter — its runner builds only the repositories the operator
203names.
199container, the bind-mounted workspace and its build home: a trusted
200build's cache is read only by later trusted builds of the same
201repository, and an untrusted build's home is discarded with it. Treat
202the runner host as executing untrusted code all the same: keep it off
203the daemon's host where the database lives, or scope it to repositories
204whose writers you trust. gitbay.org does the latter — its runner builds
205only the repositories the operator names.
204206
205207* What has not been audited
206208