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=.
357357 repositories may take; a push may be no larger than what is left.
358358- =pack_concurrency= (3), =pack_per_principal= (2), =pack_queue= (32),
359359 =pack_queue_wait= (="60s"=) — git pack generation (clones, fetches,
360 =git archive --remote=, web archive downloads) over SSH, smart HTTP
361 and git:// shares one
360 =git archive --remote=, web archive downloads, =repo download= over
361 SSH and the API) over SSH, smart HTTP and git:// shares one
362362 budget: this many at once, this many per account (per client
363363 address when anonymous: an IPv4 address, or an IPv6 /64), and this
364364 many waiting for at most the wait. Anonymous clients together hold
@@ -375,11 +375,48 @@ push=.
375375 for two minutes: a client reading below about 550 B/s, or an HTTP
376376 request body that takes over two minutes with nothing written back,
377377 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
379381 three counts 0 means the default and a negative value turns that
380382 bound off. The defaults suit a four-core host; see [[Performance]].
381383 With =ssh.mode = "system"= each SSH session is its own process and
382384 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.
383420- =max_pack_bytes= (2 GiB) — the largest pack one push may send,
384421 enforced as =receive.maxInputSize= and lowered to what an owner's
385422 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.
9797| Control | Status | Evidence |
9898|---------------------------------------------+----------+------------------------------------------------------------------|
9999| 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 |
101102| Service hardening | in place | systemd sandboxing ([[file:03-Deployment.org][3]]) |
102103| Backups offsite and append-only | in place | restic with append-only credentials (documented) |
103104| 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.
1616| Area | Gap | Severity |
1717|-------+-------------------------------------------------------------------------------------------------------------+----------|
1818| 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 |
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 |
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 |
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 | 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 |
2321| 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 |
2422
2523* 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:
4747full clones of large repositories are CPU-bound in git itself (the 17s
4848clone ran git at ~156% CPU). =limits.pack_concurrency= bounds how many
4949run 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.
5155
5256* Concurrent clones
5357
.gitbay/wiki/Threat-Model.org +4 −3
@@ -367,9 +367,10 @@ is recorded here rather than in a closed issue:
367367- *Timing and traffic analysis.* Token comparison is a hash index lookup
368368 by design, but nothing has been measured.
369369- *Denial of service by resource exhaustion* beyond rate. Concurrent
370 clones, fetches and web archives are bounded by the pack limit
371 (#262); pushes are not, beyond =max_pack_bytes= on each one. Nor are
372 pathological diffs, deep histories, or zip bombs in LFS.
370 clones, fetches, web archives and =repo download= are bounded by the
371 pack limit (#262, #308), and pushes by a separate push limit (#308)
372 on top of =max_pack_bytes= on each one. Pathological diffs, deep
373 histories and zip bombs in LFS are not.
373374
374375A sweep is a point in time. This section says what a reader should not
375376assume has been checked.
CHANGELOG.org +22
@@ -4,6 +4,28 @@ Versioning follows semver from v0.1.0. Database migrations run
44automatically on daemon start; upgrade notes appear per release when
55anything beyond "replace the binary and restart" is needed.
66
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
729* v1.40.0 — 2026-09-29
830
931DKIM verification for reply mail, and ids that are not reused after a