Wiki: Architecture/09-Controls
Architecture/09-Controls
Controls matrix
- Architecture (V1)
- Authentication (V2) and session management (V3)
- Access control (V4)
- Input handling and output encoding (V5)
- Cryptography (V6) and data protection (V8)
- Logging (V7)
- Communications and integrations (V9, V10, V12)
- CI and build isolation
- Availability and operations
One row per control an auditor typically asks about. Status: in
place (implemented and cited), partial (implemented with a stated
limit), gap (not implemented; see 10). Categories follow the
chapter names of OWASP ASVS 4.0 where one fits.
Architecture (V1)
| Control | Status | Evidence |
|---|---|---|
| One authorization path for every surface | in place | all surfaces call control.Dispatch (internal/control/control.go); web form handlers, including the pin, watch and mark-read toggles, dispatch commands; login and session bookkeeping are not commands |
| No server-side signing key | in place | internal/sig verifies only |
| Least functionality by default | in place | API, web accounts, git://, push and registration default off (internal/config/config.go) |
| No git library; git runs as a subprocess with built argv | in place | internal/gitutil |
Authentication (V2) and session management (V3)
| Control | Status | Evidence |
|---|---|---|
| No passwords anywhere | in place | SSH keys, emailed single-use links, bearer tokens |
| Credentials stored as hashes | in place | SHA-256 of 256-bit random values (internal/store/sessions.go) |
| Brute-force limit on SSH auth | in place | 10 failures a minute per IP (internal/sshd/ratelimit.go) |
| Account enumeration resistance at login | in place | uniform response (internal/control/loginlink.go) |
| Session cookie flags | in place | HttpOnly, SameSite=Lax, Secure with TLS (internal/httpd/accounts.go) |
| Session lifetime | in place | 12 hours idle, 7 days absolute (internal/store/sessions.go) |
| Credential expiry | in place | optional --ttl on API tokens, SSH and deploy keys; checked at auth and per exec |
| Revocation takes effect immediately | in place | removing a key or disabling an account closes its connections; every exec re-reads its key (internal/sshd/sshd.go); LFS transfer tokens are refused with their key (internal/httpd/lfs.go) |
| Delegation bounded by the delegating credential | in place | expiring tokens refused on MintsCredential commands; credentials record their creating token; a browser session runs credential-minting and access-granting commands only within 15 minutes of signing in, and the refusal is audited (internal/control/control.go) |
Access control (V4)
| Control | Status | Evidence |
|---|---|---|
| Deny by default on private data | in place | CanRead requires owner, public or grant (internal/policy/access.go) |
| Private resources indistinguishable from missing | in place | resolveRepo (internal/control/repo.go), runGit, smart HTTP |
| Credential scopes narrow account rights | in place | key and token scopes (control.go, policy/access.go) |
| Server-side write protections | in place | pre-receive CheckPush, signed commits (internal/hookd/hookd.go) |
| Merge gates | in place | MergeGates; ci/* statuses written only by the build subsystem; required contexts |
| Admin functions isolated | in place | admin noun gated in Dispatch; audit admin-only |
| CSRF protection | in place | SameSite=Lax plus checkOrigin (accounts.go) |
| Typed confirmation for destructive web actions | in place | internal/httpd/confirm.go |
| Deleted rows' ids never handed out again | in place | AUTOINCREMENT on accounts, orgs, repositories, keys, tokens and the delivery queues; build ids from a high-water mark (internal/store/builds.go), so a grant, deploy key, parked about text, token or claim naming a deleted row names nothing; deletes take the grants, deploy keys and about texts that name the row by id |
Input handling and output encoding (V5)
| Control | Status | Evidence |
|---|---|---|
| User markup sanitised | in place | ugcHTML with bluemonday (internal/httpd/web.go) |
| No script execution in pages | in place | CSP script-src 'none' (internal/httpd/routes.go) |
| Control characters stripped at the terminal | in place | termSafe (internal/control/term.go) |
| No shell in command execution | in place | protocol.Tokenize for SSH argv; git and podman with argv slices |
| Parsers fuzzed | partial | five fuzz targets run briefly by deploy/audit.sh |
Cryptography (V6) and data protection (V8)
| Control | Status | Evidence |
|---|---|---|
| TLS for all authenticated HTTP | in place | ACME or certificate files; HSTS |
| Secrets encrypted at rest | in place | AES-256-GCM, key file outside the database and the main backups (internal/seal) |
| Secrets kept out of argv, logs and output | in place | ReadsStdin, pruned audit argv, write-only secret commands |
| Local backups encrypted | in place | age to [backup] age_recipients (cmd/gitbayd/backup.go); offsite copy by restic |
| Data retention configurable | in place | [retention] (internal/config/config.go) |
| User data export | in place | account export |
Logging (V7)
| Control | Status | Evidence |
|---|---|---|
| Security-relevant writes audited | in place | every successful mutating command (control.go) |
| Authentication failures audited | in place | auth.failed, auth.throttled |
| Denied attempts audited | in place | refused mutating commands and pushes, ten a minute per actor, 600 in all (internal/control/auditrefusal.go) |
| Audit log tamper resistance | partial | unkeyed hash chain checked by gitbayd admin audit verify; every row the daemon writes copied to its journal; the table is writable by the daemon user, who can recompute the chain after an edit, so comparing verify's last id and hash with the journal is the check for any change |
Communications and integrations (V9, V10, V12)
| Control | Status | Evidence |
|---|---|---|
| SSRF protection on user-supplied URLs | in place | webhooks at save and connect; mirrors at save and sync, repo import and repo import-issues before they fetch, git and the import API client pinned to the checked address (internal/gitpin) |
| Webhook payload integrity | in place | HMAC-SHA256 header |
| SMTP credentials protected in transit | in place | STARTTLS required for non-local relays, implicit TLS optional (internal/mail/mail.go) |
| Upload size limits | in place | per-owner storage quota at push (internal/sshd/sshd.go); API body 1 MiB |
CI and build isolation
| Control | Status | Evidence |
|---|---|---|
| Untrusted code runs isolated | in place | rootless podman, cgroup limits; untrusted builds get a disposable home (cmd/gitbay-runner/main.go) |
| No secrets for untrusted builds | in place | internal/control/build.go |
| Runner limited to attached repositories | in place | runnerMayBuild (build.go) |
| Build images fixed by the operator | in place | --pull=never |
| Build network egress restricted | in place | by build cgroup (gitbay-runner-builds.nft): host loopback (but DNS) and private ranges closed; trusted: host public 22/80/443, internet open by decision; untrusted: internet TCP 80/443 and DNS only. measured on bay1 2026-09-29 (CI page) |
| Build results reused only across equal trust | in place | SuccessBuildForTree, SuccessBuildFor (internal/store/builds.go) |
Availability and operations
| Control | Status | Evidence |
|---|---|---|
| Rate limits on API and writes | in place | 5. Rate limits |
| Concurrency limit on git pack generation | in place | global, per-principal, bounded queue across SSH, HTTP and git://, repo download included (internal/packlimit); not in system SSH mode |
| Concurrency limit on pushes | in place | receive-pack on its own global, per-principal, bounded-queue budget; a deploy key is its own principal; killed at push_receive_timeout before pre-receive, or after push_idle with nothing moving once the pack has begun (internal/sshd/sshd.go, internal/packlimit, internal/hookd); not in system SSH mode |
| Service hardening | in place | systemd sandboxing (3) |
| Backups offsite and append-only | in place | restic with append-only credentials (documented) |
| Restore tested | partial | drill 2026-09-29 from the offsite copy (Admin wiki "Restore drill"); secrets not checked in that drill; the off-host secret.key exists (#305) and is checked in the next |
| Migrations validated before commit | in place | PRAGMA foreign_key_check runs inside the migration transaction, before commit (internal/store/store.go) |
| Signed, reviewed changes to production | in place | signed commits, require-mr, ff-only merges, clean-tree deploys |