Commit 7a6343d02d

7a6343d02d1d885c2566bfe82515fa215f103b89

parent: 84c6f81258

Verified · cmc ci/build: success ci/test: skipped

cmc <hello@cleberg.net> · 2026-09-28 03:46 UTC

wiki: architecture and security pages

.gitbay/wiki/Architecture/: overview, system context, components,
deployment, trust boundaries and data flows, identity and access, data
and cryptography, CI and supply chain, operations, a controls matrix
and known gaps, with seven SVG diagrams generated by
diagrams/diagrams.py. Claims cite the implementing file and function.
Home and Threat-Model link to it.

Ref #255, #256, #257, #258, #259, #260, #261, #262, #272

Layout: unified · split

.gitbay/wiki/Architecture/00-Overview.org added +85
@@ -0,0 +1,85 @@
1#+title: Architecture and security
2
3Architecture, trust boundaries and security controls of gitbay, written
4for a security reviewer or auditor. Every statement about behaviour
5names the file, and usually the function, that implements it;
6statements that rest on documentation or deployment files say so. The
7pages track the default branch and change in the same merge request as
8the code they describe.
9
10* Scope
11
12- The =gitbayd= daemon, the =gitbay= CLI and the =gitbay-runner= CI
13 runner, all in this repository.
14- The reference deployment described by =deploy/= (a single Linux
15 host, systemd, rootless podman for CI).
16- The iOS client (krz/gitbay-ios) only where it touches the server: the
17 JSON API and push notifications.
18
19Out of scope: the host operating system beyond the unit files and
20bootstrap in =deploy/=, the object store holding offsite backups, and
21Apple's push service.
22
23* Figures as of the last review
24
25| Item | Value |
26|------------------+----------------------------------------------------|
27| Reviewed at | =2c08460= (2026-09-27) |
28| Schema version | migration 0059 |
29| Control commands | 232 registered, 79 marked =ReadOnly= |
30| Go | 1.27, =CGO_ENABLED=0= |
31| Direct Go deps | 15 (=go.mod=) |
32
33The command count comes from =gitbay help --json= on the live instance;
34the =ReadOnly= count from the =ReadOnly: true= literals in
35=internal/control/=.
36
37* Documents
38
39| # | Document | Diagram |
40|---+----------------------------------------------------+-------------------------------------------------|
41| 1 | [[file:01-System-Context.org][System context]] | [[file:diagrams/01-context.svg][01-context.svg]] |
42| 2 | [[file:02-Components.org][Components]] | [[file:diagrams/02-components.svg][02-components.svg]] |
43| 3 | [[file:03-Deployment.org][Deployment and network]] | [[file:diagrams/03-deployment.svg][03-deployment.svg]] |
44| 4 | [[file:04-Trust-Boundaries.org][Trust boundaries and data flows]] | [[file:diagrams/04-trust-boundaries.svg][04-trust-boundaries.svg]], [[file:diagrams/06-push-flow.svg][06-push-flow.svg]] |
45| 5 | [[file:05-Identity-and-Access.org][Identity and access]] | [[file:diagrams/05-authorization.svg][05-authorization.svg]] |
46| 6 | [[file:06-Data-and-Cryptography.org][Data and cryptography]] | |
47| 7 | [[file:07-CI-and-Supply-Chain.org][CI and supply chain]] | [[file:diagrams/07-ci-flow.svg][07-ci-flow.svg]] |
48| 8 | [[file:08-Operations.org][Operations]] | |
49| 9 | [[file:09-Controls.org][Controls matrix]] | |
50| 10 | [[file:10-Known-Gaps.org][Known gaps]] | |
51
52Reading order for a first pass: 1, 4, 5, 9, 10. The others are
53reference.
54
55* Conventions
56
57- Paths are relative to the repository root. For a pinned snapshot,
58 read these pages at a tag: the wiki is part of the repository.
59- "Documented" means the statement rests on the wiki
60 (=.gitbay/wiki/=) or =deploy/= rather than on code.
61- Numbers such as =#255= are issues on krz/gitbay; =krz/gitbay-ios#15=
62 names the other repository.
63- Diagrams are SVG with a light and a dark rendering chosen by the
64 viewer's colour scheme. They are generated by
65 =.gitbay/wiki/Architecture/diagrams/diagrams.py=; edit that and rerun
66 it rather than the SVGs.
67
68* Checking a claim
69
70The instance answers the same questions the documents make claims
71about:
72
73#+begin_src sh
74gitbay help --json # the command registry: paths, flags, ReadOnly
75curl -s https://gitbay.org/healthz # the deployed commit
76curl -sI https://gitbay.org/ # security headers
77ssh git@gitbay.org whoami # identity resolution over stock OpenSSH
78#+end_src
79
80* Related wiki pages
81
82- [[file:../Threat-Model.org][Threat-Model]] — the project's own threat model; this package extends it.
83- [[file:../Parity.org][Parity]] — which capability is reachable from which surface.
84- [[file:../Admin.org][Admin]] — configuration, backups, operations.
85- [[file:../API.org][API]] — the JSON API.
.gitbay/wiki/Architecture/01-System-Context.org added +60
@@ -0,0 +1,60 @@
1#+title: 1. System context
2
3[[file:diagrams/01-context.svg]]
4
5* What gitbay is
6
7A self-hosted git forge: repositories, issues, merge requests, reviews,
8CI, releases, wikis, snippets and notifications. One Go binary
9(=gitbayd=), one SQLite database, and the system =git= binary for all
10repository operations.
11
12The design rule that shapes everything else: *SSH is the API*. Every
13operation is a control command in one registry
14(=internal/control/control.go=). Stock OpenSSH reaches all of them; the
15CLI, the web UI and the JSON API are clients of the same registry and
16do not reimplement logic (=internal/httpd/control.go=,
17=internal/httpd/api.go=).
18
19* Actors
20
21| Actor | Reaches gitbay through | Authenticates with |
22|-----------------------+-----------------------------------------------+-------------------------------------|
23| Anonymous visitor | HTTPS pages, smart HTTP fetch, git:// if on | nothing |
24| Registered user | SSH (CLI or stock OpenSSH), HTTPS web, API | SSH key; web session; API token |
25| Instance administrator| same as a user, plus host shell | SSH key with admin account; root |
26| Deploy key holder | SSH git transport for one repository | SSH key bound to that repository |
27| CI runner | SSH, =runner= commands and clone | SSH key with =runner= scope |
28| iOS app | JSON API over HTTPS; receives APNs pushes | API token pasted at sign-in |
29| Webhook receiver | receives HTTPS POSTs from gitbay | verifies HMAC-SHA256 signature |
30
31* External systems
32
33| System | Direction | Purpose | Code |
34|---------------------------+-----------+-------------------------------------------+------------------------------------|
35| ACME CA (Let's Encrypt) | out | TLS certificates | =cmd/gitbayd/main.go= |
36| SMTP relay | out | verification, login links, notifications | =internal/mail/mail.go= |
37| Apple Push Notification | out | iOS notifications | =internal/push/apns.go= |
38| Webhook endpoints | out | event delivery, user-configured | =internal/webhook/webhook.go= |
39| Mirror remotes | out / in | push and pull mirrors, user-configured | =internal/mirror/mirror.go= |
40| Package registries | out | dependency update checks (opt-in per repo)| =internal/deps/registry.go= |
41| Offsite object storage | out | restic backups (host timer, not gitbayd) | documented: Admin wiki |
42
43gitbayd makes no other outbound connection: no telemetry or update
44check.
45
46* Instance modes that change the attack surface
47
48| Setting | Default | Effect |
49|----------------------------------+-----------+-------------------------------------------------------------|
50| =web.mode= | view_only | =accounts= adds login, settings and every web write route (=routes.go=) |
51| =api.enabled= | false | when false there is no credential-bearing HTTP surface |
52| =registration.mode= | closed | =open= admits unknown SSH keys to =register=; =invite= needs a code |
53| =git_daemon.enabled= | false | anonymous =git://= on 9418 |
54| =push.enabled= | false | APNs worker and device registration |
55| =http.tls= | acme | =files= or =off=; =off= also drops HSTS and the cookie Secure flag |
56| =webhooks.allow_local= | false | when false, webhook and mirror URLs may not resolve to private or loopback addresses |
57
58gitbay.org runs with =web.mode = accounts=, the API enabled and
59=registration.mode = open=, all three observable from outside (=/login=,
60=/register=, =/api/v1/read= answering 401).
.gitbay/wiki/Architecture/02-Components.org added +92
@@ -0,0 +1,92 @@
1#+title: 2. Components
2
3[[file:diagrams/02-components.svg]]
4
5* Binaries
6
7| Binary | Role | Entry |
8|-----------------+----------------------------------------------------------------------+-------------------------------|
9| =gitbayd= | daemon: listeners, workers, git hooks, admin and maintenance | =cmd/gitbayd/main.go= |
10| =gitbay= | end-user CLI; a thin client that runs control commands over SSH | =cmd/gitbay/main.go=, =ssh.go= |
11| =gitbay-runner= | CI runner; claims builds over SSH and runs them, normally in podman | =cmd/gitbay-runner/main.go= |
12
13=gitbayd= subcommands: =serve=, =check-config=, =migrate=, =admin=,
14=authorized-keys= and =shell= (for =ssh.mode = system=), =version=, and
15the hidden =hook= used by git (=cmd/gitbayd/main.go=,
16=cmd/gitbayd/hook.go=).
17
18* Packages
19
20| Package | Responsibility |
21|----------------------+--------------------------------------------------------------------------------|
22| =internal/control= | The command registry and every handler. The only place business rules live. |
23| =internal/policy= | Access predicates (=CanRead/CanWrite/CanAdmin=), key scopes, push rules, CODEOWNERS, reserved names. |
24| =internal/store= | SQLite access, hand-written SQL, migrations (=internal/store/migrations/=). |
25| =internal/sshd= | SSH listener, public-key auth, session exec, dispatch to git transport or registry, LFS bridge. |
26| =internal/httpd= | HTTPS: web UI, smart HTTP (fetch only), LFS HTTP, JSON API, login, security headers. |
27| =internal/hookd= | Unix-socket server answering git's pre-receive and post-receive hooks. |
28| =internal/gitutil= | Subprocess wrappers around =git=. No git library is linked. |
29| =internal/sig= | Verification of OpenPGP and SSHSIG commit and tag signatures. Verification only. |
30| =internal/gitd= | Anonymous =git://= daemon, upload-pack only, off by default. |
31| =internal/ci= | =.gitbay/ci.yml= parsing, cron schedules, the scheduler and stale-build reaper. |
32| =internal/lfs= | Content-addressed LFS store and HMAC transfer tokens. |
33| =internal/webhook= | Outbound webhook delivery with SSRF checks, HMAC signing, retries. |
34| =internal/mirror= | Push and pull mirror worker. |
35| =internal/notify=, =internal/mail= | Mail queue drain and SMTP. |
36| =internal/push= | APNs queue drain and provider-token signing. |
37| =internal/deps= | Dependency manifest parsing and registry checks (opt-in per repository). |
38| =internal/config= | Configuration load and validation. |
39| =internal/web= | Embedded templates, stylesheet and fonts. |
40| =internal/protocol= | Exit codes, JSON envelope, argv tokenizer. |
41
42* The command registry
43
44Every capability is a =Command= (=internal/control/control.go=):
45
46| Field | Meaning |
47|--------------+---------------------------------------------------------------------|
48| =Path= | noun and verb, e.g. =keys add= |
49| =Flags= | parsed by one parser for every command (=internal/control/flags.go=)|
50| =ReadsStdin= | the only way a handler receives stdin; otherwise stdin is emptied |
51| =ReadOnly= | safe for read-scoped tokens and =GET /api/v1/read=; tested to write nothing |
52| =Run= | the handler |
53
54Every surface builds a =Ctx= and calls =Dispatch=
55(=internal/control/control.go=):
56
57| Surface | =Ctx.Source= | =Ctx.Scope= | =Ctx.ReadOnly= | Code |
58|--------------+-------------------+------------------------+---------------------+-----------------------------------|
59| SSH | key fingerprint | the key's scope | false | =internal/sshd/sshd.go= (=Exec=) |
60| Web | =web= | =full= | false | =internal/httpd/control.go= |
61| JSON API | =api= | =full= | token scope = read | =internal/httpd/api.go=, =apiread.go= |
62| Host (root) | =host= | =full= | false | =cmd/gitbayd= admin subcommands |
63
64=Dispatch= applies, in order: =--term= and =--json= stripping; the scope
65gate; the read-only gate; the disabled-account gate; the =admin= noun
66gate; the pending-account gate; the per-account write budget; stdin
67gating; the handler; and an audit row for every successful mutating
68command. Details in [[file:05-Identity-and-Access.org][5. Identity and access]].
69
70* Background workers
71
72Started by =gitbayd serve= (=cmd/gitbayd/main.go=):
73
74| Worker | Starts when | Trigger | Queue / table |
75|-----------------------+-----------------------------+---------------------------------+-----------------------|
76| Webhook delivery | always | 2 s poll | =webhook_deliveries= |
77| Mail | =mail.smtp_host= set | 2 s poll | =notifications= |
78| APNs push | =push.enabled= | 2 s poll | =push_queue= |
79| Mirrors | always | 10 s tick, per-mirror interval | =mirrors= |
80| CI scheduler | always | 1 min tick; reaps stale builds | =build_schedules=, =builds= |
81| Dependency checks | always (repos opt in) | =deps.check_interval_hours= | =dep_checks= |
82| Retention sweep | always | hourly | sessions, tokens, retained tables |
83| Pending-account reaper| =registration.pending_expiry= set | hourly | =users= |
84
85* Git hooks
86
87Repositories carry generated hook scripts (mode 0755, regenerated at
88startup, =internal/hookd/hookd.go=) that run
89=gitbayd hook pre-receive|post-receive=. The hook process connects to
90the daemon's Unix socket (=<root>/hook.sock=, =hookd.go=) and asks
91for a decision; the daemon holds the policy. See
92[[file:04-Trust-Boundaries.org][4. Trust boundaries]], flow B.
.gitbay/wiki/Architecture/03-Deployment.org added +80
@@ -0,0 +1,80 @@
1#+title: 3. Deployment and network
2
3[[file:diagrams/03-deployment.svg]]
4
5The reference deployment is one Linux host built from
6=deploy/cloud-init.yaml=, with the daemon installed by =make deploy=
7(=deploy/install.sh=) and the CI runner by =make deploy-runner=. The
8statements in this document about the host rest on those files.
9
10* Listeners
11
12| Port / path | Protocol | Owner | Default | Auth | Code |
13|----------------------+-------------------+------------+-------------+----------------------------------------+---------------------------------------|
14| 22/tcp | SSH | gitbayd | on | public key; unknown keys only reach =register= when registration is open | =cmd/gitbayd/main.go=, =internal/sshd/sshd.go= |
15| 443/tcp | HTTPS | gitbayd | on | none for pages; session cookie; bearer token for the API | =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= |
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= |
20
21With =ssh.mode = system= the host's sshd serves port 22 instead and
22invokes =gitbayd authorized-keys= and =gitbayd shell=
23(=cmd/gitbayd/main.go=).
24
25HTTP server limits: =ReadHeaderTimeout= 10 s, =IdleTimeout= 2 min,
26=MaxHeaderBytes= 64 KiB, no =WriteTimeout= so long git transfers and
27live build logs can stream (=cmd/gitbayd/main.go=).
28
29There is no metrics endpoint. =/healthz= reports the deployed commit and
30a database check.
31
32* Processes and accounts
33
34| Unit | User | Hardening (from the unit files) |
35|------------------------+-------------+---------------------------------------------------------------------------------------------------|
36| =gitbayd.service= | =gitbay= | =CAP_NET_BIND_SERVICE= only; =NoNewPrivileges=; =ProtectSystem=strict= with write access to =/var/lib/gitbay= and =/var/backups/gitbay= only; =ProtectHome=; =PrivateTmp=; =PrivateDevices=; kernel, clock and cgroup protections; =RestrictNamespaces=; =MemoryDenyWriteExecute=; =RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX=; =SystemCallFilter=@system-service= (=deploy/cloud-init.yaml=) |
37| =gitbay-runner.service=| =ci-runner= | =MemoryMax=6G=, =CPUQuota=300%=, =Delegate=yes=, =KillMode=mixed=, =RestrictSUIDSGID=yes=. =NoNewPrivileges=, =ProtectKernelTunables= and =ProtectControlGroups= are relaxed because rootless podman needs =newuidmap=, a proc mount and a writable delegated cgroup; the reasons are in =deploy/gitbay-runner.override.conf= |
38| CI containers | subordinate uids of =ci-runner= | rootless podman, =--pull=never=, operator-provisioned image; see [[file:07-CI-and-Supply-Chain.org][7. CI]] |
39| backup, db-backup, gc, monitor timers | =gitbay= | nightly full archive, hourly database snapshot, weekly =git gc=, hourly health heartbeat (=deploy/cloud-init.yaml=) |
40
41* Filesystem
42
43| Path | Contents | Mode set by code / deploy |
44|-----------------------------------+--------------------------------------------+---------------------------|
45| =/var/lib/gitbay= (=server.root=) | everything below | 0750 (cloud-init) |
46| =<root>/gitbay.db= | SQLite database | 0640 (=internal/store/store.go=) |
47| =<root>/repos/<owner>/<name>.git= | bare repositories | process umask |
48| =<root>/lfs= | LFS objects, content-addressed | 0755 directories (=internal/lfs/lfs.go=) |
49| =<root>/ssh/host_ed25519= | SSH host key | 0600 in a 0700 directory (=internal/sshd/sshd.go=) |
50| =<root>/acme= | ACME account key and certificates | autocert defaults |
51| =<root>/hooks= | generated hook scripts | 0755 |
52| =/etc/gitbay/config.toml= | configuration, including SMTP password | 0640 (cloud-init) |
53| =/var/backups/gitbay= | backup archives | 0750 (cloud-init) |
54
55* Outbound connections from gitbayd
56
57| Destination | Trigger | TLS | Guard |
58|------------------------+-------------------------------+----------------------------------------------+----------------------------------------------------------------|
59| ACME directory | certificate issue and renewal | yes | host policy limits names to the site and claimed pages domains (=main.go=) |
60| SMTP relay | queued mail | STARTTLS when offered | Go's =PlainAuth= will not send credentials over plaintext to a non-local host (=internal/mail/mail.go=) |
61| APNs | queued push | yes, HTTP/2 | provider token signed with the operator's .p8 key |
62| Webhook URLs | recorded events | yes when https; certificate verified | private, loopback and link-local targets refused at save and again at connect time; no redirects (=internal/webhook/webhook.go=) |
63| Mirror URLs | mirror schedule | per URL | the same address check at save time (=internal/control/mirrorcmd.go=); git makes the connection, so there is no connect-time re-check |
64| Package registries | dependency checks | yes | fixed hosts; only the package name varies (=internal/deps/registry.go=) |
65
66* Host firewall
67
68=deploy/cloud-init.yaml= opens 22, 80, 443 and 2222 inbound with ufw.
69Outbound traffic is not restricted, including from CI containers. See
70[[file:10-Known-Gaps.org][10. Known gaps]].
71
72* Change path
73
741. A signed commit merged to =main= through a merge request (direct
75 pushes to =main= are refused by =require-mr=).
762. =make deploy= refuses a dirty tree (=Makefile= =preflight=), builds
77 with the commit stamped in, copies the binary over operator SSH,
78 runs =gitbayd check-config=, restarts the unit
79 (=deploy/install.sh=).
803. =/healthz= reports the commit now serving.
.gitbay/wiki/Architecture/04-Trust-Boundaries.org added +132
@@ -0,0 +1,132 @@
1#+title: 4. Trust boundaries and data flows
2
3[[file:diagrams/04-trust-boundaries.svg]]
4
5* Zones
6
7| Zone | Contents | Trust |
8|------+-----------------------------------------------------------------+-----------------------------------------------------------|
9| Z0 | The internet: visitors, clients, webhook and mirror endpoints | none |
10| Z1 | gitbayd process | holds all policy; trusted |
11| Z2 | Local state: SQLite, repositories, LFS, keys, config | trusted; readable by the =gitbay= user |
12| Z3 | git subprocesses and hook processes | run as =gitbay= on data from Z0; their decisions come from Z1 |
13| Z4 | CI runner service | trusted to report honestly; holds secrets for trusted builds |
14| Z5 | CI containers | untrusted code from repositories and forks |
15| Z6 | Operator host access | root; outside every in-application control |
16
17* Boundaries
18
19| ID | Boundary | What crosses | Control at the boundary |
20|-----+------------------------------------+--------------------------------------------------+-----------------------------------------------------------------------------------------|
21| 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= |
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]]) |
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 |
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) |
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]]) |
29| TB9 | user content → browser | Markdown and Org bodies, READMEs, filenames | HTML sanitised (=ugcHTML=, =internal/httpd/web.go=, bluemonday); CSP =script-src 'none'= |
30| TB10| Z6 → everything | host shell | operator SSH on 2222, keys only, fail2ban; append-only offsite backup credentials |
31
32* Flows
33
34Each flow lists its hops in order. Boundary IDs refer to the table
35above.
36
37** A. SSH control command
38
391. Client opens SSH; =authenticate= looks up the key fingerprint and
40 records user id, key id and scope in the connection
41 (=internal/sshd/sshd.go=). TB1.
422. Each exec request: =runExec= reloads the account, touches the key's
43 last-used time, calls =Exec= (=sshd.go=).
443. =Exec= tokenizes the command line (no shell) and routes git
45 transport verbs to =runGit=, everything else to =control.Dispatch=
46 with =Source= set to the key fingerprint (=sshd.go=).
474. =Dispatch= gates and runs the handler; mutating successes are
48 audited (=internal/control/control.go=). TB3.
49
50** B. git push over SSH
51
52[[file:diagrams/06-push-flow.svg]]
53
541. =runGit= resolves the repository, applies the deploy-key or account
55 checks, archive and pull-mirror refusals and the owner's storage
56 quota (=sshd.go=). TB3.
572. =git receive-pack= runs with the hook socket path, repository id,
58 user id and key scope in its environment (=sshd.go=). TB4.
593. git runs =pre-receive=, which is =gitbayd hook pre-receive=. It
60 reads the ref updates, computes ancestry in git's quarantine
61 environment and asks the daemon over the socket
62 (=cmd/gitbayd/hook.go=). TB5.
634. The daemon applies =policy.CheckPush= (protected branches,
64 =require-mr=, protected tags, server-owned =refs/merge-requests/*=)
65 and, when the repository requires signed commits, asks for every
66 incoming commit object and verifies each
67 (=internal/hookd/hookd.go=).
685. On refusal the hook exits 1 and git rejects the push atomically.
696. On success git runs =post-receive=; the daemon records events,
70 marks mirrors dirty, queues CI, syncs merge request heads, processes
71 =Closes #N= references and audits force pushes
72 (=hookd.go=).
73
74The server's own merge of a merge request does not pass through the
75hooks: =runMRMerge= updates the ref with a compare-and-swap
76(=internal/control/mr.go=) after =MergeGates=
77(=mr.go=). When signed commits are required only fast-forward
78merges are allowed, so the server never writes an unsigned commit
79(=mr.go=).
80
81** C. Fetch over smart HTTP
82
83=GET info/refs= and =POST git-upload-pack= serve public repositories
84only; a private repository answers 404. =git-receive-pack= over HTTP
85always answers a pkt-line refusal, so there is no password prompt and
86no HTTP write path (=internal/httpd/smart.go=,
87=routes.go=). TB2.
88
89** D. Web read and write
90
911. The session cookie is hashed and looked up (=accounts.go=).
922. Pages dispatch read commands into the registry with =Source=web= and
93 decode the JSON result into the template (=internal/httpd/control.go=).
943. Form posts pass =checkOrigin= (=accounts.go=), dispatch the
95 matching command, and map the exit code to a redirect or an error on
96 the page. Three toggles (pin, watch, mark read) write the store
97 directly instead (#261).
98
99** E. JSON API
100
1011. =apiAuth= hashes the bearer token and looks it up; any failure is a
102 uniform 401 (=internal/httpd/api.go=).
1032. Per-account rate limit, with writes at a tenth of the read budget.
1043. =POST /api/v1/cmd= dispatches any command except git transport;
105 =GET /api/v1/read= refuses anything not marked =ReadOnly=, so a GET
106 cannot write (=apiread.go=).
1074. Exit codes map to HTTP status: 0→200, 2→400, 3→404, 4→403, else 500.
108
109** F. LFS
110
111=git-lfs-authenticate= over SSH applies the same repository checks as
112git transport and returns a one-hour HMAC token scoped to repository
113and operation (=internal/sshd/lfs.go=, =internal/lfs/lfs.go=). The
114HTTP batch, upload and download endpoints verify that token; public
115repositories allow anonymous download. Objects are verified against
116their SHA-256 id on upload.
117
118** G. Browser login by emailed link
119
1201. =/login= takes a username or email; per-IP and per-account limits
121 (5 per hour) apply, and every non-eligible case returns the same
122 response (=internal/control/loginlink.go=).
1232. A 32-byte token is mailed; only its hash is stored, valid 15 minutes.
1243. =/login?token== consumes it atomically, rechecks the account and
125 sets the session cookie (=accounts.go=).
126
127=ssh git@host web login= mints the same kind of link, valid 5 minutes,
128to an already-authenticated key (=internal/control/web.go=).
129
130** H. CI build
131
132See [[file:07-CI-and-Supply-Chain.org][7. CI and supply chain]].
.gitbay/wiki/Architecture/05-Identity-and-Access.org added +133
@@ -0,0 +1,133 @@
1#+title: 5. Identity and access
2
3[[file:diagrams/05-authorization.svg]]
4
5* Accounts
6
7| Property | Values / behaviour | Code |
8|--------------+-------------------------------------------------------------------------------------+------|
9| Registration | =closed= (host bootstrap only), =invite= (single-use code bound to an email), =open= (email required) | =internal/control/register.go= |
10| Pending | open registrations start pending until email is verified; a pending account may run only =email verify=, =email add=, =whoami=, =help=, and has no git access | =internal/control/control.go=, =internal/sshd/sshd.go= |
11| Disabled | refused in =Dispatch=, in SSH exec and at login-link redemption | =control.go= |
12| Admin | =users.is_admin=; set only by =admin user create --admin= or promotion by an admin; gates the whole =admin= noun | =control.go= |
13
14* Credentials
15
16| Credential | Format and generation | Stored as | Scope | Expiry | Revocation |
17|--------------------+-------------------------------------------------+----------------------------------+--------------------------------------------+-------------------------------+-------------------------------------|
18| SSH user key | user's public key | fingerprint and public blob | =full=, =git=, or =runner= | none | =keys remove= (own keys) |
19| Deploy key | public key | same table, scope =deploy:<repo>:ro/rw= | one repository, read or read-write | none | =repo deploy-key remove= (repo admin) |
20| API token | =gb_= + 32 random bytes hex | SHA-256 hash | =full= or =read= | optional =--ttl= | =token revoke= |
21| Web session | 32 random bytes hex, cookie =gitbay_session= | SHA-256 hash | full account | 7 days, no sliding renewal | logout, =web sessions revoke= |
22| Login link | 32 random bytes hex in a URL | SHA-256 hash, single use | creates a web session | 15 min (mail), 5 min (SSH) | consumed on use |
23| Email verification | 32 random bytes hex | SHA-256 hash, single use | verifies one address for one account | 24 h | consumed on use |
24| Invite | random code | SHA-256 hash, single use | one registration for one email | as issued | consumed on use |
25| LFS transfer token | HMAC-SHA256 over repo, operation, expiry | not stored (stateless) | one repository, upload or download | 1 h | expiry only |
26
27Generation and hashing: =internal/store/sessions.go= (=NewToken=,
28=HashToken=, =crypto/rand=). Cookie attributes: =HttpOnly=,
29=SameSite=Lax=, =Secure= unless TLS is off, =MaxAge= 7 days
30(=internal/httpd/accounts.go=). Token scope values are
31constrained by a database =CHECK= as well as the command
32(=internal/store/migrations/0004_api_tokens.up.sql=).
33
34What the scopes allow:
35
36| Scope | Control commands | git transport |
37|----------------+---------------------------------+-----------------------------------|
38| =full= key | all the account may run | read and write |
39| =git= key | none | read and write |
40| =runner= key | =runner *= only, on attached repositories | read (clone) |
41| =deploy:*= key | none | its repository only, =ro= or =rw= |
42| =full= token | all the account may run | not over the API |
43| =read= token | commands marked =ReadOnly= | not over the API |
44
45Sources: =internal/control/control.go=,
46=internal/policy/access.go=.
47
48* Authorization decision
49
50Two layers decide every request.
51
521. *=Dispatch=* (=internal/control/control.go=), in order:
53 scope gate, read-only gate, disabled account, =admin= noun, pending
54 account, per-account write budget, stdin gating, handler, audit.
552. *The handler*, which resolves the repository with =resolveRepo=
56 (=internal/control/repo.go=) and a predicate from
57 =internal/policy/access.go=:
58
59| Predicate | True when |
60|------------+--------------------------------------------------------------------------|
61| =CanRead= | owner; or the repository is public; or a grant of read, write or admin |
62| =CanWrite= | owner; or a grant of write or admin |
63| =CanAdmin= | owner; or a grant of admin |
64
65Grants come from =repo_access= rows for users, organizations and teams.
66Organization-owned repositories have no individual owner; members get
67access only through grants.
68
69Not found versus denied: when the predicate fails, =resolveRepo=
70checks =CanRead=. If the caller cannot read the repository the answer is
71"not found", identical to a missing repository. Only a caller who can
72read it gets "permission denied" for a write. git transport follows the
73same rule (=internal/sshd/sshd.go=), and so does smart HTTP, which
74serves public repositories only.
75
76Deploy keys bypass the grant model entirely: =runGit= checks only that
77the key's scope names this repository and allows the operation
78(=policy.DeployScopeAllows=, =internal/policy/access.go=).
79
80* Repository write protections
81
82Enforced in the pre-receive hook, so they apply to every credential
83that reaches git transport (=internal/policy/access.go=,
84=internal/hookd/hookd.go=):
85
86| Setting (=repo settings …=) | Effect |
87|---------------------------+-------------------------------------------------------------------------|
88| =protect= | no deletion, no force push on the branch |
89| =require-mr= | no direct push to an existing protected branch; only the server's merge writes it |
90| =protect-tag= | no deletion or move of matching tags |
91| =require-signed= | every incoming commit must verify (OpenPGP or SSHSIG) against a key registered to a verified email |
92| (always) | =refs/merge-requests/*= is server-owned |
93
94Merge gates, evaluated by =MergeGates= for =mr merge=, the web merge
95button and the displayed status (=internal/control/mr.go=):
96
97| Gate | Satisfied when |
98|-----------------------+-----------------------------------------------------------------------------|
99| draft | the merge request is not a draft (always on) |
100| =require-checks= | every status on the head is success, and some status exists if CI would have run |
101| =require-approvals N= | N fresh approvals from current writers other than the author, and no active request for changes |
102| =require-codeowners= | every changed path matching a CODEOWNERS rule has an approval from a listed owner |
103| =require-resolved= | no unresolved review threads |
104
105* Session security on the web
106
107- CSRF: =SameSite=Lax= withholds the cookie on cross-site posts;
108 =checkOrigin= rejects a post whose =Origin= host differs from the
109 request host (=internal/httpd/accounts.go=).
110- Headers on every response: CSP =default-src 'self'; script-src
111 'none'; style-src 'self' 'unsafe-inline'; img-src * data:;
112 object-src 'none'; base-uri 'none'; form-action 'self';
113 frame-ancestors 'none'=, =X-Frame-Options: DENY=, =nosniff=,
114 =Referrer-Policy: no-referrer=, =Cross-Origin-Opener-Policy:
115 same-origin=, and HSTS for one year with subdomains when TLS is on
116 (=internal/httpd/routes.go=).
117- Destructive web actions (key, email and PGP removal, release, snippet,
118 team and label deletion, user disable and demote) require the target's
119 name typed into the form (=internal/httpd/confirm.go=).
120
121* Rate limits
122
123| Limit | Default | Keyed by | Code |
124|-------------------------+-------------------+-----------------------+---------------------------------------|
125| SSH auth failures | 10 per minute | client IP | =internal/sshd/ratelimit.go= |
126| API requests | 120 per minute | account, or IP if anonymous | =internal/httpd/apilimit.go= |
127| API writes | a tenth of the above | same | =apilimit.go= |
128| Command writes, all surfaces | =limits.write_rate= (60 per minute) | account | =internal/control/control.go= |
129| Login links (web form) | 5 per hour, plus per-IP | account and IP | =internal/control/loginlink.go= |
130| Email verification mails| 5 per hour | account | =internal/control/register.go= |
131
132=X-Forwarded-For= is honoured only from addresses listed in
133=http.trusted_proxies= (=internal/httpd/apilimit.go=).
.gitbay/wiki/Architecture/06-Data-and-Cryptography.org added +116
@@ -0,0 +1,116 @@
1#+title: 6. Data and cryptography
2
3* Data inventory
4
5Schema: =internal/store/migrations/=, 59 migrations. Classification:
6*C* credential or secret, *P* personal data, *R* private repository
7content (as confidential as the repository), *O* operational.
8
9| Domain | Tables | Class | Notes |
10|-----------------+---------------------------------------------------------------------------------------------+-------+-------------------------------------------------|
11| Identity | =users=, =emails=, =ssh_keys=, =pgp_keys=, =orgs=, =org_members=, =teams=, =team_members= | P | email addresses in clear; keys are public |
12| Credentials | =api_tokens=, =web_sessions=, =login_tokens=, =email_tokens=, =invites= | C | SHA-256 hashes only |
13| Repositories | =repos=, =repo_access=, =team_repos=, =repo_topics=, =repo_watchers=, =repo_pins=, =repo_bookmarks=, =page_domains= | O | |
14| Collaboration | =issues=, =issue_*=, =merge_requests=, =mr_*=, =labels=, =milestones=, =mentions= | R | bodies of issues, comments and reviews |
15| Releases, snippets | =releases=, =release_assets=, =snippets=, =snippet_files= | R | |
16| CI | =builds= (includes logs), =build_schedules=, =runner_repos=, =runner_seen= | R | build logs can echo anything a step prints |
17| CI secrets | =build_secrets= | C | *plaintext* |
18| Integrations | =webhooks= (secret), =webhook_deliveries=, =mirrors= (username, token) | C | *plaintext* secrets and tokens |
19| Notifications | =notifications= (mail queue), =inbox=, =push_devices= (APNs token), =push_queue= | P | device tokens in clear |
20| Signatures | =commit_signatures=, =settings.key_epoch= | O | verification cache |
21| Audit and feed | =audit_log=, =events= | O, P | actor ids, pruned argv, fingerprints and IPs in some audit rows |
22| Dependencies | =dep_checks=, =dep_reports= | O | |
23
24Outside the database:
25
26| Data | Location | Class |
27|----------------------------+-----------------------------------+-------|
28| Repository contents | =<root>/repos= | R |
29| LFS objects | =<root>/lfs= | R |
30| SSH host key | =<root>/ssh/host_ed25519= | C |
31| TLS keys (ACME) | =<root>/acme= | C |
32| SMTP password | =/etc/gitbay/config.toml= | C |
33| APNs signing key (.p8) | path in =push.key_file= | C |
34| Backups | =/var/backups/gitbay=, offsite | all of the above |
35
36No table stores client IP addresses as a column. The daemon writes a
37client IP into an audit row only for authentication failures and
38throttling (=internal/sshd/sshd.go=).
39
40* At rest
41
42| Item | Protection |
43|---------------------------------------+----------------------------------------------------------------|
44| API tokens, sessions, login links, email codes, invites | SHA-256 of a 256-bit random value; the value is shown once and never stored (=internal/store/sessions.go=) |
45| CI secrets, webhook secrets, mirror tokens, APNs device tokens | stored in clear in SQLite; protection is filesystem permissions and the rule that values are write-only through the interface |
46| SQLite file | mode 0640, directory 0750 |
47| Backups | the local archive is not encrypted; restic encrypts the offsite copy |
48| Disk | no application-level encryption; any disk encryption is the host's |
49
50The code base contains no symmetric encryption. A database or backup
51file read by anyone other than the =gitbay= user discloses every CI
52secret, webhook secret and mirror token.
53
54* In transit
55
56| Channel | Protection |
57|--------------------------+--------------------------------------------------------------|
58| SSH | Go =x/crypto/ssh=; ed25519 host key generated on first start |
59| HTTPS | TLS via ACME or operator certificates; HSTS one year |
60| HTTP port 80 | ACME challenges and redirect only |
61| git:// | none (public data only; off by default) |
62| Runner ↔ server | SSH |
63| SMTP | STARTTLS when the relay offers it |
64| APNs | TLS, HTTP/2 |
65| Webhooks | TLS when the URL is https; HMAC-SHA256 body signature in =X-Gitbay-Signature-256= (=internal/webhook/webhook.go=) |
66| Mirrors | per URL; token passed through =GIT_ASKPASS=, never argv (=internal/mirror/mirror.go=) |
67
68The TLS configuration uses Go's defaults; no minimum version or cipher
69list is set in code.
70
71* Cryptographic primitives
72
73| Use | Primitive | Code |
74|---------------------------------+--------------------------------------------+------------------------------------|
75| Token generation | =crypto/rand=, 32 bytes | =internal/store/sessions.go= |
76| Token storage | SHA-256 | =sessions.go= |
77| LFS transfer tokens | HMAC-SHA256, secret in =settings= | =internal/lfs/lfs.go= |
78| Webhook signatures | HMAC-SHA256 | =internal/webhook/webhook.go= |
79| APNs provider token | ES256 JWT (ECDSA P-256) | =internal/push/token.go= |
80| SSH host key | ed25519 | =internal/sshd/sshd.go= |
81| Commit and tag signatures | verify OpenPGP (ProtonMail go-crypto) and SSHSIG | =internal/sig= |
82| LFS object ids | SHA-256 | =internal/lfs/lfs.go= |
83
84Signature verification results are cached in =commit_signatures= with
85the global =key_epoch= at the time of verification. Any change to a
86trust input (a key added or removed, an email verified) bumps the
87epoch, which invalidates every cached result (=internal/store/users.go=,
88=internal/control/sig.go=).
89
90The server holds no signing key and signs nothing. A "verified" badge
91means a user's own key signed the commit.
92
93* Secret handling rules
94
95- Secrets enter only on stdin. A command must set =ReadsStdin=
96 to receive stdin at all; =TestStdinCommandsReadStdin= enforces it.
97 Examples: =repo secret set=, =repo deploy-key add=, =repo import
98 --token-stdin= (=internal/control/build.go=, =import.go=).
99- Secrets are listed by name, never echoed back.
100- The audit log stores argv with flag values stripped
101 (=internal/control/control.go=).
102- Mail errors are logged with addresses redacted
103 (=internal/notify/notify.go=).
104- CI secrets travel in the runner's claim only for trusted builds and
105 reach the container as environment variables through a 0600 env file
106 or podman's =--env NAME= pass-through, never argv
107 (=cmd/gitbay-runner/isolate.go=).
108
109* Retention
110
111Configured under =[retention]= for =audit=, =events=,
112=webhook_deliveries=, =mail= and =push=; unset means keep forever.
113Expired sessions and tokens are swept hourly regardless
114(=internal/config/config.go=, =cmd/gitbayd/main.go=).
115Accounts that never verify are removed after
116=registration.pending_expiry=. =account export= gives a user their data.
.gitbay/wiki/Architecture/07-CI-and-Supply-Chain.org added +93
@@ -0,0 +1,93 @@
1#+title: 7. CI and supply chain
2
3[[file:diagrams/07-ci-flow.svg]]
4
5* Pipeline definition
6
7=.gitbay/ci.yml= at the pushed commit (=internal/ci/ci.go=):
8
9| Limit / rule | Value |
10|------------------------------+---------------------------------------------------------------|
11| jobs per file | 10 |
12| steps per job | 50, each at most 4096 bytes |
13| path filters | 50 each for =paths= and =paths-ignore= |
14| job name | =^[a-z0-9][a-z0-9_-]{0,39}$= |
15| image | a restricted reference; it becomes a podman argument, so no whitespace or shell characters (=ci.go=) |
16| triggers | push, merge request, =schedule= (cron), =tags= (glob) |
17
18A file that does not parse sets a =ci/config= failure status on the
19commit instead of failing silently.
20
21* Build lifecycle
22
231. *Queue.* The post-receive hook calls =queueJobs=
24 (=internal/control/build.go=). Each job gets a =ci/<job>= status:
25 =pending= when queued, =skipped= when path filters exclude it, or
26 =success= copied from an earlier build of the same tree (#177).
27 Merge requests from forks are queued against the target repository
28 with =trusted = false=.
292. *Claim.* A runner calls =runner next= over SSH
30 (=build.go=). Allowed for a =runner=-scoped key or an admin; a
31 runner key claims only for repositories it is attached to with
32 =repo runner add=. Untrusted builds are claimable only by a runner
33 started with =-untrusted= (=internal/store/builds.go=). The
34 claim returns id, repository, job, commit, ref, steps, image and —
35 for trusted builds only — the repository's secrets (=build.go=).
363. *Run.* The runner clones over SSH into =build-<id>=, starts a
37 container and runs each step with =podman exec … sh -c <step>=
38 (=cmd/gitbay-runner/isolate.go=).
394. *Log.* =runner log <id>= streams stdin into the build row; the server
40 ends the stream if the build is cancelled (=build.go=).
415. *Result.* =runner done <id> success|failure= sets the status,
42 records an event and mails the repository's watchers a log tail on
43 failure (=build.go=).
446. *Reap.* The scheduler fails a running build whose log stream closed
45 more than 2 minutes ago, or that started more than 90 minutes ago
46 (=internal/store/builds.go=).
47
48Who may do what:
49
50| Action | Requirement |
51|------------------------------------+------------------------------------------------|
52| =build list/show/log/jobs= | read on the repository |
53| =build trigger=, =build cancel= | write on the repository |
54| =repo secret set/remove/list= | admin on the repository |
55| =repo runner add/remove= | admin on the repository |
56| =runner next/log/done= | =runner= key attached to the repository, or admin |
57| =status set= | write on the repository (any context name; #258) |
58
59* Runner isolation
60
61| Control | Implementation |
62|----------------------------+----------------------------------------------------------------------------|
63| Isolation mode | =podman= by default; =none= must be chosen explicitly and logs a warning; an unknown value or missing prerequisites refuse start (=isolate.go=) |
64| Container runtime | rootless podman under the =ci-runner= user and its subordinate uid range |
65| Image | =--pull=never=; images are built by the operator (=deploy/Containerfile.ci=) and referenced by tag |
66| Workspace | =<workdir>/build-<id>=, removed after the build; workdir must be 0700 and owned by the runner (=main.go=) |
67| Build home | =<workdir>/home/<owner>/<name>=, one per repository, mounted read-write, shared by trusted and untrusted builds of that repository (#255) |
68| Secrets | env file 0600 outside the workspace, or =--env NAME= for multi-line values |
69| Resources | per-build cgroup with =memory.max= and =cpu.max= written by the runner; unit-level =MemoryMax=6G=, =CPUQuota=300%= |
70| Network | podman default (pasta); outbound unrestricted (#260) |
71| Shutdown | SIGTERM stops claiming and drains in-flight builds; the unit uses =KillMode=mixed= |
72
73* Integrations
74
75| Integration | Trigger | Security properties |
76|-------------+----------------------+--------------------------------------------------------------------------------|
77| Webhooks | recorded events | SSRF checks at save and connect time, no redirects, HMAC-SHA256 signature, 5 attempts with exponential backoff, response body capped at 4 KiB (=internal/webhook/webhook.go=) |
78| Mirrors | schedule | address check at save; token via =GIT_ASKPASS= script (0700); heads and tags only; 10-minute timeout (=internal/mirror/mirror.go=) |
79| Dependency checks | schedule, opt-in | fixed registry hosts; package names restricted (=internal/deps/registry.go=) |
80
81* The project's own supply chain
82
83| Stage | Control |
84|----------------+---------------------------------------------------------------------------------------------|
85| Source | krz/gitbay on the instance itself; signed commits required, fast-forward merges only; =require-mr= on =main= |
86| Dependencies | 15 direct Go modules (=go.mod=); pure-Go SQLite (=modernc.org/sqlite=), no cgo |
87| CI | =build= (build, vet) and =test= (full suite against real git, ssh, sshd, gpg) on every push; =vuln= (govulncheck) nightly and before release (=.gitbay/ci.yml=) |
88| Static checks | =deploy/audit.sh=: vet, govulncheck, short fuzz runs of the pkt-line, commit, signature, PGP key and tokenizer parsers |
89| Build | =CGO_ENABLED=0 -trimpath -ldflags='-s -w -buildid='= for reproducible binaries; the commit is stamped in (=deploy/release.sh=, =Makefile=) |
90| Release | =SHA256SUMS= for every binary; a minisign signature of the manifest when the release key is present (optional) |
91| Distribution | release assets on the forge; Homebrew formula in krz/homebrew-tap built from the tag; push mirror to GitHub (read-only copy) |
92| Deploy | =make deploy= refuses a dirty tree, then copies, checks config and restarts over operator SSH |
93| CI image | built on the host from =deploy/Containerfile.ci= (=golang:1.27-trixie= plus git-lfs, gnupg, openssh, python3, sqlite3); tagged, never pulled at build time |
.gitbay/wiki/Architecture/08-Operations.org added +89
@@ -0,0 +1,89 @@
1#+title: 8. Operations
2
3* Logging
4
5- The daemon logs with Go's =log/slog= default handler to stderr, which
6 systemd sends to the journal. Retention is the journal's.
7- Logged: listener start-up, schema version, worker failures (webhook,
8 mail, push, mirror), sweeps and reaps with counts, SSH lookup errors.
9- Not logged: request bodies, tokens, secrets. Mail errors are logged
10 with addresses redacted.
11
12* Audit log
13
14Table =audit_log=: actor, action, JSON data, time
15(=internal/store/audit.go=). Readable by admins with =audit=.
16
17| Recorded | How |
18|----------------------------------------------+------------------------------------------------------|
19| Every successful mutating command, every surface | =Dispatch= writes =cmd <path>= with pruned argv and the source: key fingerprint, =web=, =api= or =host= (=internal/control/control.go=) |
20| SSH authentication failures and throttling | =auth.failed= (IP, fingerprint), =auth.throttled= (IP) |
21| Registration | =auth.registered=, =pending.expired= |
22| Administration | =admin user.*=, =admin email.*=, =admin invite.issued=, =admin repo.*=, =admin mr.prune=, =admin runners.forget= |
23| Repository events of security interest | =push.forced=, =repo.runner.add/remove=, =pages.domain_verified= |
24
25Failed commands and reads are not audited. The separate =events= table is
26the product activity feed, not an audit trail.
27
28* Monitoring
29
30- =/healthz= returns the serving commit and a database check.
31- =deploy/cloud-init.yaml= installs an hourly heartbeat that checks the
32 service, disk, certificate expiry and backup age, and can POST to an
33 external monitor URL.
34- =admin runners= reports the build queue: pending builds, claims and
35 average and worst claim wait over 24 hours, reaped builds, and each
36 runner key's last poll.
37
38* Patching
39
40- Host: =unattended-upgrades= with automatic security updates and a
41 04:30 reboot (=deploy/cloud-init.yaml=).
42- Application: =govulncheck= nightly in CI; a module update is a
43 normal merge request and deploy.
44- CI image: rebuilt by the operator when =deploy/Containerfile.ci=
45 changes; weekly =podman image prune= removes old images.
46
47* Backup and recovery
48
49| Item | Schedule | Kept | Contents |
50|-----------------+----------+------+-----------------------------------------------------------------|
51| Full archive | nightly | 7 | SQLite snapshot (=VACUUM INTO=), all repositories, LFS, SSH host keys |
52| Database only | hourly | 48 | SQLite snapshot |
53| Offsite (restic)| nightly | per prune policy | =/var/lib/gitbay= and a staged database copy, to object storage |
54
55- The database snapshot is taken before repositories are read, so a
56 push during the backup leaves only unreferenced objects
57 (=cmd/gitbayd/backup.go=).
58- Excluded: WAL files, the hook socket, askpass scripts, generated
59 hooks.
60- =gitbayd admin backup --verify= checks SQLite integrity and that every
61 repository the database names is present (=backup.go=). It does
62 not check git object connectivity.
63- The host's restic credentials are append-only; the key that can
64 delete or prune snapshots is held off the host, so a compromised host
65 cannot destroy its own history (documented: Admin wiki).
66- Recovery point: about one hour for database-only data (issues, merge
67 requests, reviews), one day for repositories.
68- Recovery time: not measured. No restore onto a clean host has been
69 recorded (#259).
70
71Restore procedure: extract the archive into an empty directory, point
72=server.root= at it, start =gitbayd=; hooks regenerate and the host key
73is preserved.
74
75* Operator levers during an incident
76
77| Need | Command |
78|------------------------------------+--------------------------------------------------|
79| Stop a user | =admin user disable <name>= |
80| Remove a key | =keys remove= (own) or =admin user= commands |
81| Kill a user's browser sessions | =web sessions revoke --all= (as that user) |
82| Revoke a token | =token revoke <name>= |
83| Stop a runner key claiming | =repo runner remove=, =admin runners forget <fingerprint>= |
84| Hide a repository | =admin repo visibility <repo> private= |
85| Close registration | =registration.mode = "closed"= and restart |
86| See what happened | =audit= (filter by actor, action, time) |
87
88Open connections of a removed key keep working until they close; see
89#256.
.gitbay/wiki/Architecture/09-Controls.org added +104
@@ -0,0 +1,104 @@
1#+title: 9. Controls matrix
2
3One row per control an auditor typically asks about. *Status*: =in
4place= (implemented and cited), =partial= (implemented with a stated
5limit), =gap= (not implemented; see [[file:10-Known-Gaps.org][10]]). Categories follow the
6chapter names of OWASP ASVS 4.0 where one fits.
7
8** Architecture (V1)
9
10| Control | Status | Evidence |
11|---------------------------------------------+----------+------------------------------------------------------------------|
12| One authorization path for every surface | partial | all surfaces call =control.Dispatch= (=internal/control/control.go=); three web toggles write the store directly (#261) |
13| No server-side signing key | in place | =internal/sig= verifies only |
14| Least functionality by default | in place | API, web accounts, git://, push and registration default off (=internal/config/config.go=) |
15| No git library; git runs as a subprocess with built argv | in place | =internal/gitutil= |
16
17** Authentication (V2) and session management (V3)
18
19| Control | Status | Evidence |
20|---------------------------------------------+----------+------------------------------------------------------------------|
21| No passwords anywhere | in place | SSH keys, emailed single-use links, bearer tokens |
22| Credentials stored as hashes | in place | SHA-256 of 256-bit random values (=internal/store/sessions.go=) |
23| Brute-force limit on SSH auth | in place | 10 failures a minute per IP (=internal/sshd/ratelimit.go=) |
24| Account enumeration resistance at login | in place | uniform response (=internal/control/loginlink.go=) |
25| Session cookie flags | in place | HttpOnly, SameSite=Lax, Secure with TLS (=internal/httpd/accounts.go=) |
26| Session lifetime | partial | 7 days absolute, no idle timeout |
27| Credential expiry | partial | API tokens optional; SSH and deploy keys none |
28| Revocation takes effect immediately | gap | removed SSH key keeps open connections (#256) |
29| Delegation bounded by the delegating credential | gap | expiring tokens can mint lasting credentials (#257) |
30
31** Access control (V4)
32
33| Control | Status | Evidence |
34|---------------------------------------------+----------+------------------------------------------------------------------|
35| Deny by default on private data | in place | =CanRead= requires owner, public or grant (=internal/policy/access.go=) |
36| Private resources indistinguishable from missing | in place | =resolveRepo= (=internal/control/repo.go=), =runGit=, smart HTTP |
37| Credential scopes narrow account rights | in place | key and token scopes (=control.go=, =policy/access.go=) |
38| Server-side write protections | in place | pre-receive =CheckPush=, signed commits (=internal/hookd/hookd.go=) |
39| Merge gates | partial | =MergeGates=; any writer can post a =ci/*= status (#258) |
40| Admin functions isolated | in place | =admin= noun gated in =Dispatch=; =audit= admin-only |
41| CSRF protection | in place | SameSite=Lax plus =checkOrigin= (=accounts.go=) |
42| Typed confirmation for destructive web actions | in place | =internal/httpd/confirm.go= |
43
44** Input handling and output encoding (V5)
45
46| Control | Status | Evidence |
47|---------------------------------------------+----------+------------------------------------------------------------------|
48| User markup sanitised | in place | =ugcHTML= with bluemonday (=internal/httpd/web.go=) |
49| No script execution in pages | in place | CSP =script-src 'none'= (=internal/httpd/routes.go=) |
50| Control characters stripped at the terminal | in place | =termSafe= (=internal/control/term.go=) |
51| No shell in command execution | in place | =protocol.Tokenize= for SSH argv; git and podman with argv slices |
52| Parsers fuzzed | partial | five fuzz targets run briefly by =deploy/audit.sh= |
53
54** Cryptography (V6) and data protection (V8)
55
56| Control | Status | Evidence |
57|---------------------------------------------+----------+------------------------------------------------------------------|
58| TLS for all authenticated HTTP | in place | ACME or certificate files; HSTS |
59| Secrets encrypted at rest | gap | CI secrets, webhook secrets, mirror tokens stored in clear ([[file:06-Data-and-Cryptography.org][6]]) |
60| Secrets kept out of argv, logs and output | in place | =ReadsStdin=, pruned audit argv, write-only secret commands |
61| Local backups encrypted | gap | tar.gz in clear; offsite copy encrypted by restic |
62| Data retention configurable | in place | =[retention]= (=internal/config/config.go=) |
63| User data export | in place | =account export= |
64
65** Logging (V7)
66
67| Control | Status | Evidence |
68|---------------------------------------------+----------+------------------------------------------------------------------|
69| Security-relevant writes audited | in place | every successful mutating command (=control.go=) |
70| Authentication failures audited | in place | =auth.failed=, =auth.throttled= |
71| Denied attempts audited | gap | refused commands are not recorded |
72| Audit log tamper resistance | gap | same database, writable by the daemon user |
73
74** Communications and integrations (V9, V10, V12)
75
76| Control | Status | Evidence |
77|---------------------------------------------+----------+------------------------------------------------------------------|
78| SSRF protection on user-supplied URLs | partial | webhooks at save and connect; mirrors at save only |
79| Webhook payload integrity | in place | HMAC-SHA256 header |
80| SMTP credentials protected in transit | partial | STARTTLS opportunistic; Go refuses PLAIN auth without TLS to a remote host |
81| Upload size limits | in place | per-owner storage quota at push (=internal/sshd/sshd.go=); API body 1 MiB |
82
83** CI and build isolation
84
85| Control | Status | Evidence |
86|---------------------------------------------+----------+------------------------------------------------------------------|
87| Untrusted code runs isolated | partial | rootless podman, cgroup limits; shared build home per repository (#255) |
88| No secrets for untrusted builds | in place | =internal/control/build.go= |
89| Runner limited to attached repositories | in place | =runnerMayBuild= (=build.go=) |
90| Build images fixed by the operator | in place | =--pull=never= |
91| Build network egress restricted | gap | #260 |
92| Build results reused only across equal trust | gap | tree reuse ignores trust and image (#258) |
93
94** Availability and operations
95
96| Control | Status | Evidence |
97|---------------------------------------------+----------+------------------------------------------------------------------|
98| Rate limits on API and writes | in place | [[file:05-Identity-and-Access.org][5. Rate limits]] |
99| Concurrency limit on git pack generation | gap | #262 |
100| Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) |
101| Backups offsite and append-only | in place | restic with append-only credentials (documented) |
102| Restore tested | gap | #259 |
103| Migrations validated before commit | gap | foreign-key check runs after commit (#261) |
104| Signed, reviewed changes to production | in place | signed commits, =require-mr=, ff-only merges, clean-tree deploys |
.gitbay/wiki/Architecture/10-Known-Gaps.org added +51
@@ -0,0 +1,51 @@
1#+title: 10. Known gaps
2
3Open weaknesses. Issues on krz/gitbay are public; this page gives the
4title and the consequence, not a reproduction. The current list is the
5open issues labelled =security=:
6https://gitbay.org/krz/gitbay/issues?label=security. The table below is
7what the 2026-09-27 review found; remove a row when its issue closes.
8
9* Filed
10
11| Issue | Area | Gap | Severity |
12|-------+------------------+-----------------------------------------------------------------------+----------|
13| #255 | CI isolation | Untrusted and trusted builds of a repository share a writable build home | high |
14| #256 | Authentication | A removed SSH key keeps working on connections already open | high |
15| #257 | Credentials | An expiring token can create credentials that outlive it; tokens default to full scope | high |
16| #258 | CI integrity | Any writer can post a =ci/*= status; tree reuse ignores trust and image | high |
17| #259 | Recovery | No restore has been exercised; verification does not check git connectivity | high |
18| #260 | CI network | Builds share the runner's source address; no egress policy | medium |
19| #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium |
20| #262 | Availability | No limit on concurrent git pack generation | high |
21
22Decisions already taken on these: #256 closes a removed key's
23connections, running commands included; #257 refuses credential
24creation from expiring tokens, records which token created each
25credential, and makes =read= the default scope.
26
27* Not yet filed
28
29Found during the 2026-09-27 review.
30
31| Area | Gap | Where |
32|------------------+---------------------------------------------------------------------------------------+---------------------------------------------|
33| Data at rest | CI secrets, webhook secrets and mirror tokens are stored in clear in SQLite | =build_secrets=, =webhooks=, =mirrors= |
34| Backups | The local backup archive is not encrypted | =cmd/gitbayd/backup.go= |
35| Audit | Refused commands are not audited; the audit table is writable by the daemon user | =internal/control/control.go= |
36| Sessions | Web sessions have a 7-day absolute lifetime and no idle timeout | =internal/httpd/accounts.go= |
37| Credentials | SSH and deploy keys never expire | =ssh_keys= |
38| Login links | =web login= over SSH is not counted against the 5-per-hour login-link limit | =internal/control/web.go= |
39| SSRF | Mirror URLs are checked when saved but not when git connects, so a DNS change can redirect a mirror to a private address | =internal/control/mirrorcmd.go= |
40| Mail | STARTTLS is used only when the relay offers it | =internal/mail/mail.go= |
41| TLS | Go defaults; no explicit minimum version | =cmd/gitbayd/main.go= |
42| Hook socket | Any process that can open =hook.sock= can claim any user id; it relies on the data directory's permissions | =internal/hookd/hookd.go= |
43
44* Questions an auditor will ask that have no answer yet
45
46| Question | Status |
47|-----------------------------------------------------------+------------------------------------------|
48| What is the measured recovery time? | unmeasured (#259) |
49| How many concurrent clones does the host sustain? | unmeasured (#262) |
50| What can a build reach on the host's network? | configuration inspected, reachability untested (#260) |
51| Have the collaboration features been used by independent users? | no; one human user, tests only |
.gitbay/wiki/Architecture/diagrams/01-context.svg added +131
@@ -0,0 +1,131 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 590" width="970" height="590" role="img" aria-labelledby="t d">
2<title id="t">1. System context</title><desc id="d">Actors and external systems around a gitbay instance.</desc>
3<style>
4text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
5.bg{fill:#ffffff}
6.t{fill:#1a1a1a;font-size:13px}
7.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
8.ts{fill:#4d4d4d;font-size:11px}
9.th{fill:#1a1a1a;font-size:17px;font-weight:700}
10.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
11.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
12.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
13.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
14.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
15.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
16.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
17.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
18.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
19.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
20.zl{fill:#c2410c;font-size:12px;font-weight:700}
21.tag{fill:#c2410c;font-size:11px;font-weight:700}
22.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
23.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
24.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
25.ah{fill:#1a1a1a}
26.ahd{fill:#6b6b6b}
27@media (prefers-color-scheme: dark){
28.bg{fill:#121212}
29.t,.tb1,.th{fill:#ececec}
30.ts{fill:#b0b0b0}
31.gb{fill:#16233f;stroke:#7aa2ff}
32.ext{fill:#1e1e1e;stroke:#8a8a8a}
33.act{fill:#121212;stroke:#ececec}
34.st{fill:#2a1a0e;stroke:#f0a36b}
35.bad{fill:#2c1414;stroke:#f28b82}
36.ok{fill:#122417;stroke:#6fcf8f}
37.dec{fill:#121212;stroke:#7aa2ff}
38.host{stroke:#8a8a8a}
39.zone{stroke:#fb923c}
40.zl,.tag{fill:#fb923c}
41.ln{stroke:#ececec}
42.lnd{stroke:#9a9a9a}
43.life{stroke:#6a6a6a}
44.ah{fill:#ececec}
45.ahd{fill:#9a9a9a}
46}
47</style>
48<defs>
49<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
50<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
51</defs>
52<rect class="bg" x="0" y="0" width="970" height="590"/>
53<text class="th" x="28" y="36">1. System context</text>
54<rect class="act" x="30" y="70" width="200" height="48" rx="2"/>
55<text class="tb1" x="130.0" y="90.0" text-anchor="middle">Anonymous visitor</text>
56<text class="ts" x="130.0" y="106.0" text-anchor="middle">HTTPS · git:// if enabled</text>
57<path class="ln" d="M230,94 L425,125" marker-end="url(#a)"/>
58<rect class="act" x="30" y="140" width="200" height="48" rx="2"/>
59<text class="tb1" x="130.0" y="160.0" text-anchor="middle">User: CLI or OpenSSH</text>
60<text class="ts" x="130.0" y="176.0" text-anchor="middle">SSH :22 · public key</text>
61<path class="ln" d="M230,164 L425,170" marker-end="url(#a)"/>
62<rect class="act" x="30" y="210" width="200" height="48" rx="2"/>
63<text class="tb1" x="130.0" y="230.0" text-anchor="middle">User: browser</text>
64<text class="ts" x="130.0" y="246.0" text-anchor="middle">HTTPS :443 · session cookie</text>
65<path class="ln" d="M230,234 L425,215" marker-end="url(#a)"/>
66<rect class="act" x="30" y="280" width="200" height="48" rx="2"/>
67<text class="tb1" x="130.0" y="300.0" text-anchor="middle">iOS app</text>
68<text class="ts" x="130.0" y="316.0" text-anchor="middle">HTTPS API · bearer token</text>
69<path class="ln" d="M230,304 L425,260" marker-end="url(#a)"/>
70<rect class="act" x="30" y="350" width="200" height="48" rx="2"/>
71<text class="tb1" x="130.0" y="370.0" text-anchor="middle">CI runner</text>
72<text class="ts" x="130.0" y="386.0" text-anchor="middle">SSH :22 · runner-scoped key</text>
73<path class="ln" d="M230,374 L425,305" marker-end="url(#a)"/>
74<rect class="act" x="30" y="450" width="200" height="48" rx="2"/>
75<text class="tb1" x="130.0" y="470.0" text-anchor="middle">Operator</text>
76<text class="ts" x="130.0" y="486.0" text-anchor="middle">SSH :2222 · root</text>
77<path class="ln" d="M230,474 L425,474" marker-end="url(#a)"/>
78<rect class="host" x="400" y="60" width="290" height="470" rx="2"/>
79<text class="ts" x="412" y="80" text-anchor="start">Host (Linux, systemd)</text>
80<rect class="gb" x="425" y="100" width="240" height="300" rx="2"/>
81<text class="tb1" x="545" y="132" text-anchor="middle">gitbayd</text>
82<text class="ts" x="545" y="154" text-anchor="middle">SSH · HTTPS · git hooks</text>
83<text class="ts" x="545" y="170" text-anchor="middle">command registry and policy</text>
84<text class="ts" x="545" y="186" text-anchor="middle">workers: mail, push,</text>
85<text class="ts" x="545" y="202" text-anchor="middle">webhooks, mirrors, CI</text>
86<rect class="st" x="445" y="250" width="200" height="52" rx="2"/>
87<text class="tb1" x="545.0" y="280.0" text-anchor="middle">SQLite · repositories · LFS</text>
88<rect class="ext" x="445" y="322" width="200" height="52" rx="2"/>
89<text class="tb1" x="545.0" y="352.0" text-anchor="middle">git subprocesses</text>
90<rect class="ext" x="425" y="450" width="240" height="48" rx="2"/>
91<text class="tb1" x="545.0" y="470.0" text-anchor="middle">operator sshd :2222</text>
92<text class="ts" x="545.0" y="486.0" text-anchor="middle">keys only · fail2ban</text>
93<rect class="ext" x="750" y="70" width="200" height="48" rx="2"/>
94<text class="tb1" x="850.0" y="90.0" text-anchor="middle">ACME CA</text>
95<text class="ts" x="850.0" y="106.0" text-anchor="middle">TLS certificates</text>
96<path class="ln" d="M665,120 L750,94" marker-end="url(#a)"/>
97<rect class="ext" x="750" y="135" width="200" height="48" rx="2"/>
98<text class="tb1" x="850.0" y="155.0" text-anchor="middle">SMTP relay</text>
99<text class="ts" x="850.0" y="171.0" text-anchor="middle">mail · STARTTLS if offered</text>
100<path class="ln" d="M665,160 L750,159" marker-end="url(#a)"/>
101<rect class="ext" x="750" y="200" width="200" height="48" rx="2"/>
102<text class="tb1" x="850.0" y="220.0" text-anchor="middle">Apple Push (APNs)</text>
103<text class="ts" x="850.0" y="236.0" text-anchor="middle">iOS notifications</text>
104<path class="ln" d="M665,200 L750,224" marker-end="url(#a)"/>
105<rect class="ext" x="750" y="265" width="200" height="48" rx="2"/>
106<text class="tb1" x="850.0" y="285.0" text-anchor="middle">Webhook endpoints</text>
107<text class="ts" x="850.0" y="301.0" text-anchor="middle">HMAC-signed POSTs</text>
108<path class="ln" d="M665,240 L750,289" marker-end="url(#a)"/>
109<rect class="ext" x="750" y="330" width="200" height="48" rx="2"/>
110<text class="tb1" x="850.0" y="350.0" text-anchor="middle">Mirror remotes</text>
111<text class="ts" x="850.0" y="366.0" text-anchor="middle">push and pull mirrors</text>
112<path class="ln" d="M665,280 L750,354" marker-end="url(#a)" marker-start="url(#a)"/>
113<rect class="ext" x="750" y="395" width="200" height="48" rx="2"/>
114<text class="tb1" x="850.0" y="415.0" text-anchor="middle">Package registries</text>
115<text class="ts" x="850.0" y="431.0" text-anchor="middle">dependency checks, opt-in</text>
116<path class="ln" d="M665,320 L750,419" marker-end="url(#a)"/>
117<rect class="ext" x="750" y="470" width="200" height="48" rx="2"/>
118<text class="tb1" x="850.0" y="490.0" text-anchor="middle">Offsite object storage</text>
119<text class="ts" x="850.0" y="506.0" text-anchor="middle">restic · append-only key</text>
120<path class="lnd" d="M690,494 L750,494" marker-end="url(#ad)"/>
121<rect class="gb" x="28" y="559" width="18" height="14" rx="2"/>
122<text class="ts" x="52" y="570" text-anchor="start">gitbay</text>
123<rect class="act" x="99.2" y="559" width="18" height="14" rx="2"/>
124<text class="ts" x="123.2" y="570" text-anchor="start">actor</text>
125<rect class="ext" x="164.2" y="559" width="18" height="14" rx="2"/>
126<text class="ts" x="188.2" y="570" text-anchor="start">external or host</text>
127<rect class="st" x="297.4" y="559" width="18" height="14" rx="2"/>
128<text class="ts" x="321.4" y="570" text-anchor="start">stored data</text>
129<rect class="bad" x="399.59999999999997" y="559" width="18" height="14" rx="2"/>
130<text class="ts" x="423.59999999999997" y="570" text-anchor="start">untrusted or refused</text>
131</svg>
.gitbay/wiki/Architecture/diagrams/02-components.svg added +128
@@ -0,0 +1,128 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 680" width="970" height="680" role="img" aria-labelledby="t d">
2<title id="t">2. Components inside gitbayd</title><desc id="d">Packages of the gitbayd daemon and how requests move between them.</desc>
3<style>
4text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
5.bg{fill:#ffffff}
6.t{fill:#1a1a1a;font-size:13px}
7.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
8.ts{fill:#4d4d4d;font-size:11px}
9.th{fill:#1a1a1a;font-size:17px;font-weight:700}
10.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
11.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
12.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
13.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
14.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
15.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
16.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
17.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
18.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
19.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
20.zl{fill:#c2410c;font-size:12px;font-weight:700}
21.tag{fill:#c2410c;font-size:11px;font-weight:700}
22.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
23.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
24.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
25.ah{fill:#1a1a1a}
26.ahd{fill:#6b6b6b}
27@media (prefers-color-scheme: dark){
28.bg{fill:#121212}
29.t,.tb1,.th{fill:#ececec}
30.ts{fill:#b0b0b0}
31.gb{fill:#16233f;stroke:#7aa2ff}
32.ext{fill:#1e1e1e;stroke:#8a8a8a}
33.act{fill:#121212;stroke:#ececec}
34.st{fill:#2a1a0e;stroke:#f0a36b}
35.bad{fill:#2c1414;stroke:#f28b82}
36.ok{fill:#122417;stroke:#6fcf8f}
37.dec{fill:#121212;stroke:#7aa2ff}
38.host{stroke:#8a8a8a}
39.zone{stroke:#fb923c}
40.zl,.tag{fill:#fb923c}
41.ln{stroke:#ececec}
42.lnd{stroke:#9a9a9a}
43.life{stroke:#6a6a6a}
44.ah{fill:#ececec}
45.ahd{fill:#9a9a9a}
46}
47</style>
48<defs>
49<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
50<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
51</defs>
52<rect class="bg" x="0" y="0" width="970" height="680"/>
53<text class="th" x="28" y="36">2. Components inside gitbayd</text>
54<rect class="gb" x="40" y="70" width="250" height="64" rx="2"/>
55<text class="tb1" x="165.0" y="98.0" text-anchor="middle">internal/sshd</text>
56<text class="ts" x="165.0" y="114.0" text-anchor="middle">SSH :22 · key auth · exec · git transport</text>
57<rect class="gb" x="310" y="70" width="330" height="64" rx="2"/>
58<text class="tb1" x="475.0" y="98.0" text-anchor="middle">internal/httpd</text>
59<text class="ts" x="475.0" y="114.0" text-anchor="middle">web · JSON API · smart HTTP (fetch) · LFS</text>
60<rect class="gb" x="660" y="70" width="270" height="64" rx="2"/>
61<text class="tb1" x="795.0" y="98.0" text-anchor="middle">internal/gitd</text>
62<text class="ts" x="795.0" y="114.0" text-anchor="middle">git:// · upload-pack · off by default</text>
63<rect class="gb" x="40" y="190" width="600" height="80" rx="2"/>
64<text class="tb1" x="340.0" y="218.0" text-anchor="middle">internal/control: the command registry</text>
65<text class="ts" x="340.0" y="234.0" text-anchor="middle">Dispatch: scope · read-only · disabled · admin · pending · write budget</text>
66<text class="ts" x="340.0" y="250.0" text-anchor="middle">stdin gating · handler · audit of successful writes</text>
67<rect class="gb" x="660" y="190" width="270" height="80" rx="2"/>
68<text class="tb1" x="795.0" y="218.0" text-anchor="middle">internal/policy</text>
69<text class="ts" x="795.0" y="234.0" text-anchor="middle">CanRead / CanWrite / CanAdmin</text>
70<text class="ts" x="795.0" y="250.0" text-anchor="middle">key scopes · CheckPush · CODEOWNERS</text>
71<rect class="gb" x="40" y="320" width="180" height="70" rx="2"/>
72<text class="tb1" x="130.0" y="343.0" text-anchor="middle">internal/hookd</text>
73<text class="ts" x="130.0" y="359.0" text-anchor="middle">pre- and post-receive</text>
74<text class="ts" x="130.0" y="375.0" text-anchor="middle">decisions</text>
75<rect class="gb" x="240" y="320" width="190" height="70" rx="2"/>
76<text class="tb1" x="335.0" y="343.0" text-anchor="middle">internal/gitutil</text>
77<text class="ts" x="335.0" y="359.0" text-anchor="middle">git as a subprocess</text>
78<text class="ts" x="335.0" y="375.0" text-anchor="middle">argv only, no shell</text>
79<rect class="gb" x="450" y="320" width="190" height="70" rx="2"/>
80<text class="tb1" x="545.0" y="343.0" text-anchor="middle">internal/sig</text>
81<text class="ts" x="545.0" y="359.0" text-anchor="middle">OpenPGP and SSHSIG</text>
82<text class="ts" x="545.0" y="375.0" text-anchor="middle">verification only</text>
83<rect class="gb" x="660" y="320" width="270" height="70" rx="2"/>
84<text class="tb1" x="795.0" y="343.0" text-anchor="middle">internal/store</text>
85<text class="ts" x="795.0" y="359.0" text-anchor="middle">SQLite · hand-written SQL</text>
86<text class="ts" x="795.0" y="375.0" text-anchor="middle">59 migrations</text>
87<rect class="gb" x="40" y="440" width="600" height="70" rx="2"/>
88<text class="tb1" x="340.0" y="463.0" text-anchor="middle">background workers</text>
89<text class="ts" x="340.0" y="479.0" text-anchor="middle">webhook delivery · mail · APNs · mirrors</text>
90<text class="ts" x="340.0" y="495.0" text-anchor="middle">CI scheduler and stale-build reaper · dependency checks · retention sweep</text>
91<rect class="gb" x="800" y="440" width="130" height="70" rx="2"/>
92<text class="tb1" x="865.0" y="463.0" text-anchor="middle">internal/lfs</text>
93<text class="ts" x="865.0" y="479.0" text-anchor="middle">content-addressed</text>
94<text class="ts" x="865.0" y="495.0" text-anchor="middle">HMAC tokens</text>
95<rect class="st" x="40" y="570" width="180" height="50" rx="2"/>
96<text class="tb1" x="130.0" y="591.0" text-anchor="middle">hook.sock</text>
97<text class="ts" x="130.0" y="607.0" text-anchor="middle">unix socket</text>
98<rect class="st" x="240" y="570" width="190" height="50" rx="2"/>
99<text class="tb1" x="335.0" y="591.0" text-anchor="middle">repos/*.git</text>
100<text class="ts" x="335.0" y="607.0" text-anchor="middle">bare repositories</text>
101<rect class="st" x="660" y="570" width="120" height="50" rx="2"/>
102<text class="tb1" x="720.0" y="591.0" text-anchor="middle">gitbay.db</text>
103<text class="ts" x="720.0" y="607.0" text-anchor="middle">SQLite, 0640</text>
104<rect class="st" x="800" y="570" width="130" height="50" rx="2"/>
105<text class="tb1" x="865.0" y="591.0" text-anchor="middle">lfs/</text>
106<text class="ts" x="865.0" y="607.0" text-anchor="middle">objects</text>
107<path class="ln" d="M165,134 L165,190" marker-end="url(#a)"/>
108<path class="ln" d="M475,134 L475,190" marker-end="url(#a)"/>
109<path class="ln" d="M700,134 L610,190" marker-end="url(#a)"/>
110<path class="ln" d="M640,230 L660,230" marker-end="url(#a)"/>
111<path class="ln" d="M335,270 L335,320" marker-end="url(#a)"/>
112<text class="ts" x="342" y="300" text-anchor="start">git transport</text>
113<path class="ln" d="M545,270 L545,320" marker-end="url(#a)"/>
114<path class="ln" d="M600,270 L700,320" marker-end="url(#a)"/>
115<path class="ln" d="M130,320 L130,270" marker-end="url(#a)"/>
116<text class="ts" x="137" y="300" text-anchor="start">decision request</text>
117<path class="ln" d="M560,440 L700,390" marker-end="url(#a)"/>
118<path class="ln" d="M335,390 L335,570" marker-end="url(#a)"/>
119<path class="ln" d="M240,595 L220,595" marker-end="url(#a)"/>
120<text class="ts" x="230" y="560" text-anchor="middle">hooks</text>
121<path class="ln" d="M120,570 L120,390" marker-end="url(#a)"/>
122<path class="ln" d="M720,390 L720,570" marker-end="url(#a)"/>
123<path class="ln" d="M865,510 L865,570" marker-end="url(#a)"/>
124<rect class="gb" x="28" y="649" width="18" height="14" rx="2"/>
125<text class="ts" x="52" y="660" text-anchor="start">gitbayd package</text>
126<rect class="st" x="155.0" y="649" width="18" height="14" rx="2"/>
127<text class="ts" x="179.0" y="660" text-anchor="start">on-disk state</text>
128</svg>
.gitbay/wiki/Architecture/diagrams/03-deployment.svg added +118
@@ -0,0 +1,118 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 640" width="970" height="640" role="img" aria-labelledby="t d">
2<title id="t">3. Deployment (reference host)</title><desc id="d">Processes, users, ports and files on the single gitbay host.</desc>
3<style>
4text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
5.bg{fill:#ffffff}
6.t{fill:#1a1a1a;font-size:13px}
7.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
8.ts{fill:#4d4d4d;font-size:11px}
9.th{fill:#1a1a1a;font-size:17px;font-weight:700}
10.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
11.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
12.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
13.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
14.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
15.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
16.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
17.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
18.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
19.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
20.zl{fill:#c2410c;font-size:12px;font-weight:700}
21.tag{fill:#c2410c;font-size:11px;font-weight:700}
22.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
23.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
24.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
25.ah{fill:#1a1a1a}
26.ahd{fill:#6b6b6b}
27@media (prefers-color-scheme: dark){
28.bg{fill:#121212}
29.t,.tb1,.th{fill:#ececec}
30.ts{fill:#b0b0b0}
31.gb{fill:#16233f;stroke:#7aa2ff}
32.ext{fill:#1e1e1e;stroke:#8a8a8a}
33.act{fill:#121212;stroke:#ececec}
34.st{fill:#2a1a0e;stroke:#f0a36b}
35.bad{fill:#2c1414;stroke:#f28b82}
36.ok{fill:#122417;stroke:#6fcf8f}
37.dec{fill:#121212;stroke:#7aa2ff}
38.host{stroke:#8a8a8a}
39.zone{stroke:#fb923c}
40.zl,.tag{fill:#fb923c}
41.ln{stroke:#ececec}
42.lnd{stroke:#9a9a9a}
43.life{stroke:#6a6a6a}
44.ah{fill:#ececec}
45.ahd{fill:#9a9a9a}
46}
47</style>
48<defs>
49<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
50<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
51</defs>
52<rect class="bg" x="0" y="0" width="970" height="640"/>
53<text class="th" x="28" y="36">3. Deployment (reference host)</text>
54<rect class="ext" x="20" y="80" width="180" height="470" rx="2"/>
55<text class="tb1" x="110" y="110" text-anchor="middle">Internet</text>
56<text class="ts" x="110" y="140" text-anchor="middle">clients: SSH, HTTPS</text>
57<text class="ts" x="110" y="162" text-anchor="middle">ACME CA</text>
58<text class="ts" x="110" y="184" text-anchor="middle">SMTP relay</text>
59<text class="ts" x="110" y="206" text-anchor="middle">APNs</text>
60<text class="ts" x="110" y="228" text-anchor="middle">webhook endpoints</text>
61<text class="ts" x="110" y="250" text-anchor="middle">mirror remotes</text>
62<text class="ts" x="110" y="272" text-anchor="middle">package registries</text>
63<text class="ts" x="110" y="294" text-anchor="middle">offsite object storage</text>
64<rect class="host" x="240" y="60" width="710" height="520" rx="2"/>
65<text class="ts" x="252" y="80" text-anchor="start">Host: Ubuntu 24.04 · ufw inbound 22, 80, 443, 2222 · outbound open</text>
66<rect class="gb" x="280" y="100" width="360" height="200" rx="2"/>
67<text class="tb1" x="460.0" y="164.0" text-anchor="middle">gitbayd.service (user gitbay)</text>
68<text class="ts" x="460.0" y="180.0" text-anchor="middle">:22 SSH · :443 HTTPS · :80 ACME and redirect</text>
69<text class="ts" x="460.0" y="196.0" text-anchor="middle">hook.sock (unix)</text>
70<text class="ts" x="460.0" y="212.0" text-anchor="middle">ProtectSystem=strict · NoNewPrivileges</text>
71<text class="ts" x="460.0" y="228.0" text-anchor="middle">CAP_NET_BIND_SERVICE only · SystemCallFilter</text>
72<text class="ts" x="460.0" y="244.0" text-anchor="middle">MemoryDenyWriteExecute · PrivateTmp</text>
73<rect class="st" x="680" y="100" width="250" height="200" rx="2"/>
74<text class="tb1" x="805.0" y="156.0" text-anchor="middle">/var/lib/gitbay (0750)</text>
75<text class="ts" x="805.0" y="172.0" text-anchor="middle">gitbay.db (0640)</text>
76<text class="ts" x="805.0" y="188.0" text-anchor="middle">repos/ · lfs/ · hooks/</text>
77<text class="ts" x="805.0" y="204.0" text-anchor="middle">ssh/host_ed25519 (0600)</text>
78<text class="ts" x="805.0" y="220.0" text-anchor="middle">acme/</text>
79<text class="ts" x="805.0" y="236.0" text-anchor="middle">/etc/gitbay/config.toml (0640)</text>
80<text class="ts" x="805.0" y="252.0" text-anchor="middle">/var/backups/gitbay (0750)</text>
81<rect class="gb" x="280" y="340" width="360" height="100" rx="2"/>
82<text class="tb1" x="460.0" y="378.0" text-anchor="middle">gitbay-runner.service (user ci-runner)</text>
83<text class="ts" x="460.0" y="394.0" text-anchor="middle">polls git@127.0.0.1 over SSH with a runner key</text>
84<text class="ts" x="460.0" y="410.0" text-anchor="middle">MemoryMax 6G · CPUQuota 300% · Delegate=yes</text>
85<rect class="bad" x="300" y="470" width="320" height="80" rx="2"/>
86<text class="tb1" x="460.0" y="498.0" text-anchor="middle">CI containers (rootless podman)</text>
87<text class="ts" x="460.0" y="514.0" text-anchor="middle">untrusted steps · --pull=never</text>
88<text class="ts" x="460.0" y="530.0" text-anchor="middle">build home per repository, read-write</text>
89<rect class="ext" x="680" y="340" width="250" height="100" rx="2"/>
90<text class="tb1" x="805.0" y="370.0" text-anchor="middle">timers (user gitbay)</text>
91<text class="ts" x="805.0" y="386.0" text-anchor="middle">backup nightly · database hourly</text>
92<text class="ts" x="805.0" y="402.0" text-anchor="middle">git gc weekly · monitor hourly</text>
93<text class="ts" x="805.0" y="418.0" text-anchor="middle">restic to offsite storage</text>
94<rect class="ext" x="680" y="470" width="250" height="80" rx="2"/>
95<text class="tb1" x="805.0" y="506.0" text-anchor="middle">operator sshd :2222</text>
96<text class="ts" x="805.0" y="522.0" text-anchor="middle">keys only · fail2ban</text>
97<path class="ln" d="M200,170 L280,170" marker-end="url(#a)"/>
98<text class="ts" x="240" y="163" text-anchor="middle">:22 :443 :80</text>
99<path class="ln" d="M280,260 L200,260" marker-end="url(#a)"/>
100<text class="ts" x="240" y="276" text-anchor="middle">outbound</text>
101<path class="ln" d="M640,200 L680,200" marker-end="url(#a)"/>
102<path class="ln" d="M460,340 L460,300" marker-end="url(#a)"/>
103<text class="ts" x="468" y="324" text-anchor="start">SSH</text>
104<path class="ln" d="M460,440 L460,470" marker-end="url(#a)"/>
105<path class="ln" d="M805,340 L805,300" marker-end="url(#a)"/>
106<path class="lnd" d="M300,520 L200,500" marker-end="url(#ad)"/>
107<text class="ts" x="250" y="530" text-anchor="middle">egress open</text>
108<path class="ln" d="M200,540 L255,566 L805,566 L805,550" marker-end="url(#a)"/>
109<text class="ts" x="530" y="560" text-anchor="middle">SSH :2222</text>
110<rect class="gb" x="28" y="609" width="18" height="14" rx="2"/>
111<text class="ts" x="52" y="620" text-anchor="start">gitbay unit</text>
112<rect class="st" x="130.2" y="609" width="18" height="14" rx="2"/>
113<text class="ts" x="154.2" y="620" text-anchor="start">files</text>
114<rect class="ext" x="195.2" y="609" width="18" height="14" rx="2"/>
115<text class="ts" x="219.2" y="620" text-anchor="start">host service</text>
116<rect class="bad" x="303.6" y="609" width="18" height="14" rx="2"/>
117<text class="ts" x="327.6" y="620" text-anchor="start">untrusted code</text>
118</svg>
.gitbay/wiki/Architecture/diagrams/04-trust-boundaries.svg added +145
@@ -0,0 +1,145 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 650" width="970" height="650" role="img" aria-labelledby="t d">
2<title id="t">4. Trust boundaries</title><desc id="d">Zones Z0 to Z6 and the boundaries TB1 to TB10 that data crosses between them.</desc>
3<style>
4text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
5.bg{fill:#ffffff}
6.t{fill:#1a1a1a;font-size:13px}
7.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
8.ts{fill:#4d4d4d;font-size:11px}
9.th{fill:#1a1a1a;font-size:17px;font-weight:700}
10.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
11.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
12.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
13.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
14.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
15.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
16.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
17.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
18.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
19.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
20.zl{fill:#c2410c;font-size:12px;font-weight:700}
21.tag{fill:#c2410c;font-size:11px;font-weight:700}
22.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
23.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
24.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
25.ah{fill:#1a1a1a}
26.ahd{fill:#6b6b6b}
27@media (prefers-color-scheme: dark){
28.bg{fill:#121212}
29.t,.tb1,.th{fill:#ececec}
30.ts{fill:#b0b0b0}
31.gb{fill:#16233f;stroke:#7aa2ff}
32.ext{fill:#1e1e1e;stroke:#8a8a8a}
33.act{fill:#121212;stroke:#ececec}
34.st{fill:#2a1a0e;stroke:#f0a36b}
35.bad{fill:#2c1414;stroke:#f28b82}
36.ok{fill:#122417;stroke:#6fcf8f}
37.dec{fill:#121212;stroke:#7aa2ff}
38.host{stroke:#8a8a8a}
39.zone{stroke:#fb923c}
40.zl,.tag{fill:#fb923c}
41.ln{stroke:#ececec}
42.lnd{stroke:#9a9a9a}
43.life{stroke:#6a6a6a}
44.ah{fill:#ececec}
45.ahd{fill:#9a9a9a}
46}
47</style>
48<defs>
49<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
50<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
51</defs>
52<rect class="bg" x="0" y="0" width="970" height="650"/>
53<text class="th" x="28" y="36">4. Trust boundaries</text>
54<rect class="zone" x="20" y="60" width="190" height="560" rx="2"/>
55<text class="zl" x="28" y="76" text-anchor="start">Z0 Internet (untrusted)</text>
56<rect class="zone" x="250" y="60" width="380" height="310" rx="2"/>
57<text class="zl" x="258" y="76" text-anchor="start">Z1 gitbayd</text>
58<rect class="zone" x="660" y="60" width="290" height="270" rx="2"/>
59<text class="zl" x="668" y="76" text-anchor="start">Z2 Local state</text>
60<rect class="zone" x="250" y="400" width="380" height="110" rx="2"/>
61<text class="zl" x="258" y="416" text-anchor="start">Z3 git and hooks</text>
62<rect class="zone" x="660" y="400" width="290" height="220" rx="2"/>
63<text class="zl" x="668" y="416" text-anchor="start">Z4 Runner</text>
64<rect class="zone" x="675" y="500" width="260" height="110" rx="2"/>
65<text class="zl" x="683" y="516" text-anchor="start">Z5 Containers</text>
66<rect class="zone" x="250" y="540" width="380" height="80" rx="2"/>
67<text class="zl" x="258" y="556" text-anchor="start">Z6 Operator</text>
68<rect class="act" x="35" y="90" width="160" height="50" rx="2"/>
69<text class="tb1" x="115.0" y="119.0" text-anchor="middle">Visitor</text>
70<rect class="act" x="35" y="170" width="160" height="50" rx="2"/>
71<text class="tb1" x="115.0" y="199.0" text-anchor="middle">User over SSH</text>
72<rect class="act" x="35" y="250" width="160" height="50" rx="2"/>
73<text class="tb1" x="115.0" y="279.0" text-anchor="middle">Browser</text>
74<rect class="act" x="35" y="330" width="160" height="50" rx="2"/>
75<text class="tb1" x="115.0" y="359.0" text-anchor="middle">API client, iOS</text>
76<rect class="ext" x="35" y="500" width="160" height="60" rx="2"/>
77<text class="tb1" x="115.0" y="526.0" text-anchor="middle">Webhook and</text>
78<text class="ts" x="115.0" y="542.0" text-anchor="middle">mirror endpoints</text>
79<rect class="gb" x="270" y="95" width="150" height="55" rx="2"/>
80<text class="tb1" x="345.0" y="118.5" text-anchor="middle">sshd</text>
81<text class="ts" x="345.0" y="134.5" text-anchor="middle">key auth</text>
82<rect class="gb" x="270" y="200" width="150" height="60" rx="2"/>
83<text class="tb1" x="345.0" y="226.0" text-anchor="middle">httpd</text>
84<text class="ts" x="345.0" y="242.0" text-anchor="middle">cookie · token · CSP</text>
85<rect class="gb" x="270" y="300" width="150" height="50" rx="2"/>
86<text class="tb1" x="345.0" y="329.0" text-anchor="middle">workers</text>
87<rect class="gb" x="450" y="130" width="160" height="110" rx="2"/>
88<text class="tb1" x="530.0" y="165.0" text-anchor="middle">Dispatch</text>
89<text class="ts" x="530.0" y="181.0" text-anchor="middle">handler</text>
90<text class="ts" x="530.0" y="197.0" text-anchor="middle">resolveRepo</text>
91<text class="ts" x="530.0" y="213.0" text-anchor="middle">policy</text>
92<rect class="st" x="680" y="95" width="250" height="55" rx="2"/>
93<text class="tb1" x="805.0" y="118.5" text-anchor="middle">SQLite</text>
94<text class="ts" x="805.0" y="134.5" text-anchor="middle">token hashes · secrets in clear</text>
95<rect class="st" x="680" y="170" width="250" height="55" rx="2"/>
96<text class="tb1" x="805.0" y="201.5" text-anchor="middle">repositories · LFS</text>
97<rect class="st" x="680" y="245" width="250" height="55" rx="2"/>
98<text class="tb1" x="805.0" y="276.5" text-anchor="middle">host key · ACME · config</text>
99<rect class="ext" x="270" y="430" width="150" height="60" rx="2"/>
100<text class="tb1" x="345.0" y="456.0" text-anchor="middle">git receive-pack</text>
101<text class="ts" x="345.0" y="472.0" text-anchor="middle">upload-pack</text>
102<rect class="ext" x="450" y="430" width="160" height="60" rx="2"/>
103<text class="tb1" x="530.0" y="456.0" text-anchor="middle">gitbayd hook</text>
104<text class="ts" x="530.0" y="472.0" text-anchor="middle">to hook.sock</text>
105<rect class="gb" x="680" y="430" width="250" height="50" rx="2"/>
106<text class="tb1" x="805.0" y="459.0" text-anchor="middle">gitbay-runner</text>
107<rect class="bad" x="690" y="530" width="230" height="55" rx="2"/>
108<text class="tb1" x="805.0" y="553.5" text-anchor="middle">build steps</text>
109<text class="ts" x="805.0" y="569.5" text-anchor="middle">repository and fork code</text>
110<rect class="ext" x="270" y="565" width="340" height="40" rx="2"/>
111<text class="tb1" x="440.0" y="589.0" text-anchor="middle">root shell: outside every in-app control</text>
112<path class="ln" d="M195,195 L270,122" marker-end="url(#a)"/>
113<text class="tag" x="232" y="150" text-anchor="middle">TB1</text>
114<path class="ln" d="M195,115 L270,215" marker-end="url(#a)"/>
115<path class="ln" d="M195,275 L270,232" marker-end="url(#a)" marker-start="url(#a)"/>
116<path class="ln" d="M195,355 L270,250" marker-end="url(#a)"/>
117<text class="tag" x="232" y="300" text-anchor="middle">TB2 · TB9</text>
118<path class="ln" d="M420,122 L450,160" marker-end="url(#a)"/>
119<path class="ln" d="M420,230 L450,215" marker-end="url(#a)"/>
120<text class="tag" x="435" y="190" text-anchor="middle">TB3</text>
121<path class="ln" d="M610,160 L680,122" marker-end="url(#a)"/>
122<path class="ln" d="M610,200 L680,197" marker-end="url(#a)"/>
123<path class="ln" d="M480,240 L400,430" marker-end="url(#a)"/>
124<text class="tag" x="455" y="330" text-anchor="end">TB4</text>
125<path class="ln" d="M420,460 L450,460" marker-end="url(#a)"/>
126<path class="ln" d="M560,430 L560,240" marker-end="url(#a)"/>
127<text class="tag" x="566" y="330" text-anchor="start">TB5</text>
128<path class="ln" d="M680,450 L610,240" marker-end="url(#a)" marker-start="url(#a)"/>
129<text class="tag" x="648" y="320" text-anchor="start">TB6</text>
130<path class="ln" d="M805,480 L805,530" marker-end="url(#a)"/>
131<text class="tag" x="812" y="510" text-anchor="start">TB7</text>
132<path class="ln" d="M270,325 L195,520" marker-end="url(#a)"/>
133<text class="tag" x="226" y="460" text-anchor="middle">TB8</text>
134<path class="lnd" d="M690,545 L645,525 L195,525" marker-end="url(#ad)"/>
135<text class="ts" x="420" y="520" text-anchor="middle">egress open</text>
136<text class="tag" x="620" y="596" text-anchor="end">TB10</text>
137<rect class="act" x="28" y="629" width="18" height="14" rx="2"/>
138<text class="ts" x="52" y="640" text-anchor="start">external actor</text>
139<rect class="gb" x="148.8" y="629" width="18" height="14" rx="2"/>
140<text class="ts" x="172.8" y="640" text-anchor="start">gitbay process</text>
141<rect class="st" x="269.6" y="629" width="18" height="14" rx="2"/>
142<text class="ts" x="293.6" y="640" text-anchor="start">stored data</text>
143<rect class="bad" x="371.8" y="629" width="18" height="14" rx="2"/>
144<text class="ts" x="395.8" y="640" text-anchor="start">untrusted code</text>
145</svg>
.gitbay/wiki/Architecture/diagrams/05-authorization.svg added +167
@@ -0,0 +1,167 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 860" width="970" height="860" role="img" aria-labelledby="t d">
2<title id="t">5. Authorization decision</title><desc id="d">How a request is authorised: Dispatch gates, then repository resolution with a policy predicate, and the git transport path.</desc>
3<style>
4text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
5.bg{fill:#ffffff}
6.t{fill:#1a1a1a;font-size:13px}
7.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
8.ts{fill:#4d4d4d;font-size:11px}
9.th{fill:#1a1a1a;font-size:17px;font-weight:700}
10.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
11.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
12.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
13.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
14.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
15.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
16.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
17.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
18.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
19.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
20.zl{fill:#c2410c;font-size:12px;font-weight:700}
21.tag{fill:#c2410c;font-size:11px;font-weight:700}
22.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
23.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
24.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
25.ah{fill:#1a1a1a}
26.ahd{fill:#6b6b6b}
27@media (prefers-color-scheme: dark){
28.bg{fill:#121212}
29.t,.tb1,.th{fill:#ececec}
30.ts{fill:#b0b0b0}
31.gb{fill:#16233f;stroke:#7aa2ff}
32.ext{fill:#1e1e1e;stroke:#8a8a8a}
33.act{fill:#121212;stroke:#ececec}
34.st{fill:#2a1a0e;stroke:#f0a36b}
35.bad{fill:#2c1414;stroke:#f28b82}
36.ok{fill:#122417;stroke:#6fcf8f}
37.dec{fill:#121212;stroke:#7aa2ff}
38.host{stroke:#8a8a8a}
39.zone{stroke:#fb923c}
40.zl,.tag{fill:#fb923c}
41.ln{stroke:#ececec}
42.lnd{stroke:#9a9a9a}
43.life{stroke:#6a6a6a}
44.ah{fill:#ececec}
45.ahd{fill:#9a9a9a}
46}
47</style>
48<defs>
49<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
50<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
51</defs>
52<rect class="bg" x="0" y="0" width="970" height="860"/>
53<text class="th" x="28" y="36">5. Authorization decision</text>
54<rect class="act" x="40" y="60" width="380" height="44" rx="2"/>
55<text class="tb1" x="230.0" y="78.0" text-anchor="middle">Credential</text>
56<text class="ts" x="230.0" y="94.0" text-anchor="middle">SSH key · API token · session cookie</text>
57<path class="ln" d="M230.0,104 L230.0,124" marker-end="url(#a)"/>
58<rect class="gb" x="40" y="124" width="380" height="44" rx="2"/>
59<text class="tb1" x="230.0" y="150.0" text-anchor="middle">Resolve account and scope</text>
60<path class="ln" d="M230.0,168 L230.0,188" marker-end="url(#a)"/>
61<rect class="dec" x="40" y="188" width="380" height="44" rx="2"/>
62<text class="tb1" x="230.0" y="214.0" text-anchor="middle">Scope allows this command?</text>
63<path class="ln" d="M420,210 L470,210" marker-end="url(#a)"/>
64<text class="ts" x="442" y="204" text-anchor="middle">no</text>
65<rect class="bad" x="470" y="192" width="190" height="36" rx="2"/>
66<text class="tb1" x="565.0" y="214.0" text-anchor="middle">denied (exit 4)</text>
67<path class="ln" d="M230.0,232 L230.0,252" marker-end="url(#a)"/>
68<rect class="dec" x="40" y="252" width="380" height="44" rx="2"/>
69<text class="tb1" x="230.0" y="278.0" text-anchor="middle">Account disabled?</text>
70<path class="ln" d="M420,274 L470,274" marker-end="url(#a)"/>
71<text class="ts" x="442" y="268" text-anchor="middle">yes</text>
72<rect class="bad" x="470" y="256" width="190" height="36" rx="2"/>
73<text class="tb1" x="565.0" y="278.0" text-anchor="middle">denied (exit 4)</text>
74<path class="ln" d="M230.0,296 L230.0,316" marker-end="url(#a)"/>
75<rect class="dec" x="40" y="316" width="380" height="44" rx="2"/>
76<text class="tb1" x="230.0" y="342.0" text-anchor="middle">admin command and not an admin?</text>
77<path class="ln" d="M420,338 L470,338" marker-end="url(#a)"/>
78<text class="ts" x="442" y="332" text-anchor="middle">yes</text>
79<rect class="bad" x="470" y="320" width="190" height="36" rx="2"/>
80<text class="tb1" x="565.0" y="342.0" text-anchor="middle">denied (exit 4)</text>
81<path class="ln" d="M230.0,360 L230.0,380" marker-end="url(#a)"/>
82<rect class="dec" x="40" y="380" width="380" height="44" rx="2"/>
83<text class="tb1" x="230.0" y="406.0" text-anchor="middle">Pending account, command not allowed?</text>
84<path class="ln" d="M420,402 L470,402" marker-end="url(#a)"/>
85<text class="ts" x="442" y="396" text-anchor="middle">yes</text>
86<rect class="bad" x="470" y="384" width="190" height="36" rx="2"/>
87<text class="tb1" x="565.0" y="406.0" text-anchor="middle">denied (exit 4)</text>
88<path class="ln" d="M230.0,424 L230.0,444" marker-end="url(#a)"/>
89<rect class="dec" x="40" y="444" width="380" height="44" rx="2"/>
90<text class="tb1" x="230.0" y="470.0" text-anchor="middle">Write budget exhausted?</text>
91<path class="ln" d="M420,466 L470,466" marker-end="url(#a)"/>
92<text class="ts" x="442" y="460" text-anchor="middle">yes</text>
93<rect class="bad" x="470" y="448" width="190" height="36" rx="2"/>
94<text class="tb1" x="565.0" y="470.0" text-anchor="middle">refused, try later</text>
95<path class="ln" d="M230.0,488 L230.0,508" marker-end="url(#a)"/>
96<rect class="gb" x="40" y="508" width="380" height="44" rx="2"/>
97<text class="tb1" x="230.0" y="534.0" text-anchor="middle">Handler: resolveRepo(path, predicate)</text>
98<path class="ln" d="M230.0,552 L230.0,572" marker-end="url(#a)"/>
99<rect class="dec" x="40" y="572" width="380" height="44" rx="2"/>
100<text class="tb1" x="230.0" y="598.0" text-anchor="middle">Repository exists?</text>
101<path class="ln" d="M420,594 L470,594" marker-end="url(#a)"/>
102<text class="ts" x="442" y="588" text-anchor="middle">no</text>
103<rect class="bad" x="470" y="576" width="190" height="36" rx="2"/>
104<text class="tb1" x="565.0" y="598.0" text-anchor="middle">not found (exit 3)</text>
105<path class="ln" d="M230.0,616 L230.0,636" marker-end="url(#a)"/>
106<rect class="dec" x="40" y="636" width="380" height="44" rx="2"/>
107<text class="tb1" x="230.0" y="662.0" text-anchor="middle">Predicate passes? (CanRead / CanWrite / CanAdmin)</text>
108<path class="ln" d="M230.0,680 L230.0,716" marker-end="url(#a)"/>
109<text class="ts" x="238.0" y="702" text-anchor="start">no</text>
110<rect class="dec" x="40" y="716" width="380" height="44" rx="2"/>
111<text class="tb1" x="230.0" y="742.0" text-anchor="middle">Caller can read it?</text>
112<path class="ln" d="M420,658 L470,658" marker-end="url(#a)"/>
113<text class="ts" x="442" y="652" text-anchor="middle">yes</text>
114<rect class="ok" x="470" y="638" width="190" height="40" rx="2"/>
115<text class="tb1" x="565.0" y="662.0" text-anchor="middle">run · audit if it wrote</text>
116<path class="ln" d="M420,728 L470,716" marker-end="url(#a)"/>
117<text class="ts" x="440" y="716" text-anchor="middle">no</text>
118<rect class="bad" x="470" y="698" width="190" height="34" rx="2"/>
119<text class="tb1" x="565.0" y="719.0" text-anchor="middle">not found (exit 3)</text>
120<path class="ln" d="M420,748 L470,760" marker-end="url(#a)"/>
121<text class="ts" x="440" y="768" text-anchor="middle">yes</text>
122<rect class="bad" x="470" y="744" width="190" height="34" rx="2"/>
123<text class="tb1" x="565.0" y="765.0" text-anchor="middle">permission denied (exit 4)</text>
124<text class="tb1" x="690" y="76" text-anchor="start">git transport (runGit)</text>
125<rect class="dec" x="690" y="92" width="250" height="52" rx="2"/>
126<text class="tb1" x="815.0" y="114.0" text-anchor="middle">Deploy key?</text>
127<text class="ts" x="815.0" y="130.0" text-anchor="middle">yes: only its repository and mode</text>
128<path class="ln" d="M815.0,144 L815.0,162" marker-end="url(#a)"/>
129<rect class="dec" x="690" y="162" width="250" height="52" rx="2"/>
130<text class="tb1" x="815.0" y="184.0" text-anchor="middle">CanRead?</text>
131<text class="ts" x="815.0" y="200.0" text-anchor="middle">no: not found</text>
132<path class="ln" d="M815.0,214 L815.0,232" marker-end="url(#a)"/>
133<rect class="dec" x="690" y="232" width="250" height="52" rx="2"/>
134<text class="tb1" x="815.0" y="254.0" text-anchor="middle">Key scope allows git?</text>
135<text class="ts" x="815.0" y="270.0" text-anchor="middle">runner: read only · else denied</text>
136<path class="ln" d="M815.0,284 L815.0,302" marker-end="url(#a)"/>
137<rect class="dec" x="690" y="302" width="250" height="52" rx="2"/>
138<text class="tb1" x="815.0" y="324.0" text-anchor="middle">Push: CanWrite?</text>
139<text class="ts" x="815.0" y="340.0" text-anchor="middle">no: denied</text>
140<path class="ln" d="M815.0,354 L815.0,372" marker-end="url(#a)"/>
141<rect class="dec" x="690" y="372" width="250" height="52" rx="2"/>
142<text class="tb1" x="815.0" y="394.0" text-anchor="middle">Archived, pull mirror, quota</text>
143<text class="ts" x="815.0" y="410.0" text-anchor="middle">refused</text>
144<path class="ln" d="M815.0,424 L815.0,442" marker-end="url(#a)"/>
145<rect class="gb" x="690" y="442" width="250" height="52" rx="2"/>
146<text class="tb1" x="815.0" y="464.0" text-anchor="middle">pre-receive: CheckPush</text>
147<text class="ts" x="815.0" y="480.0" text-anchor="middle">protected · require-mr · tags</text>
148<path class="ln" d="M815.0,494 L815.0,512" marker-end="url(#a)"/>
149<rect class="gb" x="690" y="512" width="250" height="52" rx="2"/>
150<text class="tb1" x="815.0" y="534.0" text-anchor="middle">require-signed</text>
151<text class="ts" x="815.0" y="550.0" text-anchor="middle">verify each incoming commit</text>
152<path class="ln" d="M815.0,564 L815.0,582" marker-end="url(#a)"/>
153<rect class="ok" x="690" y="582" width="250" height="52" rx="2"/>
154<text class="tb1" x="815.0" y="612.0" text-anchor="middle">git applies the ref updates</text>
155<text class="ts" x="690" y="680" text-anchor="start">Merges by the server skip the hooks:</text>
156<text class="ts" x="690" y="696" text-anchor="start">MergeGates decides them, and require-signed</text>
157<text class="ts" x="690" y="712" text-anchor="start">allows only fast-forward merges, so the</text>
158<text class="ts" x="690" y="728" text-anchor="start">server never writes an unsigned commit.</text>
159<rect class="dec" x="28" y="824" width="18" height="14" rx="2"/>
160<text class="ts" x="52" y="835" text-anchor="start">check</text>
161<rect class="gb" x="93.0" y="824" width="18" height="14" rx="2"/>
162<text class="ts" x="117.0" y="835" text-anchor="start">step</text>
163<rect class="bad" x="151.8" y="824" width="18" height="14" rx="2"/>
164<text class="ts" x="175.8" y="835" text-anchor="start">refusal</text>
165<rect class="ok" x="229.20000000000002" y="824" width="18" height="14" rx="2"/>
166<text class="ts" x="253.20000000000002" y="835" text-anchor="start">allowed</text>
167</svg>
.gitbay/wiki/Architecture/diagrams/06-push-flow.svg added +120
@@ -0,0 +1,120 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 780" width="970" height="780" role="img" aria-labelledby="t d">
2<title id="t">6. git push over SSH</title><desc id="d">Sequence of a push: SSH checks, pre-receive decision by the daemon, optional signature verification, post-receive side effects.</desc>
3<style>
4text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
5.bg{fill:#ffffff}
6.t{fill:#1a1a1a;font-size:13px}
7.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
8.ts{fill:#4d4d4d;font-size:11px}
9.th{fill:#1a1a1a;font-size:17px;font-weight:700}
10.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
11.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
12.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
13.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
14.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
15.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
16.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
17.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
18.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
19.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
20.zl{fill:#c2410c;font-size:12px;font-weight:700}
21.tag{fill:#c2410c;font-size:11px;font-weight:700}
22.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
23.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
24.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
25.ah{fill:#1a1a1a}
26.ahd{fill:#6b6b6b}
27@media (prefers-color-scheme: dark){
28.bg{fill:#121212}
29.t,.tb1,.th{fill:#ececec}
30.ts{fill:#b0b0b0}
31.gb{fill:#16233f;stroke:#7aa2ff}
32.ext{fill:#1e1e1e;stroke:#8a8a8a}
33.act{fill:#121212;stroke:#ececec}
34.st{fill:#2a1a0e;stroke:#f0a36b}
35.bad{fill:#2c1414;stroke:#f28b82}
36.ok{fill:#122417;stroke:#6fcf8f}
37.dec{fill:#121212;stroke:#7aa2ff}
38.host{stroke:#8a8a8a}
39.zone{stroke:#fb923c}
40.zl,.tag{fill:#fb923c}
41.ln{stroke:#ececec}
42.lnd{stroke:#9a9a9a}
43.life{stroke:#6a6a6a}
44.ah{fill:#ececec}
45.ahd{fill:#9a9a9a}
46}
47</style>
48<defs>
49<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
50<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
51</defs>
52<rect class="bg" x="0" y="0" width="970" height="780"/>
53<text class="th" x="28" y="36">6. git push over SSH</text>
54<rect class="act" x="15" y="56" width="150" height="40" rx="2"/>
55<text class="tb1" x="90.0" y="80.0" text-anchor="middle">Client</text>
56<line class="life" x1="90" y1="96" x2="90" y2="750"/>
57<rect class="gb" x="175" y="56" width="150" height="40" rx="2"/>
58<text class="tb1" x="250.0" y="80.0" text-anchor="middle">sshd · runGit</text>
59<line class="life" x1="250" y1="96" x2="250" y2="750"/>
60<rect class="ext" x="335" y="56" width="150" height="40" rx="2"/>
61<text class="tb1" x="410.0" y="80.0" text-anchor="middle">git receive-pack</text>
62<line class="life" x1="410" y1="96" x2="410" y2="750"/>
63<rect class="ext" x="495" y="56" width="150" height="40" rx="2"/>
64<text class="tb1" x="570.0" y="80.0" text-anchor="middle">gitbayd hook</text>
65<line class="life" x1="570" y1="96" x2="570" y2="750"/>
66<rect class="gb" x="655" y="56" width="150" height="40" rx="2"/>
67<text class="tb1" x="730.0" y="80.0" text-anchor="middle">hookd</text>
68<line class="life" x1="730" y1="96" x2="730" y2="750"/>
69<rect class="gb" x="810" y="56" width="150" height="40" rx="2"/>
70<text class="tb1" x="885.0" y="80.0" text-anchor="middle">policy · sig · store</text>
71<line class="life" x1="885" y1="96" x2="885" y2="750"/>
72<path class="ln" d="M90,130 L248,130" marker-end="url(#a)"/>
73<rect class="bg" x="65.69999999999999" y="111" width="208.60000000000002" height="15"/>
74<text class="ts" x="170.0" y="123" text-anchor="middle">exec git-receive-pack 'owner/repo'</text>
75<path class="ln" d="M250,168 L883,168" marker-end="url(#a)"/>
76<rect class="bg" x="436.65" y="149" width="261.70000000000005" height="15"/>
77<text class="ts" x="567.5" y="161" text-anchor="middle">resolve repository · access · scope · quota</text>
78<path class="ln" d="M250,206 L408,206" marker-end="url(#a)"/>
79<rect class="bg" x="205.05" y="187" width="249.9" height="15"/>
80<text class="ts" x="330.0" y="199" text-anchor="middle">spawn with hook socket, repo, user, scope</text>
81<path class="ln" d="M90,244 L408,244" marker-end="url(#a)"/>
82<rect class="bg" x="219.45" y="225" width="61.1" height="15"/>
83<text class="ts" x="250.0" y="237" text-anchor="middle">pack data</text>
84<path class="ln" d="M410,282 L568,282" marker-end="url(#a)"/>
85<rect class="bg" x="388.65" y="263" width="202.70000000000002" height="15"/>
86<text class="ts" x="490.0" y="275" text-anchor="middle">pre-receive: ref updates on stdin</text>
87<path class="ln" d="M570,320 L728,320" marker-end="url(#a)"/>
88<rect class="bg" x="581.1" y="301" width="137.8" height="15"/>
89<text class="ts" x="650.0" y="313" text-anchor="middle">request over hook.sock</text>
90<path class="ln" d="M730,358 L883,358" marker-end="url(#a)"/>
91<rect class="bg" x="685.5" y="339" width="244.0" height="15"/>
92<text class="ts" x="807.5" y="351" text-anchor="middle">CheckPush: protected · require-mr · tags</text>
93<path class="lnd" d="M730,396 L572,396" marker-end="url(#ad)"/>
94<rect class="bg" x="551.6" y="377" width="196.8" height="15"/>
95<text class="ts" x="650.0" y="389" text-anchor="middle">need commits (if require-signed)</text>
96<path class="lnd" d="M570,434 L728,434" marker-end="url(#ad)"/>
97<rect class="bg" x="592.9" y="415" width="114.2" height="15"/>
98<text class="ts" x="650.0" y="427" text-anchor="middle">raw commit objects</text>
99<path class="lnd" d="M730,472 L883,472" marker-end="url(#ad)"/>
100<rect class="bg" x="741.55" y="453" width="131.9" height="15"/>
101<text class="ts" x="807.5" y="465" text-anchor="middle">VerifyCommit for each</text>
102<path class="ln" d="M730,510 L572,510" marker-end="url(#a)"/>
103<rect class="bg" x="554.55" y="491" width="190.9" height="15"/>
104<text class="ts" x="650.0" y="503" text-anchor="middle">allow, or refuse with a message</text>
105<path class="ln" d="M570,548 L412,548" marker-end="url(#a)"/>
106<rect class="bg" x="453.55" y="529" width="72.9" height="15"/>
107<text class="ts" x="490.0" y="541" text-anchor="middle">exit 0 or 1</text>
108<path class="ln" d="M410,586 L568,586" marker-end="url(#a)"/>
109<rect class="bg" x="450.6" y="567" width="78.80000000000001" height="15"/>
110<text class="ts" x="490.0" y="579" text-anchor="middle">post-receive</text>
111<path class="ln" d="M570,624 L728,624" marker-end="url(#a)"/>
112<rect class="bg" x="587.0" y="605" width="126.0" height="15"/>
113<text class="ts" x="650.0" y="617" text-anchor="middle">post-receive request</text>
114<path class="ln" d="M730,662 L883,662" marker-end="url(#a)"/>
115<rect class="bg" x="656.0" y="643" width="303.0" height="15"/>
116<text class="ts" x="807.5" y="655" text-anchor="middle">events · CI queue · mirrors · MR heads · Closes #N</text>
117<path class="ln" d="M410,700 L92,700" marker-end="url(#a)"/>
118<rect class="bg" x="228.3" y="681" width="43.400000000000006" height="15"/>
119<text class="ts" x="250.0" y="693" text-anchor="middle">result</text>
120</svg>
.gitbay/wiki/Architecture/diagrams/07-ci-flow.svg added +108
@@ -0,0 +1,108 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 660" width="970" height="660" role="img" aria-labelledby="t d">
2<title id="t">7. CI build</title><desc id="d">Sequence of a CI build from push to result, including the claim and where secrets travel.</desc>
3<style>
4text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
5.bg{fill:#ffffff}
6.t{fill:#1a1a1a;font-size:13px}
7.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
8.ts{fill:#4d4d4d;font-size:11px}
9.th{fill:#1a1a1a;font-size:17px;font-weight:700}
10.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
11.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
12.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
13.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
14.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
15.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
16.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
17.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
18.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
19.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
20.zl{fill:#c2410c;font-size:12px;font-weight:700}
21.tag{fill:#c2410c;font-size:11px;font-weight:700}
22.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
23.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
24.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
25.ah{fill:#1a1a1a}
26.ahd{fill:#6b6b6b}
27@media (prefers-color-scheme: dark){
28.bg{fill:#121212}
29.t,.tb1,.th{fill:#ececec}
30.ts{fill:#b0b0b0}
31.gb{fill:#16233f;stroke:#7aa2ff}
32.ext{fill:#1e1e1e;stroke:#8a8a8a}
33.act{fill:#121212;stroke:#ececec}
34.st{fill:#2a1a0e;stroke:#f0a36b}
35.bad{fill:#2c1414;stroke:#f28b82}
36.ok{fill:#122417;stroke:#6fcf8f}
37.dec{fill:#121212;stroke:#7aa2ff}
38.host{stroke:#8a8a8a}
39.zone{stroke:#fb923c}
40.zl,.tag{fill:#fb923c}
41.ln{stroke:#ececec}
42.lnd{stroke:#9a9a9a}
43.life{stroke:#6a6a6a}
44.ah{fill:#ececec}
45.ahd{fill:#9a9a9a}
46}
47</style>
48<defs>
49<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
50<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
51</defs>
52<rect class="bg" x="0" y="0" width="970" height="660"/>
53<text class="th" x="28" y="36">7. CI build</text>
54<rect class="act" x="15" y="56" width="150" height="40" rx="2"/>
55<text class="tb1" x="90.0" y="80.0" text-anchor="middle">Pusher</text>
56<line class="life" x1="90" y1="96" x2="90" y2="630"/>
57<rect class="gb" x="205" y="56" width="150" height="40" rx="2"/>
58<text class="tb1" x="280.0" y="80.0" text-anchor="middle">gitbayd</text>
59<line class="life" x1="280" y1="96" x2="280" y2="630"/>
60<rect class="gb" x="395" y="56" width="150" height="40" rx="2"/>
61<text class="tb1" x="470.0" y="80.0" text-anchor="middle">store</text>
62<line class="life" x1="470" y1="96" x2="470" y2="630"/>
63<rect class="gb" x="585" y="56" width="150" height="40" rx="2"/>
64<text class="tb1" x="660.0" y="80.0" text-anchor="middle">gitbay-runner</text>
65<line class="life" x1="660" y1="96" x2="660" y2="630"/>
66<rect class="bad" x="785" y="56" width="150" height="40" rx="2"/>
67<text class="tb1" x="860.0" y="80.0" text-anchor="middle">container</text>
68<line class="life" x1="860" y1="96" x2="860" y2="630"/>
69<path class="ln" d="M90,130 L278,130" marker-end="url(#a)"/>
70<rect class="bg" x="124.94999999999999" y="111" width="120.10000000000001" height="15"/>
71<text class="ts" x="185.0" y="123" text-anchor="middle">push (post-receive)</text>
72<path class="ln" d="M280,168 L468,168" marker-end="url(#a)"/>
73<rect class="bg" x="196.95" y="149" width="356.1" height="15"/>
74<text class="ts" x="375.0" y="161" text-anchor="middle">queueJobs: ci/&lt;job&gt; pending, skipped, or reused (same tree)</text>
75<path class="ln" d="M660,206 L282,206" marker-end="url(#a)"/>
76<rect class="bg" x="286.04999999999995" y="187" width="367.90000000000003" height="15"/>
77<text class="ts" x="470.0" y="199" text-anchor="middle">runner next [--untrusted] · runner key, attached repositories</text>
78<path class="ln" d="M280,244 L468,244" marker-end="url(#a)"/>
79<rect class="bg" x="238.25" y="225" width="273.5" height="15"/>
80<text class="ts" x="375.0" y="237" text-anchor="middle">ClaimBuild: trusted builds unless --untrusted</text>
81<path class="ln" d="M280,282 L658,282" marker-end="url(#a)"/>
82<rect class="bg" x="336.2" y="263" width="267.6" height="15"/>
83<text class="ts" x="470.0" y="275" text-anchor="middle">claim: steps, image, secrets only if trusted</text>
84<path class="ln" d="M660,320 L282,320" marker-end="url(#a)"/>
85<rect class="bg" x="389.3" y="301" width="161.4" height="15"/>
86<text class="ts" x="470.0" y="313" text-anchor="middle">clone over SSH (read only)</text>
87<path class="ln" d="M660,358 L858,358" marker-end="url(#a)"/>
88<rect class="bg" x="593.75" y="339" width="332.5" height="15"/>
89<text class="ts" x="760.0" y="351" text-anchor="middle">podman run --pull=never · env file 0600 · build home rw</text>
90<path class="ln" d="M660,396 L858,396" marker-end="url(#a)"/>
91<rect class="bg" x="640.95" y="377" width="238.10000000000002" height="15"/>
92<text class="ts" x="760.0" y="389" text-anchor="middle">podman exec sh -c &lt;step&gt;, for each step</text>
93<path class="ln" d="M660,434 L282,434" marker-end="url(#a)"/>
94<rect class="bg" x="371.6" y="415" width="196.8" height="15"/>
95<text class="ts" x="470.0" y="427" text-anchor="middle">runner log &lt;id&gt;: streamed output</text>
96<path class="ln" d="M280,472 L468,472" marker-end="url(#a)"/>
97<rect class="bg" x="261.85" y="453" width="226.3" height="15"/>
98<text class="ts" x="375.0" y="465" text-anchor="middle">append log · a cancel ends the stream</text>
99<path class="ln" d="M660,510 L282,510" marker-end="url(#a)"/>
100<rect class="bg" x="371.6" y="491" width="196.8" height="15"/>
101<text class="ts" x="470.0" y="503" text-anchor="middle">runner done &lt;id&gt; success|failure</text>
102<path class="ln" d="M280,548 L468,548" marker-end="url(#a)"/>
103<rect class="bg" x="258.9" y="529" width="232.20000000000002" height="15"/>
104<text class="ts" x="375.0" y="541" text-anchor="middle">status ci/&lt;job&gt; · event · failure mail</text>
105<path class="ln" d="M470,578 L500,578 L500,592 L474,592" marker-end="url(#a)"/>
106<rect class="bg" x="503" y="576" width="338.40000000000003" height="15"/>
107<text class="ts" x="506" y="588" text-anchor="start">scheduler reaps: log closed &gt; 2 min, or started &gt; 90 min</text>
108</svg>
.gitbay/wiki/Architecture/diagrams/diagrams.py added +425
@@ -0,0 +1,425 @@
1#!/usr/bin/env python3
2"""Emit the architecture package's SVG diagrams.
3
4Usage: python3 .gitbay/wiki/Architecture/diagrams/diagrams.py .gitbay/wiki/Architecture/diagrams
5"""
6import sys
7from xml.sax.saxutils import escape as esc
8
9OUT = sys.argv[1]
10
11STYLE = """<style>
12text{font-family:'Atkinson Hyperlegible Next',system-ui,-apple-system,'Segoe UI',sans-serif}
13.bg{fill:#ffffff}
14.t{fill:#1a1a1a;font-size:13px}
15.tb1{fill:#1a1a1a;font-size:13px;font-weight:700}
16.ts{fill:#4d4d4d;font-size:11px}
17.th{fill:#1a1a1a;font-size:17px;font-weight:700}
18.m{font-family:'Atkinson Hyperlegible Mono',ui-monospace,Menlo,monospace}
19.gb{fill:#eaf0fd;stroke:#1f4fd1;stroke-width:1.4}
20.ext{fill:#f3f3f3;stroke:#6b6b6b;stroke-width:1.2}
21.act{fill:#ffffff;stroke:#1a1a1a;stroke-width:1.2}
22.st{fill:#fff4e8;stroke:#9a3412;stroke-width:1.2}
23.bad{fill:#fdecec;stroke:#b42318;stroke-width:1.2}
24.ok{fill:#e8f5ec;stroke:#1a7f37;stroke-width:1.2}
25.dec{fill:#ffffff;stroke:#1f4fd1;stroke-width:1.4;stroke-dasharray:5 3}
26.host{fill:none;stroke:#6b6b6b;stroke-width:1.2;stroke-dasharray:3 3}
27.zone{fill:none;stroke:#c2410c;stroke-width:1.6;stroke-dasharray:7 4}
28.zl{fill:#c2410c;font-size:12px;font-weight:700}
29.tag{fill:#c2410c;font-size:11px;font-weight:700}
30.ln{stroke:#1a1a1a;stroke-width:1.2;fill:none}
31.lnd{stroke:#6b6b6b;stroke-width:1.2;fill:none;stroke-dasharray:4 3}
32.life{stroke:#9a9a9a;stroke-width:1;stroke-dasharray:3 4}
33.ah{fill:#1a1a1a}
34.ahd{fill:#6b6b6b}
35@media (prefers-color-scheme: dark){
36.bg{fill:#121212}
37.t,.tb1,.th{fill:#ececec}
38.ts{fill:#b0b0b0}
39.gb{fill:#16233f;stroke:#7aa2ff}
40.ext{fill:#1e1e1e;stroke:#8a8a8a}
41.act{fill:#121212;stroke:#ececec}
42.st{fill:#2a1a0e;stroke:#f0a36b}
43.bad{fill:#2c1414;stroke:#f28b82}
44.ok{fill:#122417;stroke:#6fcf8f}
45.dec{fill:#121212;stroke:#7aa2ff}
46.host{stroke:#8a8a8a}
47.zone{stroke:#fb923c}
48.zl,.tag{fill:#fb923c}
49.ln{stroke:#ececec}
50.lnd{stroke:#9a9a9a}
51.life{stroke:#6a6a6a}
52.ah{fill:#ececec}
53.ahd{fill:#9a9a9a}
54}
55</style>
56<defs>
57<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ah"/></marker>
58<marker id="ad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" class="ahd"/></marker>
59</defs>"""
60
61
62class SVG:
63 def __init__(self, w, h, title, desc):
64 self.w, self.h = w, h
65 self.parts = [
66 f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" role="img" aria-labelledby="t d">',
67 f'<title id="t">{esc(title)}</title><desc id="d">{esc(desc)}</desc>',
68 STYLE,
69 f'<rect class="bg" x="0" y="0" width="{w}" height="{h}"/>',
70 f'<text class="th" x="28" y="36">{esc(title)}</text>',
71 ]
72
73 def text(self, x, y, s, cls="t", anchor="start"):
74 self.parts.append(f'<text class="{cls}" x="{x}" y="{y}" text-anchor="{anchor}">{esc(s)}</text>')
75
76 def box(self, x, y, w, h, cls, title=None, lines=(), tcls="tb1", lcls="ts"):
77 self.parts.append(f'<rect class="{cls}" x="{x}" y="{y}" width="{w}" height="{h}" rx="2"/>')
78 n = (1 if title else 0) + len(lines)
79 lh = 16
80 cy = y + h / 2 - (n - 1) * lh / 2 + 4
81 if title:
82 self.text(x + w / 2, cy, title, tcls, "middle")
83 cy += lh
84 for ln in lines:
85 self.text(x + w / 2, cy, ln, lcls, "middle")
86 cy += lh
87
88 def rect(self, x, y, w, h, cls):
89 self.parts.append(f'<rect class="{cls}" x="{x}" y="{y}" width="{w}" height="{h}" rx="2"/>')
90
91 def arrow(self, pts, cls="ln", both=False, label=None, lx=None, ly=None, lcls="ts", anchor="middle"):
92 d = "M" + " L".join(f"{x},{y}" for x, y in pts)
93 mk = "ad" if cls == "lnd" else "a"
94 start = f' marker-start="url(#{mk})"' if both else ""
95 self.parts.append(f'<path class="{cls}" d="{d}" marker-end="url(#{mk})"{start}/>')
96 if label:
97 if lx is None:
98 (x1, y1), (x2, y2) = pts[0], pts[-1]
99 lx, ly = (x1 + x2) / 2, (y1 + y2) / 2 - 5
100 self.text(lx, ly, label, lcls, anchor)
101
102 def line(self, x1, y1, x2, y2, cls):
103 self.parts.append(f'<line class="{cls}" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"/>')
104
105 def zone(self, x, y, w, h, label):
106 self.rect(x, y, w, h, "zone")
107 self.text(x + 8, y + 16, label, "zl")
108
109 def legend(self, y, items):
110 x = 28
111 for cls, label in items:
112 self.parts.append(f'<rect class="{cls}" x="{x}" y="{y - 11}" width="18" height="14" rx="2"/>')
113 self.text(x + 24, y, label, "ts")
114 x += 34 + 6.2 * len(label)
115
116 def save(self, name):
117 self.parts.append("</svg>")
118 with open(f"{OUT}/{name}", "w") as f:
119 f.write("\n".join(self.parts) + "\n")
120
121
122LEGEND = [("gb", "gitbay"), ("act", "actor"), ("ext", "external or host"), ("st", "stored data"), ("bad", "untrusted or refused")]
123
124
125def context():
126 s = SVG(970, 590, "1. System context", "Actors and external systems around a gitbay instance.")
127 actors = [
128 ("Anonymous visitor", "HTTPS · git:// if enabled"),
129 ("User: CLI or OpenSSH", "SSH :22 · public key"),
130 ("User: browser", "HTTPS :443 · session cookie"),
131 ("iOS app", "HTTPS API · bearer token"),
132 ("CI runner", "SSH :22 · runner-scoped key"),
133 ]
134 tops = [70, 140, 210, 280, 350]
135 targets = [125, 170, 215, 260, 305]
136 for (t, sub), y, ty in zip(actors, tops, targets):
137 s.box(30, y, 200, 48, "act", t, [sub])
138 s.arrow([(230, y + 24), (425, ty)])
139 s.box(30, 450, 200, 48, "act", "Operator", ["SSH :2222 · root"])
140 s.arrow([(230, 474), (425, 474)])
141
142 s.rect(400, 60, 290, 470, "host")
143 s.text(412, 80, "Host (Linux, systemd)", "ts")
144 s.rect(425, 100, 240, 300, "gb")
145 s.text(545, 132, "gitbayd", "tb1", "middle")
146 for i, ln in enumerate(["SSH · HTTPS · git hooks", "command registry and policy", "workers: mail, push,", "webhooks, mirrors, CI"]):
147 s.text(545, 154 + i * 16, ln, "ts", "middle")
148 s.box(445, 250, 200, 52, "st", "SQLite · repositories · LFS")
149 s.box(445, 322, 200, 52, "ext", "git subprocesses")
150 s.box(425, 450, 240, 48, "ext", "operator sshd :2222", ["keys only · fail2ban"])
151
152 ext = [
153 ("ACME CA", "TLS certificates"),
154 ("SMTP relay", "mail · STARTTLS if offered"),
155 ("Apple Push (APNs)", "iOS notifications"),
156 ("Webhook endpoints", "HMAC-signed POSTs"),
157 ("Mirror remotes", "push and pull mirrors"),
158 ("Package registries", "dependency checks, opt-in"),
159 ]
160 etops = [70, 135, 200, 265, 330, 395]
161 sources = [120, 160, 200, 240, 280, 320]
162 for (t, sub), y, sy in zip(ext, etops, sources):
163 s.box(750, y, 200, 48, "ext", t, [sub])
164 s.arrow([(665, sy), (750, y + 24)], both=(t == "Mirror remotes"))
165 s.box(750, 470, 200, 48, "ext", "Offsite object storage", ["restic · append-only key"])
166 s.arrow([(690, 494), (750, 494)], cls="lnd")
167 s.legend(570, LEGEND)
168 s.save("01-context.svg")
169
170
171def components():
172 s = SVG(970, 680, "2. Components inside gitbayd", "Packages of the gitbayd daemon and how requests move between them.")
173 s.box(40, 70, 250, 64, "gb", "internal/sshd", ["SSH :22 · key auth · exec · git transport"])
174 s.box(310, 70, 330, 64, "gb", "internal/httpd", ["web · JSON API · smart HTTP (fetch) · LFS"])
175 s.box(660, 70, 270, 64, "gb", "internal/gitd", ["git:// · upload-pack · off by default"])
176 s.box(40, 190, 600, 80, "gb", "internal/control: the command registry",
177 ["Dispatch: scope · read-only · disabled · admin · pending · write budget", "stdin gating · handler · audit of successful writes"])
178 s.box(660, 190, 270, 80, "gb", "internal/policy", ["CanRead / CanWrite / CanAdmin", "key scopes · CheckPush · CODEOWNERS"])
179 s.box(40, 320, 180, 70, "gb", "internal/hookd", ["pre- and post-receive", "decisions"])
180 s.box(240, 320, 190, 70, "gb", "internal/gitutil", ["git as a subprocess", "argv only, no shell"])
181 s.box(450, 320, 190, 70, "gb", "internal/sig", ["OpenPGP and SSHSIG", "verification only"])
182 s.box(660, 320, 270, 70, "gb", "internal/store", ["SQLite · hand-written SQL", "59 migrations"])
183 s.box(40, 440, 600, 70, "gb", "background workers",
184 ["webhook delivery · mail · APNs · mirrors", "CI scheduler and stale-build reaper · dependency checks · retention sweep"])
185 s.box(800, 440, 130, 70, "gb", "internal/lfs", ["content-addressed", "HMAC tokens"])
186 s.box(40, 570, 180, 50, "st", "hook.sock", ["unix socket"])
187 s.box(240, 570, 190, 50, "st", "repos/*.git", ["bare repositories"])
188 s.box(660, 570, 120, 50, "st", "gitbay.db", ["SQLite, 0640"])
189 s.box(800, 570, 130, 50, "st", "lfs/", ["objects"])
190
191 s.arrow([(165, 134), (165, 190)])
192 s.arrow([(475, 134), (475, 190)])
193 s.arrow([(700, 134), (610, 190)])
194 s.arrow([(640, 230), (660, 230)])
195 s.arrow([(335, 270), (335, 320)], label="git transport", lx=342, ly=300, anchor="start")
196 s.arrow([(545, 270), (545, 320)])
197 s.arrow([(600, 270), (700, 320)])
198 s.arrow([(130, 320), (130, 270)], label="decision request", lx=137, ly=300, anchor="start")
199 s.arrow([(560, 440), (700, 390)])
200 s.arrow([(335, 390), (335, 570)])
201 s.arrow([(240, 595), (220, 595)])
202 s.text(230, 560, "hooks", "ts", "middle")
203 s.arrow([(120, 570), (120, 390)])
204 s.arrow([(720, 390), (720, 570)])
205 s.arrow([(865, 510), (865, 570)])
206 s.legend(660, [("gb", "gitbayd package"), ("st", "on-disk state")])
207 s.save("02-components.svg")
208
209
210def deployment():
211 s = SVG(970, 640, "3. Deployment (reference host)", "Processes, users, ports and files on the single gitbay host.")
212 s.rect(20, 80, 180, 470, "ext")
213 s.text(110, 110, "Internet", "tb1", "middle")
214 for i, ln in enumerate(["clients: SSH, HTTPS", "ACME CA", "SMTP relay", "APNs", "webhook endpoints", "mirror remotes", "package registries", "offsite object storage"]):
215 s.text(110, 140 + i * 22, ln, "ts", "middle")
216
217 s.rect(240, 60, 710, 520, "host")
218 s.text(252, 80, "Host: Ubuntu 24.04 · ufw inbound 22, 80, 443, 2222 · outbound open", "ts")
219 s.box(280, 100, 360, 200, "gb", "gitbayd.service (user gitbay)",
220 [":22 SSH · :443 HTTPS · :80 ACME and redirect", "hook.sock (unix)", "ProtectSystem=strict · NoNewPrivileges", "CAP_NET_BIND_SERVICE only · SystemCallFilter", "MemoryDenyWriteExecute · PrivateTmp"])
221 s.box(680, 100, 250, 200, "st", "/var/lib/gitbay (0750)",
222 ["gitbay.db (0640)", "repos/ · lfs/ · hooks/", "ssh/host_ed25519 (0600)", "acme/", "/etc/gitbay/config.toml (0640)", "/var/backups/gitbay (0750)"])
223 s.box(280, 340, 360, 100, "gb", "gitbay-runner.service (user ci-runner)",
224 ["polls git@127.0.0.1 over SSH with a runner key", "MemoryMax 6G · CPUQuota 300% · Delegate=yes"])
225 s.box(300, 470, 320, 80, "bad", "CI containers (rootless podman)",
226 ["untrusted steps · --pull=never", "build home per repository, read-write"])
227 s.box(680, 340, 250, 100, "ext", "timers (user gitbay)",
228 ["backup nightly · database hourly", "git gc weekly · monitor hourly", "restic to offsite storage"])
229 s.box(680, 470, 250, 80, "ext", "operator sshd :2222", ["keys only · fail2ban"])
230
231 s.arrow([(200, 170), (280, 170)], label=":22 :443 :80", lx=240, ly=163)
232 s.arrow([(280, 260), (200, 260)], label="outbound", lx=240, ly=276)
233 s.arrow([(640, 200), (680, 200)])
234 s.arrow([(460, 340), (460, 300)], label="SSH", lx=468, ly=324, anchor="start")
235 s.arrow([(460, 440), (460, 470)])
236 s.arrow([(805, 340), (805, 300)])
237 s.arrow([(300, 520), (200, 500)], cls="lnd", label="egress open", lx=250, ly=530)
238 s.arrow([(200, 540), (255, 566), (805, 566), (805, 550)], label="SSH :2222", lx=530, ly=560)
239 s.legend(620, [("gb", "gitbay unit"), ("st", "files"), ("ext", "host service"), ("bad", "untrusted code")])
240 s.save("03-deployment.svg")
241
242
243def trust():
244 s = SVG(970, 650, "4. Trust boundaries", "Zones Z0 to Z6 and the boundaries TB1 to TB10 that data crosses between them.")
245 s.zone(20, 60, 190, 560, "Z0 Internet (untrusted)")
246 s.zone(250, 60, 380, 310, "Z1 gitbayd")
247 s.zone(660, 60, 290, 270, "Z2 Local state")
248 s.zone(250, 400, 380, 110, "Z3 git and hooks")
249 s.zone(660, 400, 290, 220, "Z4 Runner")
250 s.zone(675, 500, 260, 110, "Z5 Containers")
251 s.zone(250, 540, 380, 80, "Z6 Operator")
252
253 ents = [("Visitor", 90), ("User over SSH", 170), ("Browser", 250), ("API client, iOS", 330)]
254 for name, y in ents:
255 s.box(35, y, 160, 50, "act", name)
256 s.box(35, 500, 160, 60, "ext", "Webhook and", ["mirror endpoints"])
257
258 s.box(270, 95, 150, 55, "gb", "sshd", ["key auth"])
259 s.box(270, 200, 150, 60, "gb", "httpd", ["cookie · token · CSP"])
260 s.box(270, 300, 150, 50, "gb", "workers")
261 s.box(450, 130, 160, 110, "gb", "Dispatch", ["handler", "resolveRepo", "policy"])
262
263 s.box(680, 95, 250, 55, "st", "SQLite", ["token hashes · secrets in clear"])
264 s.box(680, 170, 250, 55, "st", "repositories · LFS")
265 s.box(680, 245, 250, 55, "st", "host key · ACME · config")
266
267 s.box(270, 430, 150, 60, "ext", "git receive-pack", ["upload-pack"])
268 s.box(450, 430, 160, 60, "ext", "gitbayd hook", ["to hook.sock"])
269 s.box(680, 430, 250, 50, "gb", "gitbay-runner")
270 s.box(690, 530, 230, 55, "bad", "build steps", ["repository and fork code"])
271 s.box(270, 565, 340, 40, "ext", "root shell: outside every in-app control")
272
273 s.arrow([(195, 195), (270, 122)]); s.text(232, 150, "TB1", "tag", "middle")
274 s.arrow([(195, 115), (270, 215)]); s.arrow([(195, 275), (270, 232)], both=True); s.arrow([(195, 355), (270, 250)])
275 s.text(232, 300, "TB2 · TB9", "tag", "middle")
276 s.arrow([(420, 122), (450, 160)]); s.arrow([(420, 230), (450, 215)]); s.text(435, 190, "TB3", "tag", "middle")
277 s.arrow([(610, 160), (680, 122)]); s.arrow([(610, 200), (680, 197)])
278 s.arrow([(480, 240), (400, 430)]); s.text(455, 330, "TB4", "tag", "end")
279 s.arrow([(420, 460), (450, 460)])
280 s.arrow([(560, 430), (560, 240)]); s.text(566, 330, "TB5", "tag")
281 s.arrow([(680, 450), (610, 240)], both=True); s.text(648, 320, "TB6", "tag")
282 s.arrow([(805, 480), (805, 530)]); s.text(812, 510, "TB7", "tag")
283 s.arrow([(270, 325), (195, 520)]); s.text(226, 460, "TB8", "tag", "middle")
284 s.arrow([(690, 545), (645, 525), (195, 525)], cls="lnd", label="egress open", lx=420, ly=520)
285 s.text(620, 596, "TB10", "tag", "end")
286 s.legend(640, [("act", "external actor"), ("gb", "gitbay process"), ("st", "stored data"), ("bad", "untrusted code")])
287 s.save("04-trust-boundaries.svg")
288
289
290def authz():
291 s = SVG(970, 860, "5. Authorization decision", "How a request is authorised: Dispatch gates, then repository resolution with a policy predicate, and the git transport path.")
292 x, w = 40, 380
293 dx, dw = 470, 190
294 s.box(x, 60, w, 44, "act", "Credential", ["SSH key · API token · session cookie"])
295 steps = [
296 (124, "gb", "Resolve account and scope", None),
297 (188, "dec", "Scope allows this command?", ("no", "denied (exit 4)")),
298 (252, "dec", "Account disabled?", ("yes", "denied (exit 4)")),
299 (316, "dec", "admin command and not an admin?", ("yes", "denied (exit 4)")),
300 (380, "dec", "Pending account, command not allowed?", ("yes", "denied (exit 4)")),
301 (444, "dec", "Write budget exhausted?", ("yes", "refused, try later")),
302 (508, "gb", "Handler: resolveRepo(path, predicate)", None),
303 (572, "dec", "Repository exists?", ("no", "not found (exit 3)")),
304 (636, "dec", "Predicate passes? (CanRead / CanWrite / CanAdmin)", None),
305 (716, "dec", "Caller can read it?", None),
306 ]
307 prev = 104
308 for y, cls, title, deny in steps:
309 s.arrow([(x + w / 2, prev), (x + w / 2, y)], label=("no" if y == 716 else None), lx=x + w / 2 + 8, ly=y - 14, anchor="start")
310 s.box(x, y, w, 44, cls, title)
311 if deny:
312 s.arrow([(x + w, y + 22), (dx, y + 22)], label=deny[0], lx=x + w + 22, ly=y + 16)
313 s.box(dx, y + 4, dw, 36, "bad", deny[1])
314 prev = y + 44
315 s.arrow([(x + w, 658), (dx, 658)], label="yes", lx=x + w + 22, ly=652)
316 s.box(dx, 638, dw, 40, "ok", "run · audit if it wrote")
317 s.arrow([(x + w, 728), (dx, 716)], label="no", lx=x + w + 20, ly=716)
318 s.box(dx, 698, dw, 34, "bad", "not found (exit 3)")
319 s.arrow([(x + w, 748), (dx, 760)], label="yes", lx=x + w + 20, ly=768)
320 s.box(dx, 744, dw, 34, "bad", "permission denied (exit 4)")
321
322 gx, gw = 690, 250
323 s.text(gx, 76, "git transport (runGit)", "tb1")
324 gsteps = [
325 ("dec", "Deploy key?", "yes: only its repository and mode"),
326 ("dec", "CanRead?", "no: not found"),
327 ("dec", "Key scope allows git?", "runner: read only · else denied"),
328 ("dec", "Push: CanWrite?", "no: denied"),
329 ("dec", "Archived, pull mirror, quota", "refused"),
330 ("gb", "pre-receive: CheckPush", "protected · require-mr · tags"),
331 ("gb", "require-signed", "verify each incoming commit"),
332 ("ok", "git applies the ref updates", None),
333 ]
334 y = 92
335 for i, (cls, title, note) in enumerate(gsteps):
336 if i:
337 s.arrow([(gx + gw / 2, y - 18), (gx + gw / 2, y)])
338 s.box(gx, y, gw, 52, cls, title, [note] if note else [])
339 y += 70
340 s.text(gx, 680, "Merges by the server skip the hooks:", "ts")
341 s.text(gx, 696, "MergeGates decides them, and require-signed", "ts")
342 s.text(gx, 712, "allows only fast-forward merges, so the", "ts")
343 s.text(gx, 728, "server never writes an unsigned commit.", "ts")
344 s.legend(835, [("dec", "check"), ("gb", "step"), ("bad", "refusal"), ("ok", "allowed")])
345 s.save("05-authorization.svg")
346
347
348def sequence(name, title, desc, parts, msgs, h):
349 s = SVG(970, h, title, desc)
350 xs = [p[0] for p in parts]
351 for x, label, cls in parts:
352 s.box(x - 75, 56, 150, 40, cls, label)
353 s.line(x, 96, x, h - 30, "life")
354 y = 130
355 for m in msgs:
356 a, b, text = m[0], m[1], m[2]
357 style = m[3] if len(m) > 3 else "ln"
358 if a == b:
359 x = xs[a]
360 s.arrow([(x, y - 8), (x + 30, y - 8), (x + 30, y + 6), (x + 4, y + 6)], cls=style)
361 tw = 5.9 * len(text) + 8
362 s.parts.append(f'<rect class="bg" x="{x + 33}" y="{y - 10}" width="{tw}" height="15"/>')
363 s.text(x + 36, y + 2, text, "ts")
364 else:
365 x1, x2 = xs[a], xs[b]
366 s.arrow([(x1, y), (x2 - 2 if x2 > x1 else x2 + 2, y)], cls=style)
367 tw = 5.9 * len(text) + 8
368 cx = (x1 + x2) / 2
369 s.parts.append(f'<rect class="bg" x="{cx - tw / 2}" y="{y - 19}" width="{tw}" height="15"/>')
370 s.text(cx, y - 7, text, "ts", "middle")
371 y += 38
372 s.save(name)
373
374
375def push():
376 parts = [(90, "Client", "act"), (250, "sshd · runGit", "gb"), (410, "git receive-pack", "ext"),
377 (570, "gitbayd hook", "ext"), (730, "hookd", "gb"), (885, "policy · sig · store", "gb")]
378 msgs = [
379 (0, 1, "exec git-receive-pack 'owner/repo'"),
380 (1, 5, "resolve repository · access · scope · quota"),
381 (1, 2, "spawn with hook socket, repo, user, scope"),
382 (0, 2, "pack data"),
383 (2, 3, "pre-receive: ref updates on stdin"),
384 (3, 4, "request over hook.sock"),
385 (4, 5, "CheckPush: protected · require-mr · tags"),
386 (4, 3, "need commits (if require-signed)", "lnd"),
387 (3, 4, "raw commit objects", "lnd"),
388 (4, 5, "VerifyCommit for each", "lnd"),
389 (4, 3, "allow, or refuse with a message"),
390 (3, 2, "exit 0 or 1"),
391 (2, 3, "post-receive"),
392 (3, 4, "post-receive request"),
393 (4, 5, "events · CI queue · mirrors · MR heads · Closes #N"),
394 (2, 0, "result"),
395 ]
396 sequence("06-push-flow.svg", "6. git push over SSH", "Sequence of a push: SSH checks, pre-receive decision by the daemon, optional signature verification, post-receive side effects.", parts, msgs, 780)
397
398
399def ci():
400 parts = [(90, "Pusher", "act"), (280, "gitbayd", "gb"), (470, "store", "gb"), (660, "gitbay-runner", "gb"), (860, "container", "bad")]
401 msgs = [
402 (0, 1, "push (post-receive)"),
403 (1, 2, "queueJobs: ci/<job> pending, skipped, or reused (same tree)"),
404 (3, 1, "runner next [--untrusted] · runner key, attached repositories"),
405 (1, 2, "ClaimBuild: trusted builds unless --untrusted"),
406 (1, 3, "claim: steps, image, secrets only if trusted"),
407 (3, 1, "clone over SSH (read only)"),
408 (3, 4, "podman run --pull=never · env file 0600 · build home rw"),
409 (3, 4, "podman exec sh -c <step>, for each step"),
410 (3, 1, "runner log <id>: streamed output"),
411 (1, 2, "append log · a cancel ends the stream"),
412 (3, 1, "runner done <id> success|failure"),
413 (1, 2, "status ci/<job> · event · failure mail"),
414 (2, 2, "scheduler reaps: log closed > 2 min, or started > 90 min"),
415 ]
416 sequence("07-ci-flow.svg", "7. CI build", "Sequence of a CI build from push to result, including the claim and where secrets travel.", parts, msgs, 660)
417
418
419context()
420components()
421deployment()
422trust()
423authz()
424push()
425ci()
.gitbay/wiki/Home.org +1
@@ -11,6 +11,7 @@ CLI-first git forge: SSH is the API, the web is a rendering.
11- [[Admin][Admin guide]] — install, configuration reference, backup, security 11- [[Admin][Admin guide]] — install, configuration reference, backup, security
12- [[API][API and webhooks]] — the JSON API contract, tokens, payloads 12- [[API][API and webhooks]] — the JSON API contract, tokens, payloads
13- [[Threat-Model][Threat model]] — what the forge trusts and never does 13- [[Threat-Model][Threat model]] — what the forge trusts and never does
14- [[file:Architecture/00-Overview.org][Architecture and security]] — diagrams, trust boundaries, controls, known gaps
14- [[Parity][Parity]] — what each surface can do, and what has no page yet 15- [[Parity][Parity]] — what each surface can do, and what has no page yet
15- [[Performance][Performance]] — stress-test numbers from importing git.git 16- [[Performance][Performance]] — stress-test numbers from importing git.git
16 17
.gitbay/wiki/Threat-Model.org +2 −1
@@ -2,7 +2,8 @@
2 2
3What the forge trusts, what it refuses to do, and where the boundaries 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 4are. This is the reference for security review; it complements the audit
5log and hardening notes in [[Admin]]. 5log and hardening notes in [[Admin]]. Diagrams, data flows, a controls
6matrix and the open gaps are in the [[file:Architecture/00-Overview.org][Architecture]] pages.
6 7
7* What gitbay never does 8* What gitbay never does
8 9