Commit 5c2215b845
Verified · cmc ci/build: success ci/test: success
Layout: unified · split
.gitbay/wiki/Architecture/03-Deployment.org +1 −1
| @@ -16,7 +16,7 @@ statements in this document about the host rest on those files. | |||
| 16 | | 80/tcp | HTTP | gitbayd | on with ACME| none; ACME HTTP-01 and redirect only | =cmd/gitbayd/main.go= | | 16 | | 80/tcp | HTTP | gitbayd | on with ACME| none; ACME HTTP-01 and redirect only | =cmd/gitbayd/main.go= | |
| 17 | | 9418/tcp | git:// | gitbayd | off | none; public repositories only | =internal/gitd= | | 17 | | 9418/tcp | git:// | gitbayd | off | none; public repositories only | =internal/gitd= | |
| 18 | | 2222/tcp | SSH (operator) | host sshd | on | public key, no passwords, fail2ban | =deploy/cloud-init.yaml= | | 18 | | 2222/tcp | SSH (operator) | host sshd | on | public key, no passwords, fail2ban | =deploy/cloud-init.yaml= | |
| 19 | | =<root>/hook.sock= | Unix socket | gitbayd | on | filesystem permissions only | =internal/hookd/hookd.go= | | 19 | | =<root>/hook.sock= | Unix socket | gitbayd | on | mode 0600; peer uid must be the daemon's (Linux); per-push token | =internal/hookd/hookd.go= | |
| 20 | 20 | ||
| 21 | With =ssh.mode = system= the host's sshd serves port 22 instead and | 21 | With =ssh.mode = system= the host's sshd serves port 22 instead and |
| 22 | invokes =gitbayd authorized-keys= and =gitbayd shell= | 22 | invokes =gitbayd authorized-keys= and =gitbayd shell= |
.gitbay/wiki/Architecture/04-Trust-Boundaries.org +2 −2
| @@ -22,7 +22,7 @@ | |||
| 22 | | TB2 | Z0 → Z1 HTTPS | page requests, form posts, API calls, fetches, LFS | TLS; session cookie or bearer token; =checkOrigin= on posts; CSP and security headers (=internal/httpd/routes.go=); smart HTTP is fetch-only (=smart.go=) | | 22 | | TB2 | Z0 → Z1 HTTPS | page requests, form posts, API calls, fetches, LFS | TLS; session cookie or bearer token; =checkOrigin= on posts; CSP and security headers (=internal/httpd/routes.go=); smart HTTP is fetch-only (=smart.go=) | |
| 23 | | TB3 | identity → data | every command | =Dispatch= gates, then =resolveRepo= with =policy= predicates; unreadable repositories are indistinguishable from missing ones ([[file:05-Identity-and-Access.org][5]]) | | 23 | | TB3 | identity → data | every command | =Dispatch= gates, then =resolveRepo= with =policy= predicates; unreadable repositories are indistinguishable from missing ones ([[file:05-Identity-and-Access.org][5]]) | |
| 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, commit objects | the daemon decides with =policy.CheckPush= and =sig.VerifyCommit= (=internal/hookd/hookd.go=). The socket trusts the ids in the request, so access to the socket is equivalent to acting as any user; it is reachable only through the =gitbay= user's filesystem | | 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; the build home is shared per repository and the network is open (#255, #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]]) | |
| @@ -55,7 +55,7 @@ above. | |||
| 55 | checks, archive and pull-mirror refusals and the owner's storage | 55 | checks, archive and pull-mirror refusals and the owner's storage |
| 56 | quota (=sshd.go=). TB3. | 56 | quota (=sshd.go=). TB3. |
| 57 | 2. =git receive-pack= runs with the hook socket path, repository id, | 57 | 2. =git receive-pack= runs with the hook socket path, repository id, |
| 58 | user id and key scope in its environment (=sshd.go=). TB4. | 58 | user id and key scope in its environment, and a push token (=sshd.go=). TB4. |
| 59 | 3. git runs =pre-receive=, which is =gitbayd hook pre-receive=. It | 59 | 3. git runs =pre-receive=, which is =gitbayd hook pre-receive=. It |
| 60 | reads the ref updates, computes ancestry in git's quarantine | 60 | reads the ref updates, computes ancestry in git's quarantine |
| 61 | environment and asks the daemon over the socket | 61 | environment and asks the daemon over the socket |
.gitbay/wiki/Architecture/10-Known-Gaps.org −1
| @@ -20,7 +20,6 @@ what the 2026-09-27 review found; remove a row when its issue closes. | |||
| 20 | | #274 | Backups | The local backup archive is not encrypted | medium | | 20 | | #274 | Backups | The local backup archive is not encrypted | medium | |
| 21 | | #275 | Audit | Refused writes are not audited; the audit table is writable by the daemon user | medium | | 21 | | #275 | Audit | Refused writes are not audited; the audit table is writable by the daemon user | medium | |
| 22 | | #298 | SSRF | =repo import --from= fetches without an address check | medium | | 22 | | #298 | SSRF | =repo import --from= fetches without an address check | medium | |
| 23 | | #282 | Hook socket | Anything that can open =hook.sock= can act as any user | medium | | ||
| 24 | | #297 | Credentials | A browser session can mint tokens and keys that outlive it | low | | 23 | | #297 | Credentials | A browser session can mint tokens and keys that outlive it | low | |
| 25 | 24 | ||
| 26 | * Questions an auditor will ask that have no answer yet | 25 | * Questions an auditor will ask that have no answer yet |
CHANGELOG.org +6
| @@ -48,6 +48,12 @@ must add =--scope full=. Existing tokens keep their scope. | |||
| 48 | syncs, and each records why (#279). | 48 | syncs, and each records why (#279). |
| 49 | - Webhook and mirror targets in 100.64.0.0/10 or on a multicast | 49 | - Webhook and mirror targets in 100.64.0.0/10 or on a multicast |
| 50 | address are refused, as private addresses are (#279). | 50 | address are refused, as private addresses are (#279). |
| 51 | - The hook socket is mode 0600 and, on Linux, refuses a peer with | ||
| 52 | another uid; each receive-pack gets its own token from sshd, stored | ||
| 53 | hashed (migration 0063), and the hook must present it before the | ||
| 54 | daemon acts. *Operators:* deploy with no push in flight, since a | ||
| 55 | receive-pack started by the old daemon has no token and its | ||
| 56 | post-receive will be refused by the new one (#282). | ||
| 51 | 57 | ||
| 52 | * v1.36.0 — 2026-09-23 | 58 | * v1.36.0 — 2026-09-23 |
| 53 | 59 | ||
cmd/gitbayd/hook.go +1
| @@ -172,6 +172,7 @@ func hookCmd() *cobra.Command { | |||
| 172 | RepoID: repoID, | 172 | RepoID: repoID, |
| 173 | UserID: userID, | 173 | UserID: userID, |
| 174 | Scope: os.Getenv(hookd.EnvScope), | 174 | Scope: os.Getenv(hookd.EnvScope), |
| 175 | Token: os.Getenv(hookd.EnvToken), | ||
| 175 | Updates: updates, | 176 | Updates: updates, |
| 176 | }, func(emit func(hookd.RawCommit) error) error { | 177 | }, func(emit func(hookd.RawCommit) error) error { |
| 177 | return streamIncomingCommits(updates, emit) | 178 | return streamIncomingCommits(updates, emit) |
internal/sshd/sshd.go +10
| @@ -575,6 +575,16 @@ func runGit(cfg config.Config, st *store.Store, user store.User, scope string, a | |||
| 575 | } | 575 | } |
| 576 | } | 576 | } |
| 577 | } | 577 | } |
| 578 | if write { | ||
| 579 | // hookd answers only a hook that names this receive-pack. | ||
| 580 | token, err := st.CreatePushToken(repo.ID, user.ID, scope) | ||
| 581 | if err != nil { | ||
| 582 | fmt.Fprintln(stderr, "internal error") | ||
| 583 | return protocol.ExitFailure | ||
| 584 | } | ||
| 585 | defer st.DeletePushToken(token) | ||
| 586 | env = append(env, hookd.EnvToken+"="+token) | ||
| 587 | } | ||
| 578 | if err := gitutil.Transport(service, dir, stdin, stdout, stderr, env, maxPack, revoked); err != nil { | 588 | if err := gitutil.Transport(service, dir, stdin, stdout, stderr, env, maxPack, revoked); err != nil { |
| 579 | return protocol.ExitFailure | 589 | return protocol.ExitFailure |
| 580 | } | 590 | } |