Wiki: Architecture/04-Trust-Boundaries

Architecture/04-Trust-Boundaries

Trust boundaries and data flows

diagrams/04-trust-boundaries.svg

Zones

Zone Contents Trust
Z0 The internet: visitors, clients, webhook and mirror endpoints none
Z1 gitbayd process holds all policy; trusted
Z2 Local state: SQLite, repositories, LFS, keys, config trusted; readable by the gitbay user
Z3 git subprocesses and hook processes run as gitbay on data from Z0; their decisions come from Z1
Z4 CI runner service trusted to report honestly; holds secrets for trusted builds
Z5 CI containers untrusted code from repositories and forks
Z6 Operator host access root; outside every in-application control

Boundaries

ID Boundary What crosses Control at the boundary
TB1 Z0 → Z1 SSH key auth, exec requests, git packs public-key auth, per-IP failure limit (internal/sshd/sshd.go, ratelimit.go); unknown keys reach only register
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)
TB3 identity → data every command Dispatch gates, then resolveRepo with policy predicates; unreadable repositories are indistinguishable from missing ones (5)
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)
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)
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)
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; private ranges and the host's loopback (but DNS) closed; trusted: internet open, host public 22/80/443; untrusted: internet TCP 80/443 and DNS, no host (#260)
TB8 Z1 → Z0 outbound webhooks, mirrors, mail, push address checks on user-supplied URLs; HMAC on webhooks; no redirects (3)
TB9 user content → browser Markdown and Org bodies, READMEs, filenames HTML sanitised (ugcHTML, internal/httpd/web.go, bluemonday); CSP script-src 'none'
TB10 Z6 → everything host shell operator SSH on 2222, keys only, fail2ban; append-only offsite backup credentials

Flows

Each flow lists its hops in order. Boundary IDs refer to the table above.

A. SSH control command

  1. Client opens SSH; authenticate looks up the key fingerprint and records user id, key id and scope in the connection (internal/sshd/sshd.go). TB1.
  2. Each exec request: runExec reloads the account, touches the key's last-used time, calls Exec (sshd.go).
  3. Exec tokenizes the command line (no shell) and routes git transport verbs to runGit, everything else to control.Dispatch with Source set to the key fingerprint (sshd.go).
  4. Dispatch gates and runs the handler; mutating successes are audited (internal/control/control.go). TB3.

B. git push over SSH

diagrams/06-push-flow.svg

  1. runGit resolves the repository, applies the deploy-key or account checks, archive and pull-mirror refusals and the owner's storage quota (sshd.go). TB3.
  2. git receive-pack runs with the hook socket path, repository id, user id and key scope in its environment, and a push token (sshd.go). TB4.
  3. git runs pre-receive, which is gitbayd hook pre-receive. It reads the ref updates, computes ancestry in git's quarantine environment and asks the daemon over the socket (cmd/gitbayd/hook.go). TB5.
  4. The daemon applies policy.CheckPush (protected branches, require-mr, protected tags, server-owned refs/merge-requests/*) and, when the repository requires signed commits, asks for every incoming commit object and verifies each (internal/hookd/hookd.go).
  5. On refusal the hook exits 1 and git rejects the push atomically.
  6. On success git runs post-receive; the daemon records events, marks mirrors dirty, queues CI, syncs merge request heads, processes Closes #N references and audits force pushes (hookd.go).

The server's own merge of a merge request does not pass through the hooks: runMRMerge updates the ref with a compare-and-swap (internal/control/mr.go) after MergeGates (mr.go). When signed commits are required only fast-forward merges are allowed, so the server never writes an unsigned commit (mr.go).

C. Fetch over smart HTTP

GET info/refs and POST git-upload-pack serve public repositories only; a private repository answers 404. git-receive-pack over HTTP always answers a pkt-line refusal, so there is no password prompt and no HTTP write path (internal/httpd/smart.go, routes.go). TB2.

D. Web read and write

  1. The session cookie is hashed and looked up (accounts.go).
  2. Pages dispatch read commands into the registry with Source=web and decode the JSON result into the template (internal/httpd/control.go).
  3. Form posts pass checkOrigin (accounts.go), dispatch the matching command, and map the exit code to a redirect or an error on the page. The pin, watch and mark-read toggles dispatch repo pin, repo watch=/=mute=/=unwatch and notifications read the same way.

E. JSON API

  1. apiAuth hashes the bearer token and looks it up; any failure is a uniform 401 (internal/httpd/api.go).
  2. Per-account rate limit, with writes at a tenth of the read budget.
  3. POST /api/v1/cmd dispatches any command except git transport; GET /api/v1/read refuses anything not marked ReadOnly, so a GET cannot write (apiread.go).
  4. Exit codes map to HTTP status: 0→200, 2→400, 3→404, 4→403, else 500.

F. LFS

git-lfs-authenticate over SSH applies the same repository checks as git transport and returns a one-hour HMAC token scoped to repository, operation and the SSH key that asked for it, deploy keys included (internal/sshd/lfs.go, internal/lfs/lfs.go). The HTTP batch, upload and download endpoints verify that token and that its key is still registered, unexpired and on an enabled account (store.LiveSSHKeys), and repeat the repository check for the key on each request; public repositories allow anonymous download. Objects are verified against their SHA-256 id on upload.

G. Browser login by emailed link

  1. /login takes a username or email; per-IP and per-account limits (5 per hour) apply, and every non-eligible case returns the same response (internal/control/loginlink.go).
  2. A 32-byte token is mailed; only its hash is stored, valid 15 minutes.
  3. /login?token= consumes it atomically, rechecks the account and sets the session cookie (accounts.go).

ssh git@host web login mints the same kind of link, valid 5 minutes, to an already-authenticated key (internal/control/web.go).

H. CI build