Commit 3035725993
Verified · cmc
Layout: unified · split
.gitbay/wiki/Admin.org +14
| @@ -208,6 +208,20 @@ push=. | |||
| 208 | Organizations are not capped. | 208 | Organizations are not capped. |
| 209 | - =max_bytes_per_user= (0, unlimited) — disk the account's own | 209 | - =max_bytes_per_user= (0, unlimited) — disk the account's own |
| 210 | repositories may take; a push may be no larger than what is left. | 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 | - =max_pack_bytes=, =ssh_auth_rate= — reserved, not yet enforced. | 225 | - =max_pack_bytes=, =ssh_auth_rate= — reserved, not yet enforced. |
| 212 | 226 | ||
| 213 | ** [git_daemon] | 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 | | Control | Status | Evidence | | 96 | | Control | Status | Evidence | |
| 97 | |---------------------------------------------+----------+------------------------------------------------------------------| | 97 | |---------------------------------------------+----------+------------------------------------------------------------------| |
| 98 | | Rate limits on API and writes | in place | [[file:05-Identity-and-Access.org][5. Rate limits]] | | 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 | | Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) | | 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) | | 101 | | Backups offsite and append-only | in place | restic with append-only credentials (documented) | |
| 102 | | Restore tested | gap | #259 | | 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 | | #259 | Recovery | No restore has been exercised; the drill is written (Admin wiki) and not yet run | high | | 13 | | #259 | Recovery | No restore has been exercised; the drill is written (Admin wiki) and not yet run | high | |
| 14 | | #260 | CI network | Builds share the runner's source address; no egress policy | medium | | 14 | | #260 | CI network | Builds share the runner's source address; no egress policy | medium | |
| 15 | | #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium | | 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 | | #297 | Credentials | A browser session can mint tokens and keys that outlive it | low | | 16 | | #297 | Credentials | A browser session can mint tokens and keys that outlive it | low | |
| 19 | 17 | ||
| 20 | * Not filed | 18 | * Not filed |
| @@ -22,6 +20,7 @@ what the 2026-09-27 review found; remove a row when its issue closes. | |||
| 22 | | Area | Gap | Severity | | 20 | | Area | Gap | Severity | |
| 23 | |-------+-------------------------------------------------------------------------------------------------------------+----------| | 21 | |-------+-------------------------------------------------------------------------------------------------------------+----------| |
| 24 | | 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 | | 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 | * Questions an auditor will ask that have no answer yet | 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 | The practical ceiling on this hardware is concurrent pack generation: | 46 | The practical ceiling on this hardware is concurrent pack generation: |
| 47 | full clones of large repositories are CPU-bound in git itself (the 17s | 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 | 48 | clone ran git at ~156% CPU). =limits.pack_concurrency= bounds how many |
| 49 | cores, not with changes to gitbay. | 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 | repository on the host. (#259) | 217 | repository on the host. (#259) |
| 218 | - =gitbayd admin secrets init= and =rotate= hold an flock on =<key | 218 | - =gitbayd admin secrets init= and =rotate= hold an flock on =<key |
| 219 | file>.lock=, so two runs at once serialize. (#273) | 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 | * v1.36.0 — 2026-09-23 | 231 | * v1.36.0 — 2026-09-23 |
| 222 | 232 | ||
cmd/gitbayd/main.go +7 −3
| @@ -32,6 +32,7 @@ import ( | |||
| 32 | "gitbay.org/gitbay/internal/httpd" | 32 | "gitbay.org/gitbay/internal/httpd" |
| 33 | "gitbay.org/gitbay/internal/mirror" | 33 | "gitbay.org/gitbay/internal/mirror" |
| 34 | "gitbay.org/gitbay/internal/notify" | 34 | "gitbay.org/gitbay/internal/notify" |
| 35 | "gitbay.org/gitbay/internal/packlimit" | ||
| 35 | "gitbay.org/gitbay/internal/push" | 36 | "gitbay.org/gitbay/internal/push" |
| 36 | "gitbay.org/gitbay/internal/seal" | 37 | "gitbay.org/gitbay/internal/seal" |
| 37 | "gitbay.org/gitbay/internal/sshd" | 38 | "gitbay.org/gitbay/internal/sshd" |
| @@ -228,11 +229,14 @@ func serveCmd() *cobra.Command { | |||
| 228 | return control.RepoDir(cfg.Server.Root, owner, name) | 229 | return control.RepoDir(cfg.Server.Root, owner, name) |
| 229 | }, buildinfo.String()).Run(whCtx) | 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 | errCh := make(chan error, 3) | 235 | errCh := make(chan error, 3) |
| 232 | var sshSrv *sshd.Server | 236 | var sshSrv *sshd.Server |
| 233 | var sshLn, gitLn net.Listener | 237 | var sshLn, gitLn net.Listener |
| 234 | if cfg.SSH.Mode == "embedded" { | 238 | if cfg.SSH.Mode == "embedded" { |
| 235 | srv, err := sshd.New(cfg, st, nil) | 239 | srv, err := sshd.New(cfg, st, packs) |
| 236 | if err != nil { | 240 | if err != nil { |
| 237 | return err | 241 | return err |
| 238 | } | 242 | } |
| @@ -249,7 +253,7 @@ func serveCmd() *cobra.Command { | |||
| 249 | slog.Info("ssh handled by host sshd (ssh.mode = system)") | 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 | // Header and idle timeouts bound what an idle or slow client can | 257 | // Header and idle timeouts bound what an idle or slow client can |
| 254 | // hold open. No write timeout: archives and upload-pack stream | 258 | // hold open. No write timeout: archives and upload-pack stream |
| 255 | // for as long as they take (#104). | 259 | // for as long as they take (#104). |
| @@ -338,7 +342,7 @@ func serveCmd() *cobra.Command { | |||
| 338 | } | 342 | } |
| 339 | slog.Info("git-daemon listening", "addr", gln.Addr()) | 343 | slog.Info("git-daemon listening", "addr", gln.Addr()) |
| 340 | gitLn = gln | 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 | select { | 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" | ||