Commit db01bf4fad
Verified · cmc ci/build: success ci/test: success
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: | |||
| 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). =limits.pack_concurrency= bounds how many | 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 | 49 | run 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 |
| 51 | indexes the pack it is sent, about a core for a large one, and then runs | ||
| 52 | its hooks. =limits.push_concurrency= (2) bounds those on a separate | ||
| 53 | budget, one per account or deploy key by default, so a clone storm and | ||
| 54 | a 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 | ||
| 374 | A sweep is a point in time. This section says what a reader should not | 375 | A sweep is a point in time. This section says what a reader should not |
| 375 | assume has been checked. | 376 | assume has been checked. |
CHANGELOG.org +22
| @@ -4,6 +4,28 @@ Versioning follows semver from v0.1.0. Database migrations run | |||
| 4 | automatically on daemon start; upgrade notes appear per release when | 4 | automatically on daemon start; upgrade notes appear per release when |
| 5 | anything beyond "replace the binary and restart" is needed. | 5 | anything 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 | ||
| 9 | DKIM verification for reply mail, and ids that are not reused after a | 31 | DKIM verification for reply mail, and ids that are not reused after a |