Commit db01bf4fad

db01bf4fad6b7c82e9d33cdd2cb46dabe4cadf02

parent: d4ea40c36c

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-29 16:45 UTC

wiki, changelog: push limit and repo download under the pack limit

Closes #308

Layout: unified · split

.gitbay/wiki/Admin.org +40 −3
@@ -357,8 +357,8 @@ push=.
357 repositories may take; a push may be no larger than what is left. 357 repositories may take; a push may be no larger than what is left.
358- =pack_concurrency= (3), =pack_per_principal= (2), =pack_queue= (32), 358- =pack_concurrency= (3), =pack_per_principal= (2), =pack_queue= (32),
359 =pack_queue_wait= (="60s"=) — git pack generation (clones, fetches, 359 =pack_queue_wait= (="60s"=) — git pack generation (clones, fetches,
360 =git archive --remote=, web archive downloads) over SSH, smart HTTP 360 =git archive --remote=, web archive downloads, =repo download= over
361 and git:// shares one 361 SSH and the API) over SSH, smart HTTP and git:// shares one
362 budget: this many at once, this many per account (per client 362 budget: this many at once, this many per account (per client
363 address when anonymous: an IPv4 address, or an IPv6 /64), and this 363 address when anonymous: an IPv4 address, or an IPv6 /64), and this
364 many waiting for at most the wait. Anonymous clients together hold 364 many waiting for at most the wait. Anonymous clients together hold
@@ -375,11 +375,48 @@ push=.
375 for two minutes: a client reading below about 550 B/s, or an HTTP 375 for two minutes: a client reading below about 550 B/s, or an HTTP
376 request body that takes over two minutes with nothing written back, 376 request body that takes over two minutes with nothing written back,
377 is cut. Ref listings (info/refs, protocol v2 377 is cut. Ref listings (info/refs, protocol v2
378 =ls-refs=), pushes and =repo download= are outside the budget. For the 378 =ls-refs=) and pushes are outside the budget. A busy =repo download=
379 exits 1 with the same message; over the API it is a 503 with
380 =Retry-After: 60=. For the
379 three counts 0 means the default and a negative value turns that 381 three counts 0 means the default and a negative value turns that
380 bound off. The defaults suit a four-core host; see [[Performance]]. 382 bound off. The defaults suit a four-core host; see [[Performance]].
381 With =ssh.mode = "system"= each SSH session is its own process and 383 With =ssh.mode = "system"= each SSH session is its own process and
382 SSH clones are not counted. 384 SSH clones are not counted.
385- =push_concurrency= (2), =push_per_principal= (1), =push_queue= (16),
386 =push_queue_wait= (="60s"=) — =git receive-pack= over SSH, on a
387 budget of its own so clones cannot starve pushes or the reverse:
388 this many at once, this many per account or deploy key, and this
389 many waiting for at most the wait. One principal may have up to half
390 of =push_queue= (at least one) waiting, so parallel pushes from one
391 account queue rather than being refused, but cannot fill the queue.
392 A deploy key is its own principal, apart from the account that
393 registered it, so an account with deploy keys can hold
394 =push_per_principal= slots per key;
395 the global cap still holds. A bot account such as a runner's counts
396 like any other account. The slot is taken after the access checks and held
397 until receive-pack exits, which is after =post-receive= (merge
398 detection, CI queueing, webhooks) has run. Past that the client gets
399 "the server is busy: it is at its limit of concurrent pushes…" and
400 exit 1, and the daemon logs a =push limit= warning at most once a
401 minute. A client that disconnects while queued leaves the queue; one
402 that disconnects mid-push ends receive-pack and frees its slot, and
403 a revoked key kills it. 0 and negative values read as for =pack_*=.
404 HTTP and git:// carry no pushes. With =ssh.mode = "system"= pushes
405 are not counted and the two timeouts below do not apply.
406- =push_idle= (="60s"=), =push_receive_timeout= (="15m"=) — a push
407 holding a slot is killed, its process group with it, when its
408 pre-receive has not started =push_receive_timeout= after it took the
409 slot, or when no byte has been read from the client and none written
410 to it for =push_idle=. The idle rule starts when the pack signature
411 arrives from the client or pre-receive starts, whichever is first
412 (a delete-only push sends no pack): before that the client may be
413 silent while its =pack-objects= counts and compresses, and only
414 =push_receive_timeout= applies. receive-pack runs with
415 =receive.keepAlive= set to a quarter of =push_idle= (1 to 5 seconds),
416 so the keepalives it sends to side-band clients while it indexes and
417 runs hooks keep a working push alive. Once pre-receive has started
418 only =push_idle= applies: post-receive is never cut short by the
419 clock. A push killed before pre-receive answers updates no refs.
383- =max_pack_bytes= (2 GiB) — the largest pack one push may send, 420- =max_pack_bytes= (2 GiB) — the largest pack one push may send,
384 enforced as =receive.maxInputSize= and lowered to what an owner's 421 enforced as =receive.maxInputSize= and lowered to what an owner's
385 storage quota has left. 422 storage quota has left.
.gitbay/wiki/Architecture/09-Controls.org +2 −1
@@ -97,7 +97,8 @@ chapter names of OWASP ASVS 4.0 where one fits.
97| Control | Status | Evidence | 97| Control | Status | Evidence |
98|---------------------------------------------+----------+------------------------------------------------------------------| 98|---------------------------------------------+----------+------------------------------------------------------------------|
99| Rate limits on API and writes | in place | [[file:05-Identity-and-Access.org][5. Rate limits]] | 99| Rate limits on API and writes | in place | [[file:05-Identity-and-Access.org][5. Rate limits]] |
100| 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| Concurrency limit on git pack generation | in place | global, per-principal, bounded queue across SSH, HTTP and git://, =repo download= included (=internal/packlimit=); not in system SSH mode |
101| Concurrency limit on pushes | in place | receive-pack on its own global, per-principal, bounded-queue budget; a deploy key is its own principal; killed at =push_receive_timeout= before pre-receive, or after =push_idle= with nothing moving once the pack has begun (=internal/sshd/sshd.go=, =internal/packlimit=, =internal/hookd=); not in system SSH mode |
101| Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) | 102| Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) |
102| Backups offsite and append-only | in place | restic with append-only credentials (documented) | 103| Backups offsite and append-only | in place | restic with append-only credentials (documented) |
103| Restore tested | partial | drill 2026-09-29 from the offsite copy (Admin wiki "Restore drill"); secrets not checked in that drill; the off-host =secret.key= exists (#305) and is checked in the next | 104| Restore tested | partial | drill 2026-09-29 from the offsite copy (Admin wiki "Restore drill"); secrets not checked in that drill; the off-host =secret.key= exists (#305) and is checked in the next |
.gitbay/wiki/Architecture/10-Known-Gaps.org +2 −4
@@ -16,10 +16,8 @@ what the 2026-09-27 review found; remove a row when its issue closes.
16| Area | Gap | Severity | 16| Area | Gap | Severity |
17|-------+-------------------------------------------------------------------------------------------------------------+----------| 17|-------+-------------------------------------------------------------------------------------------------------------+----------|
18| Audit | The hash chain is unkeyed, so whoever can write the database can edit a row and recompute every later hash; removing the newest audit rows, or writing new rows under their freed ids, needs no recomputing at all. Neither is 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 | 18| Audit | The hash chain is unkeyed, so whoever can write the database can edit a row and recompute every later hash; removing the newest audit rows, or writing new rows under their freed ids, needs no recomputing at all. Neither is 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 |
19| Access | Grants and parked profile about texts (=profile_about_backfill=) of deleted accounts and organizations, and deploy keys of deleted repositories, left by deletes before #306, are removed on upgrade and the "schema migrated" log line gives the counts. One whose id a later row had already taken is no longer an orphan and stays with that row; on gitbay.org the orphans found (two grants, one deploy key) name ids no later row had taken | low | 19| Availability | Under =ssh.mode = "system"= each SSH session is a separate =gitbayd shell= process, so the pack and push limits (=internal/packlimit=, #262, #308) cannot count SSH clones or pushes across sessions; only HTTP and git:// share a budget there | low |
20| 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 | 20| Availability | A push holds its slot for at most =push_receive_timeout= (15m) before its pre-receive starts, and, once its pack has begun, at most =push_idle= (60s) with nothing moving either way. A client that trickles its pack and reconnects when cut can hold a slot per principal for up to the receive deadline each time; deploy keys multiply the per-principal share, bounded by =push_concurrency= | low |
21| Availability | Pushes have no concurrency limit; =max_pack_bytes= bounds each one, not how many run at once | medium |
22| Availability | =repo download= (SSH, API) runs =git archive= outside the pack limit; only its two-minute deadline and 512 MiB cap bound it | low |
23| Availability | An HTTP or git:// client that disconnects while queued for a pack slot keeps its place until =pack_queue_wait= runs out; only SSH notices the disconnect | low | 21| Availability | An HTTP or git:// client that disconnects while queued for a pack slot keeps its place until =pack_queue_wait= runs out; only SSH notices the disconnect | low |
24 22
25* Questions an auditor will ask that have no answer yet 23* Questions an auditor will ask that have no answer yet
.gitbay/wiki/Performance.org +5 −1
@@ -47,7 +47,11 @@ The practical ceiling on this hardware is concurrent pack generation:
47full clones of large repositories are CPU-bound in git itself (the 17s 47full clones of large repositories are CPU-bound in git itself (the 17s
48clone ran git at ~156% CPU). =limits.pack_concurrency= bounds how many 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 49run at once across SSH, HTTP and git://, with a queue behind it (see
50[[Admin]], =[limits]=). 50[[Admin]], =[limits]=). Pushes are the other git-bound load: receive-pack
51indexes the pack it is sent, about a core for a large one, and then runs
52its hooks. =limits.push_concurrency= (2) bounds those on a separate
53budget, one per account or deploy key by default, so a clone storm and
54a push storm each leave the other its slots.
51 55
52* Concurrent clones 56* Concurrent clones
53 57
.gitbay/wiki/Threat-Model.org +4 −3
@@ -367,9 +367,10 @@ is recorded here rather than in a closed issue:
367- *Timing and traffic analysis.* Token comparison is a hash index lookup 367- *Timing and traffic analysis.* Token comparison is a hash index lookup
368 by design, but nothing has been measured. 368 by design, but nothing has been measured.
369- *Denial of service by resource exhaustion* beyond rate. Concurrent 369- *Denial of service by resource exhaustion* beyond rate. Concurrent
370 clones, fetches and web archives are bounded by the pack limit 370 clones, fetches, web archives and =repo download= are bounded by the
371 (#262); pushes are not, beyond =max_pack_bytes= on each one. Nor are 371 pack limit (#262, #308), and pushes by a separate push limit (#308)
372 pathological diffs, deep histories, or zip bombs in LFS. 372 on top of =max_pack_bytes= on each one. Pathological diffs, deep
373 histories and zip bombs in LFS are not.
373 374
374A sweep is a point in time. This section says what a reader should not 375A sweep is a point in time. This section says what a reader should not
375assume has been checked. 376assume has been checked.
CHANGELOG.org +22
@@ -4,6 +4,28 @@ Versioning follows semver from v0.1.0. Database migrations run
4automatically on daemon start; upgrade notes appear per release when 4automatically on daemon start; upgrade notes appear per release when
5anything beyond "replace the binary and restart" is needed. 5anything beyond "replace the binary and restart" is needed.
6 6
7* Unreleased
8
9- Pushes have a concurrency limit of their own, separate from the pack
10 budget: =[limits] push_concurrency= (2), =push_per_principal= (1),
11 =push_queue= (16) and =push_queue_wait= ("60s"), read like the
12 =pack_*= settings. One principal may have up to half of =push_queue=
13 waiting, so parallel pushes from one account queue. A deploy key
14 counts apart from the account that registered it. The slot is held until receive-pack and its
15 post-receive hook have finished; a busy push exits 1 with "the server
16 is busy: it is at its limit of concurrent pushes…", and the daemon
17 logs a =push limit= warning at most once a minute. (#308)
18- A push holding a slot is killed when its pre-receive has not started
19 =[limits] push_receive_timeout= ("15m") after it took the slot, or,
20 once its pack has begun or pre-receive has started, when nothing has
21 moved either way for =push_idle= ("60s").
22 receive-pack runs with =receive.keepAlive= set from =push_idle=, so
23 long hooks do not trip it. (#308)
24- =repo download= over SSH and the API takes a slot from the pack
25 limit, released once =git archive= exits; busy is exit 1 with the
26 clone message, and 503 with =Retry-After: 60= on both API
27 endpoints. (#308)
28
7* v1.40.0 — 2026-09-29 29* v1.40.0 — 2026-09-29
8 30
9DKIM verification for reply mail, and ids that are not reused after a 31DKIM verification for reply mail, and ids that are not reused after a