Background
The README ("Known limitations") and docs/OSS-READINESS-AUDIT.md both note that push is authenticated but not authorized:
"Push authorization is still not capability-complete. A valid DID signature is authentication, not authorization; unprotected repo branches do not yet enforce owner/UCAN capability checks."
docs/MAINTAINER-ROADMAP.md lists this under Security hardening as the first item. This issue proposes a concrete, non-breaking first slice and offers to implement it.
Current behavior
For POST /{owner}/{repo}/git-receive-pack:
require_signature (auth/mod.rs) verifies an RFC 9421 Ed25519 signature and injects AuthenticatedDid.
- In
git_receive_pack (api/repos.rs), an owner check only runs when the target branch is protected (is_branch_protected). Pushes to unprotected branches are accepted from any authenticated signer.
Because did:key is self-certifying, "authenticated" only proves the caller controls the key for the DID they present — any party can generate a keypair, derive its did:key, sign, and push. There is no allow-list and no registration requirement on this path. Net effect: a non-owner can write to unprotected branches of an arbitrary repo.
protect and visibility endpoints already gate on the owner via a require_owner-style check, so the idiom exists; it's just not applied to the general push path.
Proposal — Phase 1 (this work)
Follow the project's established "opt-in hardening flag before mandatory" pattern (cf. GITLAWB_REQUIRE_SIGNED_PEER_WRITES):
- Add
GITLAWB_ENFORCE_OWNER_PUSH (default false -> current behavior, no break to live/test nodes).
- When
true: in git_receive_pack, require the authenticated DID to equal the repo owner_did (matching the existing full-DID-or-short-form comparison used in branch protection/visibility) before any ref update is applied; reject unsigned/non-owner pushes with a clear error.
- Factor the repeated owner-match logic into one small shared helper (currently duplicated across branch protection,
protect, and visibility).
- Tests: first coverage for the push authorization path (owner allowed, non-owner rejected, flag off = legacy behavior).
- Docs:
.env.example + a line in docs/RUN-A-NODE.md.
Out of scope (proposed Phase 2)
- Collaborator write lists and UCAN
git/push capability delegation (the caps::GIT_PUSH type already exists; the UCAN chain is validated structurally in require_ucan_chain but capabilities aren't yet checked against the operation). This would let non-owners be granted scoped push rights — the natural escape hatch once owner-only is the default.
- Making enforcement the default after a compatibility window.
Related (not asserted, worth a look)
pulls/{n}/merge and hooks are write endpoints on the same auth layer; they may warrant the same owner/capability gating. Happy to audit separately to keep this PR focused.
Offer
I'd like to implement Phase 1 if the approach (opt-in flag, owner-only, helper extraction) looks right to you. Open to a different flag name / default / error semantics — flagging the design here first per CONTRIBUTING before sending a PR.
Background
The README ("Known limitations") and
docs/OSS-READINESS-AUDIT.mdboth note that push is authenticated but not authorized:docs/MAINTAINER-ROADMAP.mdlists this under Security hardening as the first item. This issue proposes a concrete, non-breaking first slice and offers to implement it.Current behavior
For
POST /{owner}/{repo}/git-receive-pack:require_signature(auth/mod.rs) verifies an RFC 9421 Ed25519 signature and injectsAuthenticatedDid.git_receive_pack(api/repos.rs), an owner check only runs when the target branch is protected (is_branch_protected). Pushes to unprotected branches are accepted from any authenticated signer.Because
did:keyis self-certifying, "authenticated" only proves the caller controls the key for the DID they present — any party can generate a keypair, derive itsdid:key, sign, and push. There is no allow-list and no registration requirement on this path. Net effect: a non-owner can write to unprotected branches of an arbitrary repo.protectandvisibilityendpoints already gate on the owner via arequire_owner-style check, so the idiom exists; it's just not applied to the general push path.Proposal — Phase 1 (this work)
Follow the project's established "opt-in hardening flag before mandatory" pattern (cf.
GITLAWB_REQUIRE_SIGNED_PEER_WRITES):GITLAWB_ENFORCE_OWNER_PUSH(defaultfalse-> current behavior, no break to live/test nodes).true: ingit_receive_pack, require the authenticated DID to equal the repoowner_did(matching the existing full-DID-or-short-form comparison used in branch protection/visibility) before any ref update is applied; reject unsigned/non-owner pushes with a clear error.protect, andvisibility)..env.example+ a line indocs/RUN-A-NODE.md.Out of scope (proposed Phase 2)
git/pushcapability delegation (thecaps::GIT_PUSHtype already exists; the UCAN chain is validated structurally inrequire_ucan_chainbut capabilities aren't yet checked against the operation). This would let non-owners be granted scoped push rights — the natural escape hatch once owner-only is the default.Related (not asserted, worth a look)
pulls/{n}/mergeandhooksare write endpoints on the same auth layer; they may warrant the same owner/capability gating. Happy to audit separately to keep this PR focused.Offer
I'd like to implement Phase 1 if the approach (opt-in flag, owner-only, helper extraction) looks right to you. Open to a different flag name / default / error semantics — flagging the design here first per CONTRIBUTING before sending a PR.