Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dfe02c3
test(node): add HTTP-signature replay guards (#253)
Jul 27, 2026
dc1c576
refactor(core): derive the Signature-Input component list from COVERE…
Jul 27, 2026
01040e6
fix(core): make the signature acceptance window one-sided (#253)
Jul 27, 2026
f8050d9
feat(core): sign a per-request nonce into Signature-Input (#253)
Jul 27, 2026
ac62460
feat(node): add the spent-signature ledger schema and accessor (#253)
Jul 27, 2026
105f597
feat(node): publish the verified signature identity for ledger keying…
Jul 27, 2026
138b634
fix(node): spend a verified signature once on mutation routes (#253)
Jul 27, 2026
421e565
feat(node): add a staged GITLAWB_REQUIRE_SIGNATURE_NONCE flag (#253)
Jul 27, 2026
700b9d9
fix(gl): fail loudly on a spent-signature rejection instead of return…
Jul 27, 2026
a6ac5fa
fix(node): renumber the ledger migration and fail loudly on a version…
Jul 27, 2026
9c40408
fix(node): brake the signed write routes per IP, and correct three cl…
Jul 27, 2026
b790332
fix(gl): surface every signature denial, and stop trusting node prose…
Jul 27, 2026
dbc0719
fix(core): reject a reversed-parenthesis Signature-Input instead of p…
Jul 27, 2026
5082184
fix(node): validate the nonce, key the cap on the resolved key, widen…
Jul 27, 2026
15954b1
fix(node): retry a peer's retryable sync-notify rejection, and log a …
Jul 27, 2026
526955c
feat(node): count every spent-signature ledger outcome (#253)
Jul 27, 2026
64055d9
fix(core,node,gl): make a forgotten denial code a compile error (#253)
Jul 27, 2026
97a1d18
fix(node): tell the operator the truth when a migration version colli…
Jul 27, 2026
2f4995b
fix(node,gl): give the flood brake a code, and gate the retry on it (…
Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ GITLAWB_REQUIRE_SIGNED_PEER_WRITES=false
# Keep false until the repo owner is ready for owner-only writes.
GITLAWB_ENFORCE_OWNER_PUSH=false

# Require a `nonce` parameter on signatures that authorize a write. Without one
# the replay ledger keys on the signing-string hash, which rejects a repeated
# byte-identical mutation inside the same second. Keep false until every client
# signs a nonce; with this and GITLAWB_REQUIRE_SIGNED_PEER_WRITES both true, a
# peer still on a pre-nonce binary can no longer write to this node.
GITLAWB_REQUIRE_SIGNATURE_NONCE=false

# Comma-separated libp2p multiaddrs.
# Example: /ip4/1.2.3.4/udp/7546/quic-v1/p2p/12D3KooW...
GITLAWB_P2P_BOOTSTRAP=
Expand Down Expand Up @@ -127,6 +134,17 @@ GITLAWB_PUSH_RATE_LIMIT=600
# the client IP. 0 disables. Default 120.
GITLAWB_CREATE_RATE_LIMIT=120

# ── Signed-write rate limiting (per client IP, uses GITLAWB_TRUSTED_PROXY) ────
# Max requests per client IP per hour on the write routes that require an HTTP
# Signature (tasks, PR merge/close/review/comment, webhooks, branch protection,
# stars, replicas, labels, visibility, agent deregistration, bounties, profile,
# issue close/comment). Every signed attempt spends a consumed_signatures row
# before the handler checks ownership or existence, and any freshly minted
# did:key can sign, so this brake is what bounds the durable writes an
# unregistered caller can force. Separate bucket from the creation, push and
# peer-sync limits. 0 disables. Default 600.
GITLAWB_SIGNED_WRITE_RATE_LIMIT=600

# ── Peer-sync rate limiting (per client IP, uses GITLAWB_TRUSTED_PROXY below) ─
# /api/v1/peers/announce and /api/v1/sync/notify accept unsigned requests from
# known peers and run at higher frequency, so a generous bucket. Separate from
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@ GITLAWB_REQUIRE_SIGNED_PEER_WRITES=true

`POST /api/v1/sync/trigger` is not part of the staged rollout: it always requires a signature in both config modes and returns 401 without one, because each call drives an O(peers) outbound fan-out.

A verified signature is spent once on the write routes that require one: repo, issue, PR, task, bounty, profile, webhook, label, star, replica, visibility and agent mutations, plus git push and `POST /api/v1/sync/trigger`. A captured request to any of those cannot be replayed. The two peer-write routes are the exception. `POST /api/v1/peers/announce` and `POST /api/v1/sync/notify` reach the ledger only when `GITLAWB_REQUIRE_SIGNED_PEER_WRITES=true`; under the default (`false`) they verify a signature when the headers are present but never spend it, so a captured signed peer write is still replayable there until the fleet upgrades and the flag is turned on. The ledger keys on the RFC 9421 `nonce` parameter when the client signs one and falls back to the signing-string hash when it does not. The fallback is correct but coarser: a client that repeats a byte-identical mutation within the same second is rejected as a replay and has to re-sign. `GITLAWB_REQUIRE_SIGNATURE_NONCE` closes that fallback once every client emits a nonce:

```bash
GITLAWB_REQUIRE_SIGNATURE_NONCE=true
```

With it on, a nonce-less signature on a write route is rejected with 400 and `X-Gitlawb-Error: signature_nonce_required`. Signed reads are never affected. Flipping it on has a federation precondition: three of the node's own outbound signed calls (peer announce, replica registration, sync-notify) are peer traffic, so a node running with both this and `GITLAWB_REQUIRE_SIGNED_PEER_WRITES` set to true will reject peer writes from any node still on a pre-nonce binary. Upgrade the fleet before enabling it.

---

## Configuration
Expand All @@ -341,6 +349,8 @@ Important node settings:
| `GITLAWB_P2P_BOOTSTRAP` | Comma-separated libp2p multiaddrs. |
| `GITLAWB_BOOTSTRAP_DISABLE_SEEDS` | Disable embedded seed peers for isolated dev/test networks. |
| `GITLAWB_REQUIRE_SIGNED_PEER_WRITES` | Require signed peer announce/sync writes. |
| `GITLAWB_REQUIRE_SIGNATURE_NONCE` | Require a signed `nonce` on write routes. Default false; see the staged rollout note above. |
| `GITLAWB_SIGNED_WRITE_RATE_LIMIT` | Per-client-IP requests per hour on the signed write routes (tasks, PR merge/close/review/comment, webhooks, branch protection, stars, replicas, labels, visibility, agent deregistration, bounties, profile, issue close/comment). Each signed attempt spends a ledger row before the handler checks anything, so the brake bounds what an unregistered caller can write. Own bucket; `0` disables. Default 600. |
| `GITLAWB_AUTO_SYNC` | Enable automatic sync from known peers. |
| `GITLAWB_MAX_PACK_BYTES` | Max git pack body size for smart-HTTP routes. |
| `GITLAWB_GIT_SERVICE_TIMEOUT_SECS` | Max seconds a served git upload-pack/receive-pack may run before it is aborted (504). Default 600. Does not bound `info/refs` or the withheld-blob path. |
Expand Down
Loading
Loading