Commit 3035725993

3035725993b817e53e256ff0d07edfd7f46b48e6

parent: b94ae2638b

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 22:35 UTC

gitbayd: one pack-generation limit for SSH, HTTP and git://

Closes #262

Layout: unified · split

.gitbay/wiki/Admin.org +14
@@ -208,6 +208,20 @@ push=.
208208 Organizations are not capped.
209209- =max_bytes_per_user= (0, unlimited) — disk the account's own
210210 repositories may take; a push may be no larger than what is left.
211- =pack_concurrency= (3), =pack_per_principal= (2), =pack_queue= (32),
212 =pack_queue_wait= (="60s"=) — git pack generation (clones, fetches,
213 =git archive --remote=) over SSH, smart HTTP and git:// shares one
214 budget: this many at once, this many per account (per client
215 address when anonymous), and this many waiting for at most the wait.
216 Past that an SSH client gets "the server is busy…" and exit 1, HTTP
217 gets 503 with =Retry-After: 30=, git:// an =ERR= line. A queued
218 client that disconnects leaves the queue; a running clone whose
219 client disconnects is killed. Ref listings (info/refs, protocol v2
220 =ls-refs=), pushes and web archives are outside the budget. For the
221 three counts 0 means the default and a negative value turns that
222 bound off. The defaults suit a four-core host; see [[Performance]].
223 With =ssh.mode = "system"= each SSH session is its own process and
224 SSH clones are not counted.
211225- =max_pack_bytes=, =ssh_auth_rate= — reserved, not yet enforced.
212226
213227** [git_daemon]
.gitbay/wiki/Architecture/09-Controls.org +1 −1
@@ -96,7 +96,7 @@ chapter names of OWASP ASVS 4.0 where one fits.
9696| Control | Status | Evidence |
9797|---------------------------------------------+----------+------------------------------------------------------------------|
9898| 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 |
99| Concurrency limit on git pack generation | in place | global, per-principal, bounded queue across SSH, HTTP and git:// (=internal/packlimit=); not in system SSH mode |
100100| Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) |
101101| Backups offsite and append-only | in place | restic with append-only credentials (documented) |
102102| Restore tested | gap | #259 |
.gitbay/wiki/Architecture/10-Known-Gaps.org +1 −2
@@ -13,8 +13,6 @@ what the 2026-09-27 review found; remove a row when its issue closes.
1313| #259 | Recovery | No restore has been exercised; the drill is written (Admin wiki) and not yet run | high |
1414| #260 | CI network | Builds share the runner's source address; no egress policy | medium |
1515| #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium |
16| #262 | Availability | No limit on concurrent git pack generation | high |
17| #298 | SSRF | =repo import --from= fetches without an address check | medium |
1816| #297 | Credentials | A browser session can mint tokens and keys that outlive it | low |
1917
2018* Not filed
@@ -22,6 +20,7 @@ what the 2026-09-27 review found; remove a row when its issue closes.
2220| Area | Gap | Severity |
2321|-------+-------------------------------------------------------------------------------------------------------------+----------|
2422| Audit | Removing the newest audit rows, or writing new rows under their freed ids, is not detectable from the database; only comparing =gitbayd admin audit verify='s last id and hash with the daemon's journal shows it. Rows written by =gitbayd shell= (=ssh.mode = "system"=) and host admin commands have no journal copy, and the refusal caps are per process, so under that mode each connection counts separately | low |
23| Availability | Under =ssh.mode = "system"= each SSH session is a separate =gitbayd shell= process, so the pack-generation limit (=internal/packlimit=, #262) cannot count SSH clones across sessions; only HTTP and git:// share a budget there | low |
2524
2625* Questions an auditor will ask that have no answer yet
2726
.gitbay/wiki/Performance.org +12 −2
@@ -45,5 +45,15 @@ this scale never appears in a profile.
4545
4646The practical ceiling on this hardware is concurrent pack generation:
4747full clones of large repositories are CPU-bound in git itself (the 17s
48clone ran git at ~156% CPU). A busier instance would scale that with
49cores, not with changes to gitbay.
48clone ran git at ~156% CPU). =limits.pack_concurrency= bounds how many
49run at once across SSH, HTTP and git://, with a queue behind it (see
50[[Admin]], =[limits]=); the measurements below set its default.
51
52* Concurrent clones
53
54Measured with =deploy/clonebench.sh https://gitbay.org/krz/gitbay.git <n>=
55from a machine outside bay1 (four cores), before and after the pack
56limit was deployed with its defaults (=pack_concurrency= 3,
57=pack_per_principal= 2, =pack_queue= 32, =pack_queue_wait= 60s). All
58clones in one run come from one address, so the per-principal cap
59applies to them; the "limit off" run sets the counts to -1.
CHANGELOG.org +10
@@ -217,6 +217,16 @@ missing, =gitbayd admin backup --verify <archive>= names it, and
217217 repository on the host. (#259)
218218- =gitbayd admin secrets init= and =rotate= hold an flock on =<key
219219 file>.lock=, so two runs at once serialize. (#273)
220- Git pack generation (clones, fetches, =git archive --remote=) over
221 SSH, smart HTTP and git:// now shares one concurrency budget:
222 =limits.pack_concurrency= (3), =pack_per_principal= (2), =pack_queue=
223 (32) and =pack_queue_wait= (60s). Past the queue an SSH client sees
224 "the server is busy…" and exits 1, HTTP gets 503 with
225 =Retry-After: 30=, and git:// gets an =ERR= line. *Operators:* the
226 defaults are tuned for a four-core host; set the three counts to -1
227 to turn the limit off. Ref listings, pushes and web archives are
228 unaffected. Under =ssh.mode = "system"= SSH clones are not counted,
229 since each session is its own process (#262).
220230
221231* v1.36.0 — 2026-09-23
222232
cmd/gitbayd/main.go +7 −3
@@ -32,6 +32,7 @@ import (
3232 "gitbay.org/gitbay/internal/httpd"
3333 "gitbay.org/gitbay/internal/mirror"
3434 "gitbay.org/gitbay/internal/notify"
35 "gitbay.org/gitbay/internal/packlimit"
3536 "gitbay.org/gitbay/internal/push"
3637 "gitbay.org/gitbay/internal/seal"
3738 "gitbay.org/gitbay/internal/sshd"
@@ -228,11 +229,14 @@ func serveCmd() *cobra.Command {
228229 return control.RepoDir(cfg.Server.Root, owner, name)
229230 }, buildinfo.String()).Run(whCtx)
230231
232 // One pack-generation budget for SSH, smart HTTP and git://.
233 packs := packlimit.New(cfg.Limits.PackLimits())
234
231235 errCh := make(chan error, 3)
232236 var sshSrv *sshd.Server
233237 var sshLn, gitLn net.Listener
234238 if cfg.SSH.Mode == "embedded" {
235 srv, err := sshd.New(cfg, st, nil)
239 srv, err := sshd.New(cfg, st, packs)
236240 if err != nil {
237241 return err
238242 }
@@ -249,7 +253,7 @@ func serveCmd() *cobra.Command {
249253 slog.Info("ssh handled by host sshd (ssh.mode = system)")
250254 }
251255
252 web := httpd.New(cfg, st, nil)
256 web := httpd.New(cfg, st, packs)
253257 // Header and idle timeouts bound what an idle or slow client can
254258 // hold open. No write timeout: archives and upload-pack stream
255259 // for as long as they take (#104).
@@ -338,7 +342,7 @@ func serveCmd() *cobra.Command {
338342 }
339343 slog.Info("git-daemon listening", "addr", gln.Addr())
340344 gitLn = gln
341 go func() { errCh <- gitd.New(cfg, st, nil).Serve(gln) }()
345 go func() { errCh <- gitd.New(cfg, st, packs).Serve(gln) }()
342346 }
343347
344348 select {
deploy/clonebench.sh added +24
@@ -0,0 +1,24 @@
1#!/bin/sh
2# clonebench.sh <clone-url> <n>: start n full bare clones of <clone-url>
3# at once and print each one's wall time and outcome, then the total.
4# Run from a machine other than the server, against a public repository.
5set -eu
6url=$1
7n=$2
8dir=$(mktemp -d)
9trap 'rm -rf "$dir"' EXIT
10start=$(date +%s)
11i=1
12while [ "$i" -le "$n" ]; do
13 (
14 s=$(date +%s)
15 if git clone --quiet --bare "$url" "$dir/$i.git" 2>"$dir/$i.err"; then
16 echo "$i ok $(( $(date +%s) - s ))s"
17 else
18 echo "$i failed $(( $(date +%s) - s ))s: $(head -n 1 "$dir/$i.err")"
19 fi
20 ) &
21 i=$((i + 1))
22done
23wait
24echo "total $(( $(date +%s) - start ))s for $n clones"