A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 4254359d23

4254359d23f8c27c32e1a77f5f977cd9dd337d44

parent: 980dcaf33d

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-25T01:22:39Z

Move documentation to the wiki

Refs #25. users/admin/api/roadmap/threat-model now live in the
krz/gitbay wiki (itself a git repo, dogfooding the wiki feature) with
cross-references rewritten as wiki page links. README and CONTRIBUTING
point there; CONTRIBUTING stays in the repo as the file conventions
expect.
CONTRIBUTING.org +2 −2
@@ -37,7 +37,7 @@ ssh git@gitbay.org register --username you --invite <code>
3737
3838 * What holds the design together
3939
40Read =docs/roadmap.org= for direction. The invariants that reviews will
40Read the [[https://gitbay.org/krz/gitbay/wiki/Roadmap][roadmap]] (in the wiki) for direction. The invariants that reviews will
4141 hold you to:
4242
4343 - every control command must work from bare OpenSSH; the registry test
@@ -59,7 +59,7 @@ surrounding code; keep diffs surgical.
5959 * Issues
6060
6161 File at [[https://gitbay.org/krz/gitbay/issues][the tracker]] (=gitbay issue create= from a clone). Check
62=docs/roadmap.org= first — it maps the filed issues to phases.
62the [[https://gitbay.org/krz/gitbay/wiki/Roadmap][roadmap]] (in the wiki) first — it maps the filed issues to phases.
6363
6464 * License
6565
README.org +9 −4
@@ -102,10 +102,15 @@ via =gitbay man=, completions via =gitbay completion <shell>=.
102102
103103 * Documentation
104104
105- [[file:docs/users.org][user guide]] — accounts, keys, verified commits, repos, issues, MRs, scripting
106- [[file:docs/admin.org][admin guide]] — install, full configuration reference, backup/restore, upgrades
107- [[file:docs/api.org][API and webhooks]] — the JSON API contract, tokens, webhook payloads and HMAC
108- [[file:docs/roadmap.org][roadmap]] — status, phased plan, and what is deliberately not planned
105Docs live in [[https://gitbay.org/krz/gitbay/wiki][the wiki]] — itself a git repository
106(=git clone ssh://git@gitbay.org/krz/gitbay.wiki.git=), dogfooding the
107wiki feature:
108
109 [[https://gitbay.org/krz/gitbay/wiki/Users][user guide]] — accounts, keys, verified commits, repos, issues, MRs, scripting
110 [[https://gitbay.org/krz/gitbay/wiki/Admin][admin guide]] — install, full configuration reference, backup/restore, security
111 [[https://gitbay.org/krz/gitbay/wiki/API][API and webhooks]] — the JSON API contract, tokens, webhook payloads and HMAC
112 [[https://gitbay.org/krz/gitbay/wiki/Roadmap][roadmap]] — status, phased plan, and what is deliberately not planned
113 [[https://gitbay.org/krz/gitbay/wiki/Threat-Model][threat model]] — what the forge trusts and never does
109114
110115 * Contributing
111116
docs/admin.org deleted −225
@@ -1,225 +0,0 @@
1#+title: gitbay admin guide
2
3One static binary (=gitbayd=), one SQLite file, bare repositories on
4disk, and the system =git=. Schema migrations run automatically on
5startup and on every admin command.
6
7* Install
8
9Build from source (=go build ./cmd/gitbayd=), install via the vanity
10module path (=go install gitbay.org/gitbay/cmd/gitbayd@latest=), or use
11a release build: =deploy/release.sh <tag>= cross-compiles reproducible
12linux/amd64, linux/arm64, and darwin/arm64 binaries with a SHA256SUMS
13manifest (CGO off, trimpath, stripped — byte-identical per commit and
14toolchain).
15
16#+begin_src sh
17install -m 755 gitbayd /usr/local/bin/
18adduser --system --group --home /var/lib/gitbay --shell /usr/sbin/nologin gitbay
19install -d -o gitbay -g gitbay -m 750 /var/lib/gitbay
20gitbayd --config /etc/gitbay/config.toml check-config
21#+end_src
22
23=deploy/= in the source tree has a cloud-init file, a hardened systemd
24unit, and a nightly backup timer. Run as the unprivileged =gitbay= user;
25the unit's =AmbientCapabilities=CAP_NET_BIND_SERVICE= covers ports
2622/80/443 without root.
27
28** The SSH port decision
29
30- =ssh.mode = "embedded"= (default): gitbayd itself listens, normally on
31 22 — move the host's admin sshd to another port. Remotes read
32 =git@host:owner/repo= with no port gymnastics.
33- =ssh.mode = "system"=: the host sshd owns 22 and invokes gitbayd via
34 =AuthorizedKeysCommand=:
35 #+begin_example
36 AuthorizedKeysCommand /usr/local/bin/gitbayd --config /etc/gitbay/config.toml authorized-keys %t %k
37 AuthorizedKeysCommandUser gitbay
38 #+end_example
39 sshd requires that binary to be root-owned and not group/world
40 writable. Unknown keys fail authentication inside sshd, so system mode
41 requires =registration.mode = "closed"= (check-config enforces this).
42
43* Configuration reference
44
45=/etc/gitbay/config.toml=. =check-config= validates and names every
46contradiction; =--no-host-checks= skips port/path probes.
47
48** [server]
49- =root= (default =/var/lib/gitbay=) — repositories, database, host
50 keys, ACME cache all live here.
51- =site_url= (required) — canonical =https://host=; drives ACME, clone
52 URLs, mail links.
53
54** [ssh]
55- =mode==embedded= | =system= (above).
56- =port= (22) — embedded listener port.
57- =host_keys= — list of private key paths; empty generates an ed25519
58 key at =<root>/ssh/host_ed25519=.
59
60** [http]
61- =addr= (=:443=), =tls==acme= | =files= | =off=.
62- =acme=: certificates via TLS-ALPN-01 on the HTTPS port, cached at
63 =<root>/acme=; =acme_email= for the CA account; =acme_http_addr=
64 (=:80=, ="off"= to disable) adds HTTP-01 and an https redirect —
65 failing to bind it is a warning, not fatal. Requires an =https://=
66 site_url with a public DNS name.
67- =files=: =cert_file= + =key_file=.
68- =off=: plain HTTP — development, or behind a TLS-terminating proxy.
69
70** [web]
71- =mode==view_only= (default) | =accounts=. In view_only the mutating
72 web routes are never registered; in accounts, browser sessions are
73 minted over SSH (=web login=), and users with write access can create
74 repos, comment, and make simple file edits (which commit unsigned,
75 honestly). =password_auth= is reserved and currently rejected.
76
77** [registration]
78- =mode==closed= (default) | =invite= | =open=. invite/open require
79 [mail]. See the user guide for the flows.
80
81** [mail]
82- =smtp_host= (host:port, 587 assumed), =from=, optional =smtp_user= /
83 =smtp_pass=. STARTTLS when offered. Required for invite/open
84 registration and self-service =email add=; in closed mode you may omit
85 it entirely and assert addresses by hand (below).
86
87** [api]
88- =enabled= (false) — the JSON API surface; see =docs/api.org=. Off
89 means no credential-bearing HTTP endpoint exists at all.
90
91** [webhooks]
92- =allow_local= (false) — permit webhook targets on loopback/private
93 addresses. Leave off unless you know why you need it (SSRF).
94
95** [limits]
96- =clone_timeout= (3600s) — cap on =repo import= fetches.
97- =max_blob_bytes= (100MB) — cap on raw file serving over the web.
98- =max_asset_bytes= (512MB) — cap per uploaded release asset.
99- =max_pack_bytes=, =ssh_auth_rate= — reserved, not yet enforced.
100
101** [git_daemon]
102- =enabled= (false), =port= (9418) — the anonymous =git://= listener.
103 Serves only public repositories that additionally ran
104 =repo settings git-daemon <repo> on=.
105
106** [mirrors]
107- =pull_interval_minutes= (15) — how often pull mirrors fetch their
108 upstream. Push mirrors sync shortly after each local ref update.
109 Mirror URLs pass the same SSRF rules as webhook targets.
110
111** [go_import]
112Vanity Go module paths, one per line: ="host/module" = "owner/repo"=.
113Requests with =?go-get=1= at or under the module path answer with the
114go-import meta tag pointing at the repository's HTTPS clone URL, so
115=go install host/module/cmd/...@latest= resolves. The repository should
116be public (the module path itself confirms it exists).
117
118* Users, email, invites
119
120#+begin_src sh
121gitbayd admin user create alice --key alice.pub --email a@example.org --verified [--admin]
122gitbayd admin email verify alice a@example.org # admin assertion, no SMTP needed
123gitbayd admin invite --email b@example.org # mails a code; prints it if no SMTP
124#+end_src
125
126"Verified" means SMTP-confirmed or host-admin-asserted; the database
127records which. Verified emails are what make commit signatures
128meaningful — an unverified address never produces a =verified= badge.
129
130* Audit and account control
131
132The audit log is the security feed (events are the product feed): every
133successful mutating command with its argv and source credential (SSH key
134fingerprint or API), registrations, admin actions, force-pushes, and
135auth failures/throttling. Secrets never appear — they travel on stdin,
136never in argv.
137
138#+begin_src sh
139gitbayd admin audit [--limit n] # host-local
140ssh git@<host> audit [--limit n] # instance admins, SSH only
141gitbayd admin user disable <name> # suspend: SSH, web, API all refused;
142gitbayd admin user enable <name> # sessions dropped, nothing deleted
143#+end_src
144
145=limits.ssh_auth_rate= (10) throttles per-IP authentication *failures*
146per minute — successful auths never count and clear the slate.
147=limits.max_pack_bytes= is enforced as =receive.maxInputSize= on every
148push.
149
150* Maintenance
151
152#+begin_src sh
153gitbayd admin stats [--json] # counts, database size, per-repo disk
154gitbayd admin gc [--repo owner/name] # git gc: repack and prune; per-repo sizes
155gitbayd admin gc --aggressive # thorough repack; slow, rarely needed
156#+end_src
157
158=deploy/cloud-init.yaml= ships a =gitbay-gc.timer= that runs =admin gc=
159weekly (Sunday 07:00 UTC). Imported repositories keep whatever pack
160layout the source sent, so a first manual =admin gc= after a bulk
161import is worthwhile.
162
163* Backup and restore
164
165#+begin_src sh
166gitbayd admin backup --out /var/backups/gitbay/backup.tar.gz
167#+end_src
168
169One archive: a consistent SQLite snapshot (taken *before* the
170repositories are read, so the database never references objects the
171archive missed), every repository, and the SSH host keys. Excluded:
172hook socket, regenerated hook scripts, WAL files. Safe to run against a
173live daemon.
174
175Restore: extract into an empty directory, point =server.root= at it,
176start gitbayd. Host keys are preserved, so clients keep their
177known_hosts entries; hooks regenerate at startup.
178
179* Upgrades
180
181Replace the binary, restart the unit. Migrations apply automatically and
182are transactional; hook scripts under =<root>/hooks= are rewritten at
183startup to point at the current binary path.
184
185* Security
186
187The =docs/threat-model.org= file is the reference for what the forge
188trusts and refuses to do. Operational checklist:
189
190- *Software checks.* =deploy/audit.sh= runs =go vet=, =govulncheck=
191 (the module list is deliberately short — review it on each release),
192 and a short fuzz pass over every attacker-facing parser (pkt-line,
193 commit, SSHSIG armor, OpenPGP key, SSH tokenizer). Run it before
194 tagging a release.
195- *Web responses* carry a scripts-forbidden CSP, =X-Frame-Options:
196 DENY=, =nosniff=, =no-referrer=, and HSTS when TLS is on — no
197 configuration needed.
198- *Host sandboxing.* The systemd unit in =deploy/cloud-init.yaml= runs
199 gitbayd unprivileged with =ProtectSystem=strict=, =PrivateDevices=,
200 =LockPersonality=, =MemoryDenyWriteExecute=,
201 =SystemCallFilter=@system-service=, and =RestrictAddressFamilies= to
202 INET/INET6/UNIX. It keeps =CAP_NET_BIND_SERVICE= only, to bind 22/80/443.
203- *OS patches* apply via =unattended-upgrades= (security origins,
204 auto-reboot 04:30 if required).
205- *Admin sshd (2222)* is throttled by =MaxStartups=/=MaxAuthTries= and
206 watched by =fail2ban=; gitbayd's own port 22 is throttled by
207 =limits.ssh_auth_rate= (auth failures per IP per minute).
208- *Monitoring.* =gitbay-monitor.timer= posts disk/service/cert status
209 hourly to the webhook URL in =/etc/gitbay/monitor.url= (create the
210 file to enable; absent = silent). Alerts fire on a stopped service or
211 disk ≥ 85%.
212- *Database.* =gitbay.db= and its WAL live under =/var/lib/gitbay= (mode
213 0750, owned by =gitbay=). The nightly archive plus provider snapshots
214 are the recovery path; for tighter RPO, add continuous replication
215 (litestream) against the same file — it coexists with the WAL.
216
217* Odds and ends
218
219- deleting a fork marks MRs sourced from it =source_gone=; their diffs
220 remain viewable and mergeable because the target repo owns the
221 objects.
222- =refs/merge-requests/*= is server-owned and unpushable by clients.
223- audit-relevant activity (issue/MR lifecycle, imports, pushes) lands in
224 the =events= table, which also feeds webhooks.
225- the daemon idles under 10MB RSS; the smallest VPS tier is adequate.
docs/api.org deleted −127
@@ -1,127 +0,0 @@
1#+title: gitbay API and webhooks
2
3* The JSON API
4
5One endpoint fronts the entire command registry — everything the SSH
6control plane can do, current and future, with identical semantics.
7Disabled by default; the instance must set:
8
9#+begin_src toml
10[api]
11enabled = true
12#+end_src
13
14** Tokens
15
16Tokens are minted over SSH and only over SSH — an API token can never
17create further credentials.
18
19#+begin_src sh
20gitbay auth token create --name ci [--scope full|read] [--ttl 30d]
21gitbay auth token list
22gitbay auth token revoke ci
23#+end_src
24
25The token (prefix =gb_=, shown exactly once) is presented as
26=Authorization: Bearer gb_...=. Only a hash is stored server-side.
27Scope =read= permits list/show/log/diff-style commands and refuses
28anything that modifies state. =--ttl= takes Go durations or a day
29suffix (=30d=); expired, revoked, and unknown tokens all answer the
30same 401.
31
32** POST /api/v1/cmd
33
34Request body:
35#+begin_src json
36{"argv": ["issue", "create", "you/project", "--title", "from CI"],
37 "stdin": "optional body for --file - style input"}
38#+end_src
39
40=argv= is real argv — no shell, no quoting rules. The response is the
41command's own JSON envelope with =exit_code= added, plus =stderr= when
42the command wrote diagnostics:
43
44#+begin_src json
45{"protocol_version": 1, "exit_code": 0, "data": {"number": 7}}
46#+end_src
47
48HTTP status maps the exit code: 0→200, 2→400, 3→404, 4→403, else 500.
49Commands that emit raw text rather than an envelope (=help=, =mr diff=)
50come wrapped as ={"output": "..."}=. Git transport commands and the
51token commands are refused by name.
52
53#+begin_src sh
54curl -s -H "Authorization: Bearer $TOKEN" \
55 -d '{"argv":["whoami"]}' https://gitbay.org/api/v1/cmd
56
57printf '{"argv":["issue","create","you/project","--title","t","--file","-"],"stdin":"body\n"}' |
58curl -s -H "Authorization: Bearer $TOKEN" -d @- https://gitbay.org/api/v1/cmd
59#+end_src
60
61* Commit statuses (CI reporting)
62
63CI reports results through the same command surface (over SSH or the
64JSON API with a full-scope token; reporting requires write access):
65
66#+begin_src sh
67gitbay status set <owner/name> <sha> --context build --state pending
68gitbay status set <owner/name> <sha> --context build --state success --url https://ci.example/run/1
69gitbay status list <owner/name> <sha> --json # {"combined": "...", "statuses": [...]}
70#+end_src
71
72One row per (commit, context): re-reporting updates in place. States:
73=pending=, =success=, =failure=, =error=; the combined state is the
74worst of them. Statuses appear on commit pages, MR pages, and
75=mr show=. With =repo settings require-checks <repo> on=, merging
76requires the MR head to carry statuses and all of them green. Each
77report also emits a =status= event to webhooks.
78
79* Webhooks
80
81Per-repository outbound POSTs for repository events. Managed by repo
82admins:
83
84#+begin_src sh
85gitbay webhook add <url> --secret s3cret [--events push,issue.created] # default *
86gitbay webhook list
87gitbay webhook deliveries [--limit 50] # status, attempts, last error
88gitbay webhook redeliver <delivery-id> # requeue, including dead letters
89gitbay webhook remove <id>
90#+end_src
91
92** Events
93
94=push= (data: ref, old, new, forced, deleted), =issue.created=,
95=issue.commented=, =issue.closed=, =issue.open=, =mr.created=,
96=mr.merged= (data: number, sha), =repo.imported= (data: from).
97
98** Delivery
99
100Each event POSTs one JSON body:
101
102#+begin_src json
103{"event": "push",
104 "repo": "you/project",
105 "actor": "alice",
106 "created_at": "2026-08-24T01:00:00.000Z",
107 "data": {"ref": "refs/heads/main", "old": "...", "new": "...",
108 "forced": false, "deleted": false}}
109#+end_src
110
111Headers: =X-Gitbay-Event=, =X-Gitbay-Delivery= (id), and — when the hook
112has a secret — =X-Gitbay-Signature-256: sha256=<hex hmac>= over the raw
113body. Verify before trusting:
114
115#+begin_src python
116import hmac, hashlib
117expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
118ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"])
119#+end_src
120
121A 2xx within 10 seconds is success. Anything else retries with
122exponential backoff (30s base, doubling) and dead-letters after five
123attempts; =webhook deliveries= shows the trail and =redeliver= revives a
124dead letter. Redirects are never followed, and targets resolving to
125loopback/private/link-local addresses are refused both at registration
126and again at connect time, unless the instance sets
127=[webhooks] allow_local=.
docs/roadmap.org deleted −149
@@ -1,149 +0,0 @@
1#+title: gitbay roadmap
2
3Status and direction as of 2026-08-24. Issue numbers reference this
4repository's tracker; this file is the narrative, the tracker is the
5truth.
6
7* Where things stand
8
9Everything in the original plan is built, tested end-to-end against real
10git/ssh/sshd/gpg, and running in production at gitbay.org: the SSH
11control plane (usable from bare OpenSSH, enforced by test), git over
12SSH/HTTPS/git://, signature verification with six states and epoch
13caching, protected branches and =require_signed_commits= (push-time and
14merge-time), issues, merge requests with four merge strategies, orgs
15with membership-derived access, rename/transfer, repo import, invite and
16open registration with SMTP verification, ACME TLS, the read-only and
17accounts web modes, the JSON API fronting the whole command registry,
18signed webhooks with retries and dead-lettering, restore-tested backups,
19the =gitbay= CLI, and docs. The instance hosts 65 repositories including
20this one, and its own development already runs through its issues and
21merge requests.
22
23What it is today: an excellent forge for its author and for CLI-native
24individuals. What it is not yet: a forge a GitHub-habituated *team*
25would stay on, or a project outsiders can easily run themselves.
26
27* Phase 1 — collaboration credibility [COMPLETE 2026-08-24]
28
29Goal: a second contributor works here for a week and misses nothing they
30would act on. All five shipped: deploy keys, commit statuses with
31require-checks gating, email notifications, inline review threads, and
32required approvals with CODEOWNERS and require-resolved.
33
34- [[https://gitbay.org/krz/gitbay/issues/22][#22]] deploy keys — smallest item, unblocks CI checkout; the scope
35 already exists in the policy layer
36- [[https://gitbay.org/krz/gitbay/issues/1][#1]] commit statuses API and MR check display
37- [[https://gitbay.org/krz/gitbay/issues/3][#3]] email notifications for issue/MR activity
38- [[https://gitbay.org/krz/gitbay/issues/2][#2]] inline review comments on MR diffs
39- [[https://gitbay.org/krz/gitbay/issues/19][#19]] required approvals and CODEOWNERS (builds on #1 and #2)
40
41* Phase 2 — a product, not a debug view [COMPLETE 2026-08-24; #30 activity graph follows on]
42
43Goal: the site looks and reads like something you would recommend.
44Mostly web-layer; descriptions and profiles already landed as the first
45step.
46
47- [[https://gitbay.org/krz/gitbay/issues/10][#10]] design revamp — closed 2026-08-24 after review (further design work is iteration under new issues)
48 — shipped 2026-08-24, open pending visual review
49- [[https://gitbay.org/krz/gitbay/issues/6][#6]] cross-references (#N) and @mentions — done 2026-08-24
50 (rendering-side; backlinks and mention notifications later)
51- [[https://gitbay.org/krz/gitbay/issues/23][#23]] archived repos and topics — done 2026-08-24 (topic
52 filtering rides along with search, #7)
53- [[https://gitbay.org/krz/gitbay/issues/24][#24]] blame view — done 2026-08-24
54- [[https://gitbay.org/krz/gitbay/issues/7][#7]] search — done 2026-08-24 (repo search by name/desc/topic,
55 per-repo git grep on web+SSH; cross-repo indexer only if ever needed)
56- [[https://gitbay.org/krz/gitbay/issues/20][#20]] milestones and issue templates — done 2026-08-24
57- [[https://gitbay.org/krz/gitbay/issues/30][#30]] activity graph on owner pages (platform-recorded activity
58 signal; events table already covers issues/MRs, extend to commits)
59- [[https://gitbay.org/krz/gitbay/issues/31][#31]] issue actions from commit messages — done 2026-08-24
60 (closes/fixes/resolves #N closes on landing; bare #N leaves a comment)
61
62* Phase 3 — other people's forges [COMPLETE 2026-08-24]
63
64Goal: someone who is not the author runs an instance and moves their
65work to it.
66
67- [[https://gitbay.org/krz/gitbay/issues/26][#26]] release engineering — done 2026-08-24 except artifact
68 hosting, which waits on #8 (go-install vanity live, release.sh,
69 CHANGELOG, Homebrew formula in krz/homebrew-tap)
70 imports, Homebrew/deb — the adoption gate for everything below
71- [[https://gitbay.org/krz/gitbay/issues/27][#27]] repository maintenance — done 2026-08-24 (admin gc/stats, weekly gitbay-gc.timer)
72- [[https://gitbay.org/krz/gitbay/issues/8][#8]] releases — done 2026-08-24 (notes + assets; v0.1.0 binaries hosted)
73- [[https://gitbay.org/krz/gitbay/issues/17][#17]] issue/PR history import from GitHub — done 2026-08-24
74- [[https://gitbay.org/krz/gitbay/issues/18][#18]] push/pull mirroring — done 2026-08-24 (worker sync, read-only pull mirrors)
75- [[https://gitbay.org/krz/gitbay/issues/29][#29]] account migration — done 2026-08-24 (bundle export/replay + client-side git mirror; no lock-in,
76 ever; the export bundle doubles as a user-level backup)
77- [[https://gitbay.org/krz/gitbay/issues/14][#14]] audit logging and multi-user hardening — done 2026-08-24 (quotas and key-expiry warnings ride with #28)
78- [[https://gitbay.org/krz/gitbay/issues/9][#9]] web signup for open/invite instances — done 2026-08-24
79
80* Phase 4 — reach
81
82Bigger bets, each valuable independently; order by appetite.
83
84- [[https://gitbay.org/krz/gitbay/issues/13][#13]] CI/CD via external runners (after #1; the forge never executes
85 repository content)
86- [[https://gitbay.org/krz/gitbay/issues/16][#16]] Git LFS
87- [[https://gitbay.org/krz/gitbay/issues/15][#15]] static page hosting (needs the separate-origin decision)
88- [[https://gitbay.org/krz/gitbay/issues/11][#11]] iOS app (hutch-based) and [[https://gitbay.org/krz/gitbay/issues/12][#12]] Android
89- [[https://gitbay.org/krz/gitbay/issues/21][#21]] teams within orgs — done 2026-08-25 (members-role scoping + per-repo team grants)
90- [[https://gitbay.org/krz/gitbay/issues/25][#25]] wikis — done 2026-08-25 (push-edited .wiki companions; krz/gitbay has one)
91
92When #15 (pages) and #25 (wikis) land, =docs/= moves out of the blob
93view and becomes gitbay.org's own published documentation site — the
94docs dogfooding the features the same way the tracker and MRs already
95do.
96
97* Phase S — security (cross-cutting)
98
99Not a sequential phase: items land alongside whatever phase is active,
100and the whole set gates flipping gitbay.org to open registration.
101
102- [[https://gitbay.org/krz/gitbay/issues/14][#14]] audit logging, rate limiting, quotas, user disable — the
103 multi-user half
104- [[https://gitbay.org/krz/gitbay/issues/28][#28]] hardening umbrella — concrete items done 2026-08-24
105 (umbrella stays open for ongoing work). Both layers landed:
106 - software: fuzz targets for every attacker-facing parser (found and
107 fixed a decodeArmor slice bug), CSP + security headers, govulncheck
108 in =deploy/audit.sh= (fixed circl GO-2026-4550), token comparison
109 audit (hash-lookup, no Go-level compare), =docs/threat-model.org=,
110 optional minisign over release manifests
111 - host: systemd sandbox (=SystemCallFilter=@system-service=,
112 =PrivateDevices=, =LockPersonality=, =MemoryDenyWriteExecute=, …),
113 unattended-upgrades, fail2ban + =MaxStartups=/=MaxAuthTries= on the
114 admin sshd, hourly disk/service/cert monitoring; DB file modes 0750,
115 litestream noted for continuous replication. Applied to bay1.
116
117Already true and worth preserving (the threat model will write these
118down): the forge never executes repository content; no server signing
119key; repo-authored HTML never renders on the forge origin; tokens and
120sessions stored as hashes only; SSRF guards at registration and dial
121time; private repositories indistinguishable from nonexistent.
122
123* Explicitly not planned
124
125Recorded so their absence reads as a decision, not an oversight:
126
127- container/package registry — scope creep away from "forge"; external
128 registries integrate via CI
129- email patch flow — revisit only if sourcehut-style demand appears
130- federation (ForgeFed) and Postgres — no current need at this scale
131
132* Decisions
133
134- **gitbay.org will eventually be open to all.** (Decided 2026-08-24.)
135 Sequencing consequence: before flipping =registration = "open"=, the
136 instance needs #14 (audit log, rate limiting, quotas), #9 (web
137 signup), an SMTP relay configured for verification mail, and enough
138 of Phase 1 that new users get a credible product. Interim step:
139 invite mode for early collaborators as soon as [mail] is configured.
140- **Versioning**: semver, starting at v0.1.0 on the current state.
141 0.x signals moving surfaces; =protocol_version= increments only on
142 breaking envelope/command changes and is otherwise decoupled from
143 release numbers. v1.0.0 when Phase 1 and #26 land. Tags are
144 annotated and signed.
145- **Second contributors**: invite mode is the on-ramp (their keys and
146 verified emails make signed-main enforceable for them too). A
147 CONTRIBUTING file states the workflow: fork on gitbay.org, MR with
148 signed commits, =go test ./...= green, review required once #19
149 exists. No CLA — 0BSD needs none; sign-off optional.
docs/threat-model.org deleted −92
@@ -1,92 +0,0 @@
1#+title: gitbay threat model
2
3What the forge trusts, what it refuses to do, and where the boundaries
4are. This is the reference for security review; it complements the audit
5log and hardening notes in =admin.org=.
6
7* What gitbay never does
8
9- *Execute repository content.* Git object contents are never run. Hooks
10 are gitbay's own binary, invoked by git; they compute facts and ask the
11 daemon over a unix socket. Repo files are only ever read.
12- *Hold a signing key.* There is no server-side signing key. "Verified"
13 means a signature made by a key the *user* registered — the server
14 never vouches for a commit it did not receive already signed. Merge
15 commits the server creates are honestly =unsigned=.
16- *Serve repository HTML on its own origin as active content.* Raw file
17 serving is =text/plain= with =nosniff=. Rendered markdown/org is
18 sanitized (bluemonday) and served under a CSP that forbids scripts.
19- *Put secrets in argv, URLs, or logs.* Import and mirror credentials,
20 registration invites, and API tokens travel on stdin or in request
21 bodies, never as command arguments (visible in =/proc=) or query
22 strings. Tokens are stored only as SHA-256 hashes.
23- *Confirm the existence of private repositories.* Every surface answers
24 "not found" identically for a private repo and a nonexistent one — web
25 pages, git transport, control commands, release asset downloads.
26
27* Trust boundaries
28
29- *SSH public key = identity.* The SSH username is ignored; the presented
30 key's fingerprint resolves to an account. Key uniqueness is global.
31- *Per-instance trust.* Email verification and key registration are local
32 to an instance and never transfer. Account migration re-registers keys
33 and re-verifies emails on the target by design.
34- *The control plane is one authenticated channel* (SSH), fully usable
35 from stock OpenSSH. The JSON API fronts the same command registry with
36 bearer tokens minted only over SSH; git transport never runs over it.
37- *Anonymous surfaces* — HTTPS clone of public repos, =git://= where
38 enabled, the read-only web UI — carry no credentials and expose only
39 public data. HTTP push is refused via a pkt-line =ERR=, never a 401.
40
41* Attacker-controlled parsers
42
43Every parser that eats bytes from a pusher, a key registrant, or an
44anonymous client has a fuzz target and must never panic:
45
46- =internal/protocol= — the SSH command tokenizer (fuzzed against argv
47 round-tripping).
48- =internal/gitd= — the =git://= pkt-line reader.
49- =internal/sig= — the commit parser, the SSHSIG armor decoder and blob
50 parser, and the OpenPGP armored-key reader.
51
52Run =deploy/audit.sh= to exercise them plus =go vet= and =govulncheck=.
53
54* Secret handling
55
56Tokens (web sessions, login links, email verification, API bearer tokens,
57deploy/CI) are random 256-bit values. Only their SHA-256 hash is stored,
58and verification is a database index lookup on that hash — the secret
59itself is never compared in Go, so there is no timing oracle to exploit.
60Webhook payloads are signed outbound with HMAC-SHA256; the forge verifies
61no inbound HMAC.
62
63* Network-facing request forgery
64
65Anything that makes the *server* open an outbound connection to a
66user-supplied address — webhook delivery, GitHub-history import
67=--api-base=, mirror remotes — passes the same SSRF guard: the scheme
68must be http/https and, unless =webhooks.allow_local= is set, the
69resolved address must not be loopback, private, or link-local. The
70webhook dialer re-checks at connect time so a DNS answer that changes
71after validation still cannot reach private space. Redirects are never
72followed.
73
74* Web responses
75
76Every response carries =Content-Security-Policy= (no scripts, no plugins,
77no embedding; inline styles allowed for chroma and label chips; images
78from any origin so external README images render), =X-Frame-Options:
79DENY=, =X-Content-Type-Options: nosniff=, =Referrer-Policy: no-referrer=,
80and =Strict-Transport-Security= when TLS is on. The UI needs no
81JavaScript, so =script-src 'none'= costs nothing.
82
83* Residual risks, accepted
84
85- External images in rendered READMEs load from their origin (no image
86 proxy), which a repo author can use as a tracking pixel against a
87 viewer. Documented; proxying is future work.
88- Backups are consistent per the DB-snapshot-first ordering but are not a
89 single atomic snapshot; a few orphaned git objects are possible and
90 harmless (see =admin.org=).
91- A global signature-verification epoch over-invalidates the cache on any
92 trust-input change. Correct, not a leak; a performance tradeoff.
docs/users.org deleted −333
@@ -1,333 +0,0 @@
1#+title: gitbay user guide
2
3Everything here works from stock OpenSSH — replace =gitbay= with
4=ssh git@<host>= in any command and it behaves identically. The CLI adds
5convenience (instance profiles, repo inference, =$EDITOR=), nothing more.
6=ssh git@<host> help= lists every command the server knows.
7
8* Installing the CLI
9
10#+begin_src sh
11go install gitbay.org/gitbay/cmd/gitbay@latest # any platform with Go
12
13brew tap krz/tap https://gitbay.org/krz/homebrew-tap.git
14brew install krz/tap/gitbay # Homebrew (macOS/Linux)
15#+end_src
16
17Or build from source: =go build ./cmd/gitbay= in a clone of
18=https://gitbay.org/krz/gitbay.git=.
19
20* Getting an account
21
22How you join depends on the instance's registration mode. On instances
23with web accounts enabled, =/register= offers the same signup as a
24browser form (paste your SSH public key); everything below works from
25the terminal alone:
26
27- closed :: an admin creates your account on the host and registers your
28 first SSH key. Nothing for you to do but hand over your public key.
29- invite :: you receive a single-use code by email. With the SSH key you
30 want to use:
31 #+begin_src sh
32 ssh git@<host> register --username you --invite <code>
33 #+end_src
34 Your account is active immediately; the invited address is your
35 verified email.
36- open ::
37 #+begin_src sh
38 ssh git@<host> register --username you --email you@example.org
39 #+end_src
40 A verification code arrives by mail. Until you run
41 =ssh git@<host> email verify <code>=, the account is pending: you can
42 run =whoami= and the email commands, and nothing else — no git, no
43 repos.
44
45* SSH keys
46
47Your key is your identity; there are no passwords anywhere. The SSH
48username is always =git= — the key alone determines who you are.
49
50#+begin_src sh
51gitbay auth keys list
52gitbay auth keys add --scope git < ~/.ssh/ci_key.pub # key on stdin
53gitbay auth keys remove SHA256:...
54#+end_src
55
56Scopes: =full= (default; git plus every control command) or =git= (git
57transport only — right for CI and automation keys, which then cannot
58touch issues, settings, or your account).
59
60A key belongs to exactly one account instance-wide. Registering a key
61someone else already holds is refused without telling you whose it is.
62
63* Verified commits
64
65The commit badge is driven by the *author* email and the signing key:
66=verified= means the signature is valid, the key is registered to an
67account, and the author email is a verified address on that account.
68
69For OpenPGP signing (git's default):
70#+begin_src sh
71gpg --armor --export you@example.org | gitbay auth pgp add
72#+end_src
73
74For SSH signing (=git config gpg.format ssh=): sign with any key
75registered on your account; your verified addresses act as the principal
76set. No separate registration step.
77
78Add and verify additional addresses with =email add <address>= /
79=email verify <code>= (requires the instance to have SMTP; otherwise an
80admin can assert an address for you).
81
82The states you will see, in decreasing order of trust: =verified=,
83=signed_unknown_key= (valid signature, key not registered here — register
84it and history upgrades retroactively), =signed_email_mismatch= (real
85key, author line claims someone else), =signed_key_expired= /
86=signed_key_revoked=, =bad_signature=, =unsigned=. Server-created commits
87(web edits, merge commits) are always =unsigned= — the server holds no
88signing key on principle.
89
90* Repositories
91
92#+begin_src sh
93gitbay repo create you/project [--private]
94gitbay repo clone you/project
95gitbay repo list
96gitbay repo show you/project
97gitbay repo log you/project --limit 20 # commits with signature states
98gitbay repo fork other/project [--name mine]
99gitbay repo delete you/project --yes
100#+end_src
101
102Pushing is SSH-only. Public repositories are anonymously readable over
103HTTPS (and =git://= where enabled); private repositories exist only over
104SSH and answer "not found" to everyone without access.
105
106Access and settings (owner or =admin= grant):
107#+begin_src sh
108gitbay repo access grant you/project alice write # read | write | admin
109gitbay repo access revoke you/project alice
110gitbay repo settings protect you/project main # no force-push, no delete
111gitbay repo settings require-signed you/project on # every commit must verify
112gitbay repo settings git-daemon you/project on # expose over git://
113gitbay repo topics add you/project cli forge # free-form tags, shown on the web
114gitbay repo search forge # find repos by name/description/topic
115gitbay repo grep you/project "some string" # literal git grep over the default branch
116gitbay repo pin you/project # pin to your web dashboard
117gitbay repo unpin you/project
118gitbay repo archive you/project # read-only: pushes and issue/MR
119gitbay repo unarchive you/project # writes refused, browsing intact
120#+end_src
121
122Import from another forge — git data first, then optionally the GitHub
123issue and PR history (issues keep state/labels/comments; PRs land as
124closed or merged MRs with their discussion; originals are attributed
125inline since foreign authors have no local account; re-running resumes
126where it stopped):
127#+begin_src sh
128gitbay repo import you/mirror --from https://github.com/you/repo.git \
129 [--private] [--token-stdin] # token on stdin, never in the URL
130gitbay repo import-issues you/mirror --from you/repo --token-stdin
131#+end_src
132
133Moving between gitbay instances (no lock-in): run on the TARGET, with
134your key registered on both sides. Profile, repos with settings,
135issues, MRs, and comments replay with attribution; git data mirrors
136client-side through your own key. Keys never transfer and emails
137arrive unverified — trust is per-instance. Re-running resumes.
138Push-blocking policies (require-signed, protected branches) are
139deferred and printed for you to re-apply after the data lands.
140=gitbay auth export= alone doubles as a user-level backup.
141
142#+begin_src sh
143gitbay migrate --from old-instance.example [--from-port 22]
144#+end_src
145
146Mirroring keeps a foreign remote in sync during a gradual migration
147(repo admin; https remotes; the token is stored server-side for the
148recurring sync and never echoed back):
149
150#+begin_src sh
151gitbay repo mirror add you/project https://github.com/you/project.git \
152 --direction push --token-stdin # propagate after every local push
153gitbay repo mirror add you/copy https://github.com/them/theirs.git \
154 --direction pull # follow upstream; local pushes refused
155gitbay repo mirror list # sync status and last error, per mirror
156gitbay repo mirror sync / remove <id>
157#+end_src
158
159* Organizations
160
161Orgs share the owner namespace with users and own repositories at
162=org/repo=. By default members get write on all org repos; org admins
163get repo admin, create repos under the org, and manage membership.
164
165#+begin_src sh
166gitbay org create krz
167gitbay org members add krz alice [--role admin]
168gitbay org show krz
169gitbay org rename krz newname # clone URLs change
170gitbay org delete krz --yes # only when it owns no repositories
171#+end_src
172
173Large orgs scope access with teams: set what plain membership implies,
174then grant per-repo roles through named teams (org admins always keep
175admin; the default =write= keeps the simple model):
176
177#+begin_src sh
178gitbay org settings members-role krz none # write | read | none
179gitbay org team create krz core-devs
180gitbay org team add krz core-devs alice bob # org members only
181gitbay org team grant krz core-devs krz/gitbay write
182gitbay org team show krz core-devs # members + grants
183gitbay org team revoke / remove / delete ...
184#+end_src
185
186* Issues
187
188Anyone who can read a repository can file and comment. Closing/reopening
189is for the author or anyone with write; labels and assignees need write.
190
191#+begin_src sh
192gitbay issue create --title "it breaks" [--body "..." | --file -]
193gitbay issue list [--state open|closed|all]
194gitbay issue show 4
195gitbay issue comment 4 --message "same here"
196gitbay issue edit 4 --title "better title" [--body|--file -] # author or write
197gitbay issue close 4 / reopen 4
198gitbay issue label 4 --add bug --remove wontfix
199gitbay issue assign 4 --add alice
200gitbay issue milestone 4 v1.0 # or "none" to clear
201#+end_src
202
203Inside a clone, the repository is inferred from the =origin= remote —
204that is why no =owner/name= appears above. Anywhere else, pass it as the
205first argument. Long text: =--body= inline, =--file -= from stdin, or
206neither on a terminal and =$EDITOR= opens.
207
208Wikis are companion repositories edited by push — no separate storage,
209no web editor, the same rendering pipeline as READMEs (markdown and
210org). Access mirrors the parent repo (read to view, write to push);
211the companion is created on your first push and follows the repo
212through transfer and delete:
213
214#+begin_src sh
215git clone ssh://git@<host>/you/project.wiki.git
216# add Home.md (or .org), more pages, images; relative links between
217# pages just work on the web at /you/project/wiki
218git push
219#+end_src
220
221Releases anchor notes and binary assets to a pushed tag (write access;
222assets stream over SSH, capped by the instance's =max_asset_bytes=):
223
224#+begin_src sh
225gitbay release create v1.0 --title "First light" [--notes|--file -|$EDITOR]
226gitbay release asset add v1.0 tool-linux-amd64 < dist/tool-linux-amd64
227gitbay release asset get v1.0 tool-linux-amd64 > tool # or the web download link
228gitbay release list / show v1.0 / delete v1.0 --yes
229#+end_src
230
231The web shows them under the repository's =releases= tab with rendered
232notes, sha256 sums, and download links.
233
234Commit messages act on issues when the commits land on the default
235branch (direct push or MR merge): =closes/fixes/resolves #4= closes the
236issue with a linking comment, and a bare =#4= leaves a reference
237comment. Each issue/commit pair acts once, ever. Same repository only.
238
239Milestones group issues and MRs toward a release (write access to
240manage, attach with =issue milestone= / =mr milestone=; progress shows
241on the web at =/owner/name/milestones=):
242
243#+begin_src sh
244gitbay milestone create v1.0 --description "first release" --due 2027-01-01
245gitbay milestone list [--state open|closed|all]
246gitbay milestone close v1.0 / reopen v1.0
247#+end_src
248
249Issue templates: commit =.gitbay/issue-template.md= (and optional
250=issue-template-<name>.md= variants) to the default branch. =gitbay
251issue create= prefills =$EDITOR= with the default template, the web
252form prefills its textarea, and =gitbay issue templates= lists them.
253
254* Merge requests
255
256#+begin_src sh
257gitbay mr create --source feature --target main --title "add thing"
258gitbay mr create other/upstream --source you/fork:feature --target main --title "..."
259gitbay mr list / show 4 / diff 4
260gitbay mr checkout 4 # local branch mr/4 from the MR head
261gitbay mr review 4 --approve # or --request-changes / --comment
262gitbay mr merge 4 [--strategy ff|merge|squash|rebase]
263gitbay mr close 4
264#+end_src
265
266Semantics worth knowing:
267
268- the MR head lives in the *target* repository as
269 =refs/merge-requests/N/head= (fetchable by any reader), so an MR
270 survives deletion of its source branch or fork.
271- force-pushing the source updates the MR and marks existing reviews
272 stale.
273- default strategy: fast-forward when possible, else a merge commit.
274 Squash makes one commit authored by the MR author, committed by the
275 merger. Rebase replays a linear range preserving authors; it refuses
276 ranges containing merge commits, and when fast-forward is possible it
277 *is* one (original commits and signatures land untouched).
278- on =require_signed_commits= branches only fast-forwards of fully
279 verified commits merge; everything server-created is refused with
280 instructions to rebase locally.
281
282Repo admins can gate merges (=repo settings ...=): =require-approvals
283<n>= (fresh, non-author approvals; each reviewer's latest review is
284their stance, and a fresh request-changes blocks), =require-resolved=
285(no open review threads), =require-checks= (all statuses green). With
286approvals required, a =CODEOWNERS= file on the target branch (root or
287=.gitbay/=) additionally demands an approval from an owner of every
288owned changed file — gitignore-style patterns, last match wins.
289
290Review threads anchor to diff lines:
291
292#+begin_src sh
293gitbay mr diff-comment 4 --path main.go --line 12 --message "use log here"
294gitbay mr diff-comment 4 --reply 7 --message "done" # join thread 7
295gitbay mr threads 4 # threads with staleness
296gitbay mr resolve 4 7 / unresolve 4 7
297#+end_src
298
299Threads render inline on the MR page. A force-push marks them stale
300(shown under "threads on earlier revisions") rather than guessing new
301anchors; =mr show= reports the unresolved count. Resolving is for the
302thread author, the MR author, or anyone with write.
303
304* Notifications
305
306When the instance has SMTP configured, activity mails you: someone
307opens an issue or MR on your repository, comments where you are a
308participant (author, commenter, reviewer), reviews, closes, or merges.
309You are never mailed about your own actions, and only verified primary
310addresses receive anything. Delivery retries on relay failure.
311
312* Scripting
313
314Every read command takes =--json= and emits one envelope:
315={"protocol_version": 1, "data": ...}=. stdout is data, stderr is
316messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
3174 denied, 5 server/protocol error. Nothing ever prompts; destructive
318commands take =--yes=.
319
320For HTTP automation see =docs/api.org=.
321
322* CLI setup
323
324#+begin_src sh
325gitbay remote add myforge forge.example.org [--port n] [--user u] --default
326gitbay remote list
327gitbay init [name] [--private] # git init + repo create + origin, in one step
328#+end_src
329
330Configuration lives at =~/.config/gitbay/config.toml=. The CLI shells
331out to your real =ssh=, so =~/.ssh/config=, the agent, and hardware keys
332all apply. Man pages: =gitbay man --dir <dir>=; completions:
333=gitbay completion bash|zsh|fish=.