Commit e977337c14

e977337c141d5bc5acbe0bccfa97c657a13feee3

parent: a4c429937c

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-03 21:42 UTC

Threat model: the CI runner; Admin: runner key scope, MR builds, trusted_proxies

Ref krz/gitbay#138

Layout: unified · split

Admin.org +29 −7
@@ -75,6 +75,12 @@ validation still prints, followed by the contradiction.
7575- =files=: =cert_file= + =key_file=.
7676- =off=: plain HTTP — development, or behind a TLS-terminating proxy.
7777
78=trusted_proxies= lists the addresses or CIDRs of reverse proxies in
79front of the daemon. A request from one of them is attributed, for API
80rate limiting, to the last =X-Forwarded-For= hop that is not itself a
81trusted proxy; from anyone else the header is ignored. Empty, the
82default, is right when gitbayd terminates TLS itself.
83
7884** [web]
7985- =mode= — =view_only= (default) | =accounts=. In view_only the mutating
8086 web routes are never registered; in accounts, browser sessions are
@@ -302,23 +308,39 @@ startup to point at the current binary path.
302308
303309* CI runner
304310
305=gitbay-runner= executes builds queued by pushes. It polls over SSH as
306an admin account (runner commands are admin-only: a runner executes
307arbitrary repo code), clones, runs the steps, streams the log back, and
308resolves the commit status. Run it as a dedicated unprivileged user:
311=gitbay-runner= executes builds queued by pushes and merge requests. It
312polls over SSH with a key added by =keys add --scope runner=, which
313reaches only the runner protocol and read-only git (a runner executes
314arbitrary repository code, so the key it holds must not do more), then
315clones, runs the steps, streams the log back and resolves the commit
316status. Run it as a dedicated unprivileged user on a non-admin account.
317=admin user create --key= registers a full-scope key, so the runner key
318is added afterwards through a bootstrap key that is then removed:
309319
310320#+begin_src sh
311321useradd --system --create-home --home-dir /var/lib/gitbay-runner ci-runner
312322sudo -u ci-runner ssh-keygen -t ed25519 -N "" -f /var/lib/gitbay-runner/.ssh/id_ed25519
313gitbayd --config /etc/gitbay/config.toml admin user create ci --admin \
314 --key /var/lib/gitbay-runner/.ssh/id_ed25519.pub
323ssh-keygen -t ed25519 -N "" -f /tmp/ci-bootstrap
324gitbayd --config /etc/gitbay/config.toml admin user create ci --key /tmp/ci-bootstrap.pub
325ssh -i /tmp/ci-bootstrap git@127.0.0.1 keys add --scope runner < /var/lib/gitbay-runner/.ssh/id_ed25519.pub
326ssh -i /tmp/ci-bootstrap git@127.0.0.1 keys remove "$(ssh-keygen -lf /tmp/ci-bootstrap.pub | awk '{print $2}')"
327rm /tmp/ci-bootstrap /tmp/ci-bootstrap.pub
315328gitbay-runner -remote git@127.0.0.1 -workdir /var/lib/gitbay-runner/work
316329#+end_src
317330
331=admin runners= shows which account each runner polls as; an admin key
332still works for the protocol during a rotation. A merge request head
333from a fork is built in the target repository as untrusted: the claim
334carries no secrets. Same-repository heads were built by their branch
335push and are not built again.
336
318337=make deploy-runner= also installs
319338=deploy/gitbay-runner.override.conf= as a systemd drop-in: =Nice=10=,
320339=CPUWeight=30=, =IOWeight=30=, so a build never starves the host's sshd,
321the daemon or the backup timers. The e2e suite alone starts sixty
340the daemon or the backup timers, and =NoNewPrivileges=,
341=ProtectSystem=full=, =ProtectKernelTunables=, =ProtectControlGroups=
342and =RestrictSUIDSGID=, so a step cannot reach outside its workspace
343and the runner's home. The e2e suite alone starts sixty
322344daemon instances; without the drop-in a deploy's copy over the admin
323345sshd stalled. Both deploy targets copy with =rsync --partial=, which
324346resumes a stalled transfer.
Threat-Model.org +33
@@ -104,6 +104,36 @@ DENY=, =X-Content-Type-Options: nosniff=, =Referrer-Policy: no-referrer=,
104104and =Strict-Transport-Security= when TLS is on. The UI needs no
105105JavaScript, so =script-src 'none'= costs nothing.
106106
107* The CI runner
108
109=gitbay-runner= is the one component that executes repository content.
110gitbayd never does: it reads =.gitbay/ci.yml= and queues a build, and a
111runner, polling over SSH, clones the commit and runs its steps.
112
113- *What the runner holds.* A key added with =keys add --scope runner=,
114 which the dispatcher confines to =runner next=, =runner log= and
115 =runner done= and to read-only git. A step that reads the key off the
116 disk gets exactly that: it cannot administer the instance, push, or
117 read a repository the runner's account cannot. An admin key still
118 works for the runner protocol so an operator can rotate at their own
119 pace; a runner host should not hold one.
120- *What a build sees.* The commit, the =GITBAY_*= variables and the
121 repository's secrets — unless the head came from another repository.
122 A merge request from a fork is built in the target as untrusted, with
123 no secrets, so a stranger's branch cannot read the target's deploy
124 credentials.
125- *Where it runs.* Steps run as the runner's own user on the runner
126 host, with no container; the systemd drop-in adds =NoNewPrivileges=,
127 =ProtectSystem=full= and the kernel and cgroup protections. =-repos=
128 limits a runner to named repositories, which is the control that
129 matters on an open instance: without it a runner builds whatever
130 anyone pushes.
131
132Anything a step can do as the runner's user, a pushed =ci.yml= can do.
133Treat the runner host as executing untrusted code: keep it off the
134daemon's host where the database lives, or scope it to repositories
135whose writers you trust.
136
107137* Residual risks, accepted
108138
109139- External images in rendered READMEs and profile about text load from
@@ -116,3 +146,6 @@ JavaScript, so =script-src 'none'= costs nothing.
116146 harmless (see [[Admin]]).
117147- A global signature-verification epoch over-invalidates the cache on any
118148 trust-input change. Correct, not a leak; a performance tradeoff.
149- Build steps run as the runner's user with no container. Isolation is
150 the key scope, the sandboxing drop-in and =-repos=; containers are
151 future work.