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=. |
| 208 | 208 | Organizations are not capped. |
| 209 | 209 | - =max_bytes_per_user= (0, unlimited) — disk the account's own |
| 210 | 210 | 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. |
| 211 | 225 | - =max_pack_bytes=, =ssh_auth_rate= — reserved, not yet enforced. |
| 212 | 226 | |
| 213 | 227 | ** [git_daemon] |
.gitbay/wiki/Architecture/09-Controls.org
+1 −1
| @@ -96,7 +96,7 @@ chapter names of OWASP ASVS 4.0 where one fits. |
| 96 | 96 | | Control | Status | Evidence | |
| 97 | 97 | |---------------------------------------------+----------+------------------------------------------------------------------| |
| 98 | 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 | |
| 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 | |
| 100 | 100 | | Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) | |
| 101 | 101 | | Backups offsite and append-only | in place | restic with append-only credentials (documented) | |
| 102 | 102 | | 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. |
| 13 | 13 | | #259 | Recovery | No restore has been exercised; the drill is written (Admin wiki) and not yet run | high | |
| 14 | 14 | | #260 | CI network | Builds share the runner's source address; no egress policy | medium | |
| 15 | 15 | | #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 | |
| 18 | 16 | | #297 | Credentials | A browser session can mint tokens and keys that outlive it | low | |
| 19 | 17 | |
| 20 | 18 | * Not filed |
| @@ -22,6 +20,7 @@ what the 2026-09-27 review found; remove a row when its issue closes. |
| 22 | 20 | | Area | Gap | Severity | |
| 23 | 21 | |-------+-------------------------------------------------------------------------------------------------------------+----------| |
| 24 | 22 | | 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 | |
| 25 | 24 | |
| 26 | 25 | * Questions an auditor will ask that have no answer yet |
| 27 | 26 | |
.gitbay/wiki/Performance.org
+12 −2
| @@ -45,5 +45,15 @@ this scale never appears in a profile. |
| 45 | 45 | |
| 46 | 46 | The practical ceiling on this hardware is concurrent pack generation: |
| 47 | 47 | full clones of large repositories are CPU-bound in git itself (the 17s |
| 48 | | clone ran git at ~156% CPU). A busier instance would scale that with |
| 49 | | cores, not with changes to gitbay. |
| 48 | clone ran git at ~156% CPU). =limits.pack_concurrency= bounds how many |
| 49 | run 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 | |
| 54 | Measured with =deploy/clonebench.sh https://gitbay.org/krz/gitbay.git <n>= |
| 55 | from a machine outside bay1 (four cores), before and after the pack |
| 56 | limit was deployed with its defaults (=pack_concurrency= 3, |
| 57 | =pack_per_principal= 2, =pack_queue= 32, =pack_queue_wait= 60s). All |
| 58 | clones in one run come from one address, so the per-principal cap |
| 59 | applies 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 |
| 217 | 217 | repository on the host. (#259) |
| 218 | 218 | - =gitbayd admin secrets init= and =rotate= hold an flock on =<key |
| 219 | 219 | 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). |
| 220 | 230 | |
| 221 | 231 | * v1.36.0 — 2026-09-23 |
| 222 | 232 | |
cmd/gitbayd/main.go
+7 −3
| @@ -32,6 +32,7 @@ import ( |
| 32 | 32 | "gitbay.org/gitbay/internal/httpd" |
| 33 | 33 | "gitbay.org/gitbay/internal/mirror" |
| 34 | 34 | "gitbay.org/gitbay/internal/notify" |
| 35 | "gitbay.org/gitbay/internal/packlimit" |
| 35 | 36 | "gitbay.org/gitbay/internal/push" |
| 36 | 37 | "gitbay.org/gitbay/internal/seal" |
| 37 | 38 | "gitbay.org/gitbay/internal/sshd" |
| @@ -228,11 +229,14 @@ func serveCmd() *cobra.Command { |
| 228 | 229 | return control.RepoDir(cfg.Server.Root, owner, name) |
| 229 | 230 | }, buildinfo.String()).Run(whCtx) |
| 230 | 231 | |
| 232 | // One pack-generation budget for SSH, smart HTTP and git://. |
| 233 | packs := packlimit.New(cfg.Limits.PackLimits()) |
| 234 | |
| 231 | 235 | errCh := make(chan error, 3) |
| 232 | 236 | var sshSrv *sshd.Server |
| 233 | 237 | var sshLn, gitLn net.Listener |
| 234 | 238 | if cfg.SSH.Mode == "embedded" { |
| 235 | | srv, err := sshd.New(cfg, st, nil) |
| 239 | srv, err := sshd.New(cfg, st, packs) |
| 236 | 240 | if err != nil { |
| 237 | 241 | return err |
| 238 | 242 | } |
| @@ -249,7 +253,7 @@ func serveCmd() *cobra.Command { |
| 249 | 253 | slog.Info("ssh handled by host sshd (ssh.mode = system)") |
| 250 | 254 | } |
| 251 | 255 | |
| 252 | | web := httpd.New(cfg, st, nil) |
| 256 | web := httpd.New(cfg, st, packs) |
| 253 | 257 | // Header and idle timeouts bound what an idle or slow client can |
| 254 | 258 | // hold open. No write timeout: archives and upload-pack stream |
| 255 | 259 | // for as long as they take (#104). |
| @@ -338,7 +342,7 @@ func serveCmd() *cobra.Command { |
| 338 | 342 | } |
| 339 | 343 | slog.Info("git-daemon listening", "addr", gln.Addr()) |
| 340 | 344 | gitLn = gln |
| 341 | | go func() { errCh <- gitd.New(cfg, st, nil).Serve(gln) }() |
| 345 | go func() { errCh <- gitd.New(cfg, st, packs).Serve(gln) }() |
| 342 | 346 | } |
| 343 | 347 | |
| 344 | 348 | 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. |
| 5 | set -eu |
| 6 | url=$1 |
| 7 | n=$2 |
| 8 | dir=$(mktemp -d) |
| 9 | trap 'rm -rf "$dir"' EXIT |
| 10 | start=$(date +%s) |
| 11 | i=1 |
| 12 | while [ "$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)) |
| 22 | done |
| 23 | wait |
| 24 | echo "total $(( $(date +%s) - start ))s for $n clones" |