Commit 7e5de5325c
Verified · cmc ci/build: success ci/test: success
Layout: unified · split
.gitbay/wiki/Admin.org +42
| @@ -18,8 +18,14 @@ install -m 755 gitbayd /usr/local/bin/ | ||
| 18 | 18 | adduser --system --group --home /var/lib/gitbay --shell /usr/sbin/nologin gitbay |
| 19 | 19 | install -d -o gitbay -g gitbay -m 750 /var/lib/gitbay |
| 20 | 20 | gitbayd --config /etc/gitbay/config.toml check-config |
| 21 | gitbayd --config /etc/gitbay/config.toml admin secrets init | |
| 22 | chown gitbay:gitbay /etc/gitbay/secret.key | |
| 21 | 23 | #+end_src |
| 22 | 24 | |
| 25 | =admin secrets init= refuses when the key file already exists, so a | |
| 26 | reinstall on the same host should skip it — =deploy/install.sh= does | |
| 27 | this with a file check before running it. | |
| 28 | ||
| 23 | 29 | =deploy/= in the source tree has a cloud-init file, a hardened systemd |
| 24 | 30 | unit, and a nightly backup timer. Run as the unprivileged =gitbay= user; |
| 25 | 31 | the unit's =AmbientCapabilities=CAP_NET_BIND_SERVICE= covers ports |
| @@ -57,6 +63,10 @@ validation still prints, followed by the contradiction. | ||
| 57 | 63 | keys, ACME cache all live here. |
| 58 | 64 | - =site_url= (required) — canonical =https://host=; drives ACME, clone |
| 59 | 65 | URLs, mail links. |
| 66 | - =secret_key_file= (default =/etc/gitbay/secret.key=) — the keys that | |
| 67 | seal CI secrets, webhook secrets, mirror tokens and push device | |
| 68 | tokens in the database. Must be outside =root=, mode 0600, readable | |
| 69 | by the daemon's user. See "Secret key" below. | |
| 60 | 70 | - =source_repo= (optional, =owner/name=) — the repository this instance |
| 61 | 71 | develops itself in. Startup warns when the running build's commit is |
| 62 | 72 | not on that repository's default branch, which is how a binary built |
| @@ -503,6 +513,38 @@ snapshots sit beside them and the data stays referenced. Snapshot IDs | ||
| 503 | 513 | change; their times do not. Done for krz/keycask (formerly rust-pass) |
| 504 | 514 | on 2026-09-18, across 22 snapshots. |
| 505 | 515 | |
| 516 | ** Secret key | |
| 517 | ||
| 518 | CI secrets, webhook secrets, mirror tokens and APNs device tokens are | |
| 519 | stored sealed: AES-256-GCM under a key in =server.secret_key_file=, | |
| 520 | each value prefixed with the id of the key that sealed it | |
| 521 | (=gbs1:<id>:=). The key file is not in the database, not under | |
| 522 | =server.root=, and therefore in neither the local archives nor the | |
| 523 | main restic repository. It must be copied off the host separately; | |
| 524 | without it a restored database's secrets cannot be opened, and | |
| 525 | gitbayd refuses to start against them. | |
| 526 | ||
| 527 | #+begin_src sh | |
| 528 | gitbayd admin secrets init # once; deploy/install.sh does it on first install | |
| 529 | gitbayd admin secrets check # open every value, count by key | |
| 530 | gitbayd admin secrets rotate # new key, reseal, retire the old one (as root) | |
| 531 | #+end_src | |
| 532 | ||
| 533 | - Missing file: every gitbayd process that opens the database refuses | |
| 534 | to run and names the path, including =serve= and, in system mode, | |
| 535 | =authorized-keys=. =migrate= does not need it. | |
| 536 | - Wrong key: =serve= stops at startup naming the first row that does | |
| 537 | not open; =secrets check= does the same without starting anything. | |
| 538 | - Upgrade: the first start after the upgrade seals every value still | |
| 539 | in clear and logs =sealed secret values=. | |
| 540 | - Rotation: =rotate= adds a key, reseals every value under it in one | |
| 541 | transaction, then removes the old keys. Run it as root, since it | |
| 542 | replaces the key file in =/etc/gitbay=; the file keeps its owner. | |
| 543 | The daemon re-reads the file when it changes, so it needs no | |
| 544 | restart. Copy the new file off the host afterwards. | |
| 545 | - Push devices are looked up by the SHA-256 of their token | |
| 546 | (=push_devices.token_hash=), since two seals of one token differ. | |
| 547 | ||
| 506 | 548 | * Upgrades |
| 507 | 549 | |
| 508 | 550 | Replace the binary, restart the unit. Migrations apply automatically and |
.gitbay/wiki/Architecture/03-Deployment.org +1
| @@ -50,6 +50,7 @@ a database check. | ||
| 50 | 50 | | =<root>/acme= | ACME account key and certificates | autocert defaults | |
| 51 | 51 | | =<root>/hooks= | generated hook scripts | 0755 | |
| 52 | 52 | | =/etc/gitbay/config.toml= | configuration, including SMTP password | 0640 (cloud-init) | |
| 53 | | =/etc/gitbay/secret.key= | keys sealing secret columns | 0600, owner =gitbay= (=deploy/install.sh=) | | |
| 53 | 54 | | =/var/backups/gitbay= | backup archives | 0750 (cloud-init) | |
| 54 | 55 | |
| 55 | 56 | * Outbound connections from gitbayd |
.gitbay/wiki/Architecture/06-Data-and-Cryptography.org +10 −8
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | * Data inventory |
| 4 | 4 | |
| 5 | Schema: =internal/store/migrations/=, 59 migrations. Classification: | |
| 5 | Schema: =internal/store/migrations/=, 66 migrations. Classification: | |
| 6 | 6 | *C* credential or secret, *P* personal data, *R* private repository |
| 7 | 7 | content (as confidential as the repository), *O* operational. |
| 8 | 8 | |
| @@ -14,9 +14,9 @@ content (as confidential as the repository), *O* operational. | ||
| 14 | 14 | | Collaboration | =issues=, =issue_*=, =merge_requests=, =mr_*=, =labels=, =milestones=, =mentions= | R | bodies of issues, comments and reviews | |
| 15 | 15 | | Releases, snippets | =releases=, =release_assets=, =snippets=, =snippet_files= | R | | |
| 16 | 16 | | CI | =builds= (includes logs), =build_schedules=, =runner_repos=, =runner_seen= | R | build logs can echo anything a step prints | |
| 17 | | CI secrets | =build_secrets= | C | *plaintext* | | |
| 18 | | Integrations | =webhooks= (secret), =webhook_deliveries=, =mirrors= (username, token) | C | *plaintext* secrets and tokens | | |
| 19 | | Notifications | =notifications= (mail queue), =inbox=, =push_devices= (APNs token), =push_queue= | P | device tokens in clear | | |
| 17 | | CI secrets | =build_secrets= | C | sealed (AES-256-GCM) | | |
| 18 | | Integrations | =webhooks= (secret), =webhook_deliveries=, =mirrors= (username, token) | C | webhook secret and mirror token sealed | | |
| 19 | | Notifications | =notifications= (mail queue), =inbox=, =push_devices= (APNs token), =push_queue= | P | device tokens sealed; looked up by SHA-256 | | |
| 20 | 20 | | Signatures | =commit_signatures=, =settings.key_epoch= | O | verification cache | |
| 21 | 21 | | Audit and feed | =audit_log=, =events= | O, P | actor ids, pruned argv, fingerprints and IPs in some audit rows, a hash chain (=prev_hash=, =hash=) | |
| 22 | 22 | | Dependencies | =dep_checks=, =dep_reports= | O | | |
| @@ -31,6 +31,7 @@ Outside the database: | ||
| 31 | 31 | | TLS keys (ACME) | =<root>/acme= | C | |
| 32 | 32 | | SMTP password | =/etc/gitbay/config.toml= | C | |
| 33 | 33 | | APNs signing key (.p8) | path in =push.key_file= | C | |
| 34 | | Secret key file | =server.secret_key_file= (=/etc/gitbay/secret.key=) | C | | |
| 34 | 35 | | Backups | =/var/backups/gitbay=, offsite | all of the above | |
| 35 | 36 | |
| 36 | 37 | No table stores client IP addresses as a column. The daemon writes a |
| @@ -42,14 +43,15 @@ throttling (=internal/sshd/sshd.go=). | ||
| 42 | 43 | | Item | Protection | |
| 43 | 44 | |---------------------------------------+----------------------------------------------------------------| |
| 44 | 45 | | API tokens, sessions, login links, email codes, invites | SHA-256 of a 256-bit random value; the value is shown once and never stored (=internal/store/sessions.go=) | |
| 45 | | CI secrets, webhook secrets, mirror tokens, APNs device tokens | stored in clear in SQLite; protection is filesystem permissions and the rule that values are write-only through the interface | | |
| 46 | | CI secrets, webhook secrets, mirror tokens, APNs device tokens | AES-256-GCM under a key file outside the database and outside =server.root=; additional data binds table, column and row (=internal/seal=, =internal/store/secrets.go=) | | |
| 46 | 47 | | SQLite file | mode 0640, directory 0750 | |
| 47 | 48 | | Backups | the local archive is not encrypted; restic encrypts the offsite copy | |
| 48 | 49 | | Disk | no application-level encryption; any disk encryption is the host's | |
| 49 | 50 | |
| 50 | The code base contains no symmetric encryption. A database or backup | |
| 51 | file read by anyone other than the =gitbay= user discloses every CI | |
| 52 | secret, webhook secret and mirror token. | |
| 51 | The database file or a backup read by anyone other than the =gitbay= | |
| 52 | user discloses no CI secret, webhook secret, mirror token or device | |
| 53 | token without the key file, which neither carries. Rotation: | |
| 54 | =gitbayd admin secrets rotate= (Admin wiki). | |
| 53 | 55 | |
| 54 | 56 | * In transit |
| 55 | 57 | |
.gitbay/wiki/Architecture/09-Controls.org +1 −1
| @@ -56,7 +56,7 @@ chapter names of OWASP ASVS 4.0 where one fits. | ||
| 56 | 56 | | Control | Status | Evidence | |
| 57 | 57 | |---------------------------------------------+----------+------------------------------------------------------------------| |
| 58 | 58 | | TLS for all authenticated HTTP | in place | ACME or certificate files; HSTS | |
| 59 | | Secrets encrypted at rest | gap | CI secrets, webhook secrets, mirror tokens stored in clear (#273) | | |
| 59 | | Secrets encrypted at rest | in place | AES-256-GCM, key file outside the database and the main backups (=internal/seal=) | | |
| 60 | 60 | | Secrets kept out of argv, logs and output | in place | =ReadsStdin=, pruned audit argv, write-only secret commands | |
| 61 | 61 | | Local backups encrypted | gap | tar.gz in clear; offsite copy encrypted by restic (#274) | |
| 62 | 62 | | Data retention configurable | in place | =[retention]= (=internal/config/config.go=) | |
.gitbay/wiki/Architecture/10-Known-Gaps.org −1
| @@ -14,7 +14,6 @@ what the 2026-09-27 review found; remove a row when its issue closes. | ||
| 14 | 14 | | #260 | CI network | Builds share the runner's source address; no egress policy | medium | |
| 15 | 15 | | #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium | |
| 16 | 16 | | #262 | Availability | No limit on concurrent git pack generation | high | |
| 17 | | #273 | Data at rest | CI secrets, webhook secrets and mirror tokens are stored in clear in SQLite | high | | |
| 18 | 17 | | #274 | Backups | The local backup archive is not encrypted | medium | |
| 19 | 18 | | #298 | SSRF | =repo import --from= fetches without an address check | medium | |
| 20 | 19 | | #297 | Credentials | A browser session can mint tokens and keys that outlive it | low | |
CHANGELOG.org +13
| @@ -123,6 +123,19 @@ for the eighteen commands whose CLI path differs from the registry's | ||
| 123 | 123 | browser (#269). |
| 124 | 124 | - Untrusted builds (merge requests from forks) get a fresh HOME removed after the build and no secrets; trusted builds keep a per-repository home under =<workdir>/trusted-home=. Deploy gitbayd before the runner; the old shared homes under the runner's workdir can be deleted. (#255) |
| 125 | 125 | - =status set= refuses =ci/= contexts, which belong to the instance's builds. Build results are reused only from trusted builds on the same image. =repo settings require-contexts= names status contexts that must report green; setting any turns require-checks on, and one not yet reported counts as pending. (#258) |
| 126 | *Upgrade note.* gitbayd needs =server.secret_key_file= (default | |
| 127 | =/etc/gitbay/secret.key=) and refuses to start without it. Before | |
| 128 | replacing the binary, run =gitbayd admin secrets init= as root and | |
| 129 | =chown gitbay:gitbay /etc/gitbay/secret.key= (=deploy/install.sh= does | |
| 130 | both when the file is missing). The first start seals the stored | |
| 131 | secrets. Back the key file up separately: =admin backup= archives do | |
| 132 | not carry it (see the Admin wiki, "Secret key"). Downgrading to an | |
| 133 | earlier release after values are sealed is not supported: an older | |
| 134 | gitbayd reads a sealed value's =gbs1:...= prefix as the literal | |
| 135 | secret. | |
| 136 | - CI secrets, webhook secrets, mirror tokens and push device tokens are | |
| 137 | stored sealed with AES-256-GCM (#273). =gitbayd admin secrets | |
| 138 | init|rotate|check=. | |
| 126 | 139 | - Untrusted builds (merge requests from forks) get a fresh HOME |
| 127 | 140 | removed after the build and no secrets; trusted builds keep a |
| 128 | 141 | per-repository home under =<workdir>/trusted-home=. Deploy gitbayd |
deploy/install.sh +6
| @@ -14,6 +14,12 @@ ssh -p "$port" "root@$host" ' | ||
| 14 | 14 | chmod 755 /usr/local/bin/gitbayd.new |
| 15 | 15 | mv /usr/local/bin/gitbayd.new /usr/local/bin/gitbayd |
| 16 | 16 | /usr/local/bin/gitbayd --config /etc/gitbay/config.toml check-config --no-host-checks |
| 17 | # The key that seals secrets in the database. Created on the first | |
| 18 | # install, never replaced here; gitbayd refuses to start without it. | |
| 19 | if [ ! -e /etc/gitbay/secret.key ]; then | |
| 20 | /usr/local/bin/gitbayd --config /etc/gitbay/config.toml admin secrets init | |
| 21 | chown gitbay:gitbay /etc/gitbay/secret.key | |
| 22 | fi | |
| 17 | 23 | systemctl restart gitbayd |
| 18 | 24 | sleep 1 |
| 19 | 25 | systemctl --no-pager --lines=5 status gitbayd |
e2e/backup_test.go +1 −1
| @@ -16,7 +16,7 @@ import ( | ||
| 16 | 16 | // secretsCheckOneSealed matches "admin secrets check" reporting the one |
| 17 | 17 | // build secret set in TestAdminBackup as sealed under some key, e.g. |
| 18 | 18 | // "build_secrets.value: key 98e412e4 1". |
| 19 | var secretsCheckOneSealed = regexp.MustCompile(`build_secrets\.value: key \S+ 1`) | |
| 19 | var secretsCheckOneSealed = regexp.MustCompile(`(?m)build_secrets\.value: key \S+ 1$`) | |
| 20 | 20 | |
| 21 | 21 | func TestAdminBackup(t *testing.T) { |
| 22 | 22 | t.Parallel() |