Commit 7b644421d3
Verified · cmc ci/build: success ci/test: success
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 | |||
| 681 | rather than at a fair share. =OOMPolicy=continue= keeps systemd from | 681 | rather than at a fair share. =OOMPolicy=continue= keeps systemd from |
| 682 | stopping the runner when a build is OOM-killed. | 682 | stopping the runner when a build is OOM-killed. |
| 683 | 683 | ||
| 684 | Each repository gets its own build home under the runner's workdir, | 684 | A trusted build's home is its repository's, under |
| 685 | mounted into its containers as =HOME=. Caches persist between builds of | 685 | =<workdir>/trusted-home/<owner>/<name>=, mounted into its containers as |
| 686 | one repository and are never read by another's. | 686 | =HOME=: caches persist between trusted builds of one repository and are |
| 687 | never read by another's. An untrusted build — a merge request head from | ||
| 688 | a fork — gets =<workdir>/build-<id>-home=, new and empty, removed when | ||
| 689 | the build ends. Homes under =<workdir>/home= are from runners before | ||
| 690 | krz/gitbay#255, which shared them with untrusted builds; nothing reads | ||
| 691 | them 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=). | ||
| 36 | 3. *Run.* The runner clones over SSH into =build-<id>=, starts a | 37 | 3. *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 | ||
| 195 | Under =-isolation none=, anything a step can do as the runner's user a | 197 | Under =-isolation none=, anything a step can do as the runner's user a |
| 196 | pushed =ci.yml= can do. Under podman a step is confined to its | 198 | pushed =ci.yml= can do. Under podman a step is confined to its |
| 197 | container, the bind-mounted workspace and the repository's own build | 199 | container, the bind-mounted workspace and its build home: a trusted |
| 198 | home, so what a build leaves in a cache is read only by later builds of | 200 | build's cache is read only by later trusted builds of the same |
| 199 | the same repository. Treat the runner host as executing untrusted code | 201 | repository, and an untrusted build's home is discarded with it. Treat |
| 200 | all the same: keep it off the daemon's host where the database lives, | 202 | the runner host as executing untrusted code all the same: keep it off |
| 201 | or scope it to repositories whose writers you trust. gitbay.org does | 203 | the daemon's host where the database lives, or scope it to repositories |
| 202 | the latter — its runner builds only the repositories the operator | 204 | whose writers you trust. gitbay.org does the latter — its runner builds |
| 203 | names. | 205 | only the repositories the operator names. |
| 204 | 206 | ||
| 205 | * What has not been audited | 207 | * What has not been audited |
| 206 | 208 | ||