wiki: architecture and security pages !474
21 files changed, +2380 −1
Layout: unified · split
.gitbay/wiki/Architecture/00-Overview.org added +85
| @@ -0,0 +1,85 @@ | ||
| 1 | #+title: Architecture and security | |
| 2 | ||
| 3 | Architecture, trust boundaries and security controls of gitbay, written | |
| 4 | for a security reviewer or auditor. Every statement about behaviour | |
| 5 | names the file, and usually the function, that implements it; | |
| 6 | statements that rest on documentation or deployment files say so. The | |
| 7 | pages track the default branch and change in the same merge request as | |
| 8 | the 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 | ||
| 19 | Out of scope: the host operating system beyond the unit files and | |
| 20 | bootstrap in =deploy/=, the object store holding offsite backups, and | |
| 21 | Apple'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 | ||
| 33 | The command count comes from =gitbay help --json= on the live instance; | |
| 34 | the =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 | ||
| 52 | Reading order for a first pass: 1, 4, 5, 9, 10. The others are | |
| 53 | reference. | |
| 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 | ||
| 70 | The instance answers the same questions the documents make claims | |
| 71 | about: | |
| 72 | ||
| 73 | #+begin_src sh | |
| 74 | gitbay help --json # the command registry: paths, flags, ReadOnly | |
| 75 | curl -s https://gitbay.org/healthz # the deployed commit | |
| 76 | curl -sI https://gitbay.org/ # security headers | |
| 77 | ssh 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 | ||
| 7 | A self-hosted git forge: repositories, issues, merge requests, reviews, | |
| 8 | CI, releases, wikis, snippets and notifications. One Go binary | |
| 9 | (=gitbayd=), one SQLite database, and the system =git= binary for all | |
| 10 | repository operations. | |
| 11 | ||
| 12 | The design rule that shapes everything else: *SSH is the API*. Every | |
| 13 | operation is a control command in one registry | |
| 14 | (=internal/control/control.go=). Stock OpenSSH reaches all of them; the | |
| 15 | CLI, the web UI and the JSON API are clients of the same registry and | |
| 16 | do 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 | ||
| 43 | gitbayd makes no other outbound connection: no telemetry or update | |
| 44 | check. | |
| 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 | ||
| 58 | gitbay.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 | |
| 15 | the 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 | ||
| 44 | Every 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 | ||
| 54 | Every 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 | |
| 65 | gate; the read-only gate; the disabled-account gate; the =admin= noun | |
| 66 | gate; the pending-account gate; the per-account write budget; stdin | |
| 67 | gating; the handler; and an audit row for every successful mutating | |
| 68 | command. Details in [[file:05-Identity-and-Access.org][5. Identity and access]]. | |
| 69 | ||
| 70 | * Background workers | |
| 71 | ||
| 72 | Started 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 | ||
| 87 | Repositories carry generated hook scripts (mode 0755, regenerated at | |
| 88 | startup, =internal/hookd/hookd.go=) that run | |
| 89 | =gitbayd hook pre-receive|post-receive=. The hook process connects to | |
| 90 | the daemon's Unix socket (=<root>/hook.sock=, =hookd.go=) and asks | |
| 91 | for 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 | ||
| 5 | The 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 | |
| 8 | statements 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 | ||
| 21 | With =ssh.mode = system= the host's sshd serves port 22 instead and | |
| 22 | invokes =gitbayd authorized-keys= and =gitbayd shell= | |
| 23 | (=cmd/gitbayd/main.go=). | |
| 24 | ||
| 25 | HTTP server limits: =ReadHeaderTimeout= 10 s, =IdleTimeout= 2 min, | |
| 26 | =MaxHeaderBytes= 64 KiB, no =WriteTimeout= so long git transfers and | |
| 27 | live build logs can stream (=cmd/gitbayd/main.go=). | |
| 28 | ||
| 29 | There is no metrics endpoint. =/healthz= reports the deployed commit and | |
| 30 | a 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. | |
| 69 | Outbound traffic is not restricted, including from CI containers. See | |
| 70 | [[file:10-Known-Gaps.org][10. Known gaps]]. | |
| 71 | ||
| 72 | * Change path | |
| 73 | ||
| 74 | 1. A signed commit merged to =main= through a merge request (direct | |
| 75 | pushes to =main= are refused by =require-mr=). | |
| 76 | 2. =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=). | |
| 80 | 3. =/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 | ||
| 34 | Each flow lists its hops in order. Boundary IDs refer to the table | |
| 35 | above. | |
| 36 | ||
| 37 | ** A. SSH control command | |
| 38 | ||
| 39 | 1. 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. | |
| 42 | 2. Each exec request: =runExec= reloads the account, touches the key's | |
| 43 | last-used time, calls =Exec= (=sshd.go=). | |
| 44 | 3. =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=). | |
| 47 | 4. =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 | ||
| 54 | 1. =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. | |
| 57 | 2. =git receive-pack= runs with the hook socket path, repository id, | |
| 58 | user id and key scope in its environment (=sshd.go=). TB4. | |
| 59 | 3. 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. | |
| 63 | 4. 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=). | |
| 68 | 5. On refusal the hook exits 1 and git rejects the push atomically. | |
| 69 | 6. 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 | ||
| 74 | The server's own merge of a merge request does not pass through the | |
| 75 | hooks: =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 | |
| 78 | merges 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 | |
| 84 | only; a private repository answers 404. =git-receive-pack= over HTTP | |
| 85 | always answers a pkt-line refusal, so there is no password prompt and | |
| 86 | no HTTP write path (=internal/httpd/smart.go=, | |
| 87 | =routes.go=). TB2. | |
| 88 | ||
| 89 | ** D. Web read and write | |
| 90 | ||
| 91 | 1. The session cookie is hashed and looked up (=accounts.go=). | |
| 92 | 2. Pages dispatch read commands into the registry with =Source=web= and | |
| 93 | decode the JSON result into the template (=internal/httpd/control.go=). | |
| 94 | 3. 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 | ||
| 101 | 1. =apiAuth= hashes the bearer token and looks it up; any failure is a | |
| 102 | uniform 401 (=internal/httpd/api.go=). | |
| 103 | 2. Per-account rate limit, with writes at a tenth of the read budget. | |
| 104 | 3. =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=). | |
| 107 | 4. 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 | |
| 112 | git transport and returns a one-hour HMAC token scoped to repository | |
| 113 | and operation (=internal/sshd/lfs.go=, =internal/lfs/lfs.go=). The | |
| 114 | HTTP batch, upload and download endpoints verify that token; public | |
| 115 | repositories allow anonymous download. Objects are verified against | |
| 116 | their SHA-256 id on upload. | |
| 117 | ||
| 118 | ** G. Browser login by emailed link | |
| 119 | ||
| 120 | 1. =/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=). | |
| 123 | 2. A 32-byte token is mailed; only its hash is stored, valid 15 minutes. | |
| 124 | 3. =/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, | |
| 128 | to an already-authenticated key (=internal/control/web.go=). | |
| 129 | ||
| 130 | ** H. CI build | |
| 131 | ||
| 132 | See [[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 | ||
| 27 | Generation 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 | |
| 31 | constrained by a database =CHECK= as well as the command | |
| 32 | (=internal/store/migrations/0004_api_tokens.up.sql=). | |
| 33 | ||
| 34 | What 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 | ||
| 45 | Sources: =internal/control/control.go=, | |
| 46 | =internal/policy/access.go=. | |
| 47 | ||
| 48 | * Authorization decision | |
| 49 | ||
| 50 | Two layers decide every request. | |
| 51 | ||
| 52 | 1. *=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. | |
| 55 | 2. *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 | ||
| 65 | Grants come from =repo_access= rows for users, organizations and teams. | |
| 66 | Organization-owned repositories have no individual owner; members get | |
| 67 | access only through grants. | |
| 68 | ||
| 69 | Not found versus denied: when the predicate fails, =resolveRepo= | |
| 70 | checks =CanRead=. If the caller cannot read the repository the answer is | |
| 71 | "not found", identical to a missing repository. Only a caller who can | |
| 72 | read it gets "permission denied" for a write. git transport follows the | |
| 73 | same rule (=internal/sshd/sshd.go=), and so does smart HTTP, which | |
| 74 | serves public repositories only. | |
| 75 | ||
| 76 | Deploy keys bypass the grant model entirely: =runGit= checks only that | |
| 77 | the key's scope names this repository and allows the operation | |
| 78 | (=policy.DeployScopeAllows=, =internal/policy/access.go=). | |
| 79 | ||
| 80 | * Repository write protections | |
| 81 | ||
| 82 | Enforced in the pre-receive hook, so they apply to every credential | |
| 83 | that 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 | ||
| 94 | Merge gates, evaluated by =MergeGates= for =mr merge=, the web merge | |
| 95 | button 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 | ||
| 5 | Schema: =internal/store/migrations/=, 59 migrations. Classification: | |
| 6 | *C* credential or secret, *P* personal data, *R* private repository | |
| 7 | content (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 | ||
| 24 | Outside 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 | ||
| 36 | No table stores client IP addresses as a column. The daemon writes a | |
| 37 | client IP into an audit row only for authentication failures and | |
| 38 | throttling (=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 | ||
| 50 | The code base contains no symmetric encryption. A database or backup | |
| 51 | file read by anyone other than the =gitbay= user discloses every CI | |
| 52 | secret, 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 | ||
| 68 | The TLS configuration uses Go's defaults; no minimum version or cipher | |
| 69 | list 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 | ||
| 84 | Signature verification results are cached in =commit_signatures= with | |
| 85 | the global =key_epoch= at the time of verification. Any change to a | |
| 86 | trust input (a key added or removed, an email verified) bumps the | |
| 87 | epoch, which invalidates every cached result (=internal/store/users.go=, | |
| 88 | =internal/control/sig.go=). | |
| 89 | ||
| 90 | The server holds no signing key and signs nothing. A "verified" badge | |
| 91 | means 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 | ||
| 111 | Configured under =[retention]= for =audit=, =events=, | |
| 112 | =webhook_deliveries=, =mail= and =push=; unset means keep forever. | |
| 113 | Expired sessions and tokens are swept hourly regardless | |
| 114 | (=internal/config/config.go=, =cmd/gitbayd/main.go=). | |
| 115 | Accounts 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 | ||
| 18 | A file that does not parse sets a =ci/config= failure status on the | |
| 19 | commit instead of failing silently. | |
| 20 | ||
| 21 | * Build lifecycle | |
| 22 | ||
| 23 | 1. *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=. | |
| 29 | 2. *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=). | |
| 36 | 3. *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=). | |
| 39 | 4. *Log.* =runner log <id>= streams stdin into the build row; the server | |
| 40 | ends the stream if the build is cancelled (=build.go=). | |
| 41 | 5. *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=). | |
| 44 | 6. *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 | ||
| 48 | Who 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 | ||
| 14 | Table =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 | ||
| 25 | Failed commands and reads are not audited. The separate =events= table is | |
| 26 | the 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 | ||
| 71 | Restore procedure: extract the archive into an empty directory, point | |
| 72 | =server.root= at it, start =gitbayd=; hooks regenerate and the host key | |
| 73 | is 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 | ||
| 88 | Open 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 | ||
| 3 | One row per control an auditor typically asks about. *Status*: =in | |
| 4 | place= (implemented and cited), =partial= (implemented with a stated | |
| 5 | limit), =gap= (not implemented; see [[file:10-Known-Gaps.org][10]]). Categories follow the | |
| 6 | chapter 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 | ||
| 3 | Open weaknesses. Issues on krz/gitbay are public; this page gives the | |
| 4 | title and the consequence, not a reproduction. The current list is the | |
| 5 | open issues labelled =security=: | |
| 6 | https://gitbay.org/krz/gitbay/issues?label=security. The table below is | |
| 7 | what 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 | ||
| 22 | Decisions already taken on these: #256 closes a removed key's | |
| 23 | connections, running commands included; #257 refuses credential | |
| 24 | creation from expiring tokens, records which token created each | |
| 25 | credential, and makes =read= the default scope. | |
| 26 | ||
| 27 | * Not yet filed | |
| 28 | ||
| 29 | Found 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> | |
| 4 | text{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> | |
| 4 | text{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> | |
| 4 | text{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> | |
| 4 | text{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> | |
| 4 | text{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> | |
| 4 | text{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> | |
| 4 | text{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/<job> 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 <step>, 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 <id>: 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 <id> 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/<job> · 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 > 2 min, or started > 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 | ||
| 4 | Usage: python3 .gitbay/wiki/Architecture/diagrams/diagrams.py .gitbay/wiki/Architecture/diagrams | |
| 5 | """ | |
| 6 | import sys | |
| 7 | from xml.sax.saxutils import escape as esc | |
| 8 | ||
| 9 | OUT = sys.argv[1] | |
| 10 | ||
| 11 | STYLE = """<style> | |
| 12 | text{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 | ||
| 62 | class 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 | ||
| 122 | LEGEND = [("gb", "gitbay"), ("act", "actor"), ("ext", "external or host"), ("st", "stored data"), ("bad", "untrusted or refused")] | |
| 123 | ||
| 124 | ||
| 125 | def 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 | ||
| 171 | def 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 | ||
| 210 | def 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 | ||
| 243 | def 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 | ||
| 290 | def 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 | ||
| 348 | def 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 | ||
| 375 | def 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 | ||
| 399 | def 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 | ||
| 419 | context() | |
| 420 | components() | |
| 421 | deployment() | |
| 422 | trust() | |
| 423 | authz() | |
| 424 | push() | |
| 425 | ci() | |
.gitbay/wiki/Home.org +1
| @@ -11,6 +11,7 @@ CLI-first git forge: SSH is the API, the web is a rendering. | ||
| 11 | 11 | - [[Admin][Admin guide]] — install, configuration reference, backup, security |
| 12 | 12 | - [[API][API and webhooks]] — the JSON API contract, tokens, payloads |
| 13 | 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 | 15 | - [[Parity][Parity]] — what each surface can do, and what has no page yet |
| 15 | 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 | |
| 3 | 3 | What the forge trusts, what it refuses to do, and where the boundaries |
| 4 | 4 | are. This is the reference for security review; it complements the audit |
| 5 | log and hardening notes in [[Admin]]. | |
| 5 | log and hardening notes in [[Admin]]. Diagrams, data flows, a controls | |
| 6 | matrix and the open gaps are in the [[file:Architecture/00-Overview.org][Architecture]] pages. | |
| 6 | 7 | |
| 7 | 8 | * What gitbay never does |
| 8 | 9 | |