#+title: Controls matrix 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 [[file:10-Known-Gaps.org][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 | partial | all surfaces call =control.Dispatch= (=internal/control/control.go=); three web toggles write the store directly (#261) | | 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=) | | Delegation bounded by the delegating credential | partial | expiring tokens refused on =MintsCredential= commands; credentials record their creating token (=internal/control/control.go=); a web session can still mint credentials that outlive it (#297) | ** 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= | ** 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 | 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, and removing the newest rows (or reusing their ids) shows only by comparing verify's last id and hash with the journal | ** Communications and integrations (V9, V10, V12) | Control | Status | Evidence | |---------------------------------------------+----------+------------------------------------------------------------------| | SSRF protection on user-supplied URLs | partial | webhooks at save and connect; mirrors at save and sync, git pinned to the checked address (=internal/mirror/mirror.go=); =repo import --from= has no address check (#298) | | 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 | partial | host: loopback closed, public 22/80/443 only (=gitbay-runner-egress.nft=); internet outbound open by decision (#260) | | 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 | [[file:05-Identity-and-Access.org][5. Rate limits]] | | Concurrency limit on git pack generation | in place | global, per-principal, bounded queue across SSH, HTTP and git:// (=internal/packlimit=); not in system SSH mode | | Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) | | Backups offsite and append-only | in place | restic with append-only credentials (documented) | | Restore tested | gap | #259 | | Migrations validated before commit | gap | foreign-key check runs after commit (#261) | | Signed, reviewed changes to production | in place | signed commits, =require-mr=, ff-only merges, clean-tree deploys |