.gitbay/wiki/Threat-Model.org

ca02f30bc09194b796e456c746a08421359b58d4
gitbay/.gitbay/wiki/Threat-Model.org rendered · source · history · blame · raw

151 lines · 7724 bytes

  1#+title: gitbay threat model
  2
  3What the forge trusts, what it refuses to do, and where the boundaries
  4are. This is the reference for security review; it complements the audit
  5log and hardening notes in [[Admin]].
  6
  7* What gitbay never does
  8
  9- *Execute repository content.* Git object contents are never run. Hooks
 10  are gitbay's own binary, invoked by git; they compute facts and ask the
 11  daemon over a unix socket. Repo files are only ever read.
 12- *Hold a signing key.* There is no server-side signing key. "Verified"
 13  means a signature made by a key the *user* registered — the server
 14  never vouches for a commit it did not receive already signed. Merge
 15  commits the server creates are honestly =unsigned=.
 16- *Serve repository HTML on its own origin as active content.* Raw file
 17  serving is =text/plain= with =nosniff=. Rendered markdown/org is
 18  sanitized (bluemonday) and served under a CSP that forbids scripts.
 19- *Put secrets in argv, URLs, or logs.* Import and mirror credentials,
 20  registration invites, and API tokens travel on stdin or in request
 21  bodies, never as command arguments (visible in =/proc=) or query
 22  strings. Tokens are stored only as SHA-256 hashes.
 23- *Confirm the existence of private repositories.* Every surface answers
 24  "not found" identically for a private repo and a nonexistent one — web
 25  pages, git transport, control commands, release asset downloads.
 26
 27* Trust boundaries
 28
 29- *SSH public key = identity.* The SSH username is ignored; the presented
 30  key's fingerprint resolves to an account. Key uniqueness is global.
 31- *Per-instance trust.* Email verification and key registration are local
 32  to an instance and never transfer. Account migration re-registers keys
 33  and re-verifies emails on the target by design.
 34- *The control plane is one authenticated channel* (SSH), fully usable
 35  from stock OpenSSH. The JSON API fronts the same command registry with
 36  bearer tokens minted only over SSH; git transport never runs over it.
 37- *Anonymous surfaces* — HTTPS clone of public repos, =git://= where
 38  enabled, the read-only web UI — carry no credentials and expose only
 39  public data. HTTP push is refused via a pkt-line =ERR=, never a 401.
 40
 41* Attacker-controlled parsers
 42
 43Every parser that eats bytes from a pusher, a key registrant, or an
 44anonymous client has a fuzz target and must never panic:
 45
 46- =internal/protocol= — the SSH command tokenizer (fuzzed against argv
 47  round-tripping).
 48- =internal/gitd= — the =git://= pkt-line reader.
 49- =internal/sig= — the commit parser, the SSHSIG armor decoder and blob
 50  parser, and the OpenPGP armored-key reader.
 51
 52Run =deploy/audit.sh= to exercise them plus =go vet= and =govulncheck=.
 53
 54* Secret handling
 55
 56Tokens (web sessions, login links, email verification, API bearer tokens,
 57deploy/CI) are random 256-bit values. Only their SHA-256 hash is stored,
 58and verification is a database index lookup on that hash — the secret
 59itself is never compared in Go, so there is no timing oracle to exploit.
 60Webhook payloads are signed outbound with HMAC-SHA256; the forge verifies
 61no inbound HMAC.
 62
 63* Network-facing request forgery
 64
 65Anything that makes the *server* open an outbound connection to a
 66user-supplied address — webhook delivery, GitHub-history import
 67=--api-base=, mirror remotes — passes the same SSRF guard: the scheme
 68must be http/https and, unless =webhooks.allow_local= is set, the
 69resolved address must not be loopback, private, or link-local. The
 70webhook dialer re-checks at connect time so a DNS answer that changes
 71after validation still cannot reach private space. Redirects are never
 72followed.
 73
 74* Rendering pushed markup
 75
 76Rendered markup is attacker-controlled: a README, a wiki page and a
 77profile's about text are all whatever someone pushed or typed. The risk
 78is not only what the output contains but what the *parser* is willing to
 79go and fetch — the filesystem counterpart of the SSRF guard above.
 80
 81Org is rendered by go-org, whose default configuration resolves
 82=#+INCLUDE:= and =#+SETUPFILE:= targets with =os.ReadFile=. Both are
 83refused outright (=orgConfig()= in =internal/httpd=): the file is never
 84opened and the keyword stays the inert text it already was, so the rest
 85of the document renders normally. There is no safe subset to allow
 86instead — an absolute path skips go-org's relative-path join, a relative
 87one resolves against the daemon's working directory, and the content
 88came from a git object rather than a checkout, so there is no directory
 89to scope a read to. Markdown is goldmark, which has no include
 90mechanism. go-org's parse warnings are discarded rather than logged, so
 91pushed content cannot write to the server's log.
 92
 93Org output is then sanitized (bluemonday UGC policy) because go-org
 94passes raw HTML through — export blocks and inline export snippets —
 95while goldmark drops it and needs no pass. The policy admits chroma's
 96short token classes and nothing else.
 97
 98* Web responses
 99
100Every response carries =Content-Security-Policy= (no scripts, no plugins,
101no embedding; inline styles allowed for chroma and label chips; images
102from any origin so external README images render), =X-Frame-Options:
103DENY=, =X-Content-Type-Options: nosniff=, =Referrer-Policy: no-referrer=,
104and =Strict-Transport-Security= when TLS is on. The UI needs no
105JavaScript, so =script-src 'none'= costs nothing.
106
107* The CI runner
108
109=gitbay-runner= is the one component that executes repository content.
110gitbayd never does: it reads =.gitbay/ci.yml= and queues a build, and a
111runner, polling over SSH, clones the commit and runs its steps.
112
113- *What the runner holds.* A key added with =keys add --scope runner=,
114  which the dispatcher confines to =runner next=, =runner log= and
115  =runner done= and to read-only git. A step that reads the key off the
116  disk gets exactly that: it cannot administer the instance, push, or
117  read a repository the runner's account cannot. An admin key still
118  works for the runner protocol so an operator can rotate at their own
119  pace; a runner host should not hold one.
120- *What a build sees.* The commit, the =GITBAY_*= variables and the
121  repository's secrets — unless the head came from another repository.
122  A merge request from a fork is built in the target as untrusted, with
123  no secrets, so a stranger's branch cannot read the target's deploy
124  credentials.
125- *Where it runs.* Steps run as the runner's own user on the runner
126  host, with no container; the systemd drop-in adds =NoNewPrivileges=,
127  =ProtectSystem=full= and the kernel and cgroup protections. =-repos=
128  limits a runner to named repositories, which is the control that
129  matters on an open instance: without it a runner builds whatever
130  anyone pushes.
131
132Anything a step can do as the runner's user, a pushed =ci.yml= can do.
133Treat the runner host as executing untrusted code: keep it off the
134daemon's host where the database lives, or scope it to repositories
135whose writers you trust.
136
137* Residual risks, accepted
138
139- External images in rendered READMEs and profile about text load from
140  their origin (no image proxy), which the author can use as a tracking
141  pixel against a viewer. A profile is the wider surface of the two: it
142  is linked from every commit and issue its owner touches. Documented;
143  proxying is future work.
144- Backups are consistent per the DB-snapshot-first ordering but are not a
145  single atomic snapshot; a few orphaned git objects are possible and
146  harmless (see [[Admin]]).
147- A global signature-verification epoch over-invalidates the cache on any
148  trust-input change. Correct, not a leak; a performance tradeoff.
149- Build steps run as the runner's user with no container. Isolation is
150  the key scope, the sandboxing drop-in and =-repos=; containers are
151  future work.