* feat(node): owner-only push enforcement behind GITLAWB_ENFORCE_OWNER_PUSH (#31)
A valid did:key signature on git-receive-pack is authentication, not
authorization: any party can generate a key, derive its DID, sign, and push to
an unprotected branch. Owner checks only ran on protected branches, so a
non-owner could write to unprotected branches of an arbitrary repo.
Add an opt-in flag GITLAWB_ENFORCE_OWNER_PUSH (default false, mirroring
GITLAWB_REQUIRE_SIGNED_PEER_WRITES) that requires the authenticated pusher to be
the repo owner on every branch. Default-off keeps live nodes unaffected.
Design follows the maintainer's review on #31:
- Authorize on the canonical AuthenticatedDid injected by require_signature, via
Extension<AuthenticatedDid> on the handler — not a re-parse of the headers.
The legacy branch-protection check is left as-is.
- Fail closed: an absent identity or a non-owner caller is rejected. The policy
lives in a pure fn (owner_push_rejection) so it is unit-tested without a DB.
- The gate runs before branch protection, so one rejection yields one error body.
- Factor the duplicated owner-match into auth::is_repo_owner, reused by
receive-pack, protect.rs, and visibility.rs (each keeps its own message).
events.rs / peers.rs are left out — they aren't owner checks.
- Name the push gate by intent (caller_authorized_to_push) so the planned
Phase 2 UCAN git/push capability is a pure addition, not a rewrite.
Tests: owner (full + short DID) allowed, non-owner rejected, missing-DID
rejected, flag-off legacy. Docs: .env.example + docs/RUN-A-NODE.md.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* fix(node): address review on #68 — 403 status, finish owner-match consolidation
Per @beardthelion's review:
- Return 403 Forbidden (not 400) for authorization denials: the owner-push gate,
the branch-protection denial, and the owner checks in protect.rs / visibility.rs.
400 reads as a malformed request and some git/CI clients retry it.
- Finish the is_repo_owner consolidation: route the branch-protection check and
the repos list filter through the helper. The branch-protection check now uses
the verified AuthenticatedDid (auth.0) instead of re-parsing the headers.
- Drop the second identity source on the push path entirely: the post-push event
recording also uses auth.0, so extract_did_from_auth (and the handler's
HeaderMap param + import) are removed.
- Tests assert the Forbidden variant (so a future status regression fails loudly)
and add direct coverage for is_repo_owner / caller_authorized_to_push.
- Docs: warn that enabling the flag blocks delegated and CI agents until the
Phase 2 UCAN git/push capability is honored.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: FrozenRaspberry <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
* refactor: move fly.toml into infra/fly/
Deployment configs now live under infra/, organized per target.
Dockerfiles and docker-compose.yml intentionally stay at the repo
root (shared by CI, build scripts, and the macOS app).
Deploy with: fly deploy -c infra/fly/fly.toml
Co-Authored-By: OpenClaude <[email protected]>
* feat(infra): Terraform for single-EC2 AWS deployment
infra/aws/ provisions a production-leaning single-node deployment:
- t4g.small (AL2023 arm64) running the published ghcr.io/gitlawb/node
image + postgres:16 via docker compose
- separate encrypted EBS data volume (prevent_destroy) with daily DLM
snapshots; survives instance replacement
- postgres password via random_password -> SSM SecureString, fetched at
boot by instance profile (never in user-data); optional secrets
(operator key, Pinata JWT, S3 secret) follow the same path
- SSM Session Manager access (no SSH by default), IMDSv2 required,
metrics port closed unless explicitly opened
- SSM command document for image upgrades (user-data runs once)
Verified: terraform fmt/init/validate clean; rendered compose passes
docker compose config; rendered user-data passes bash -n.
Co-Authored-By: OpenClaude <[email protected]>
* fix(infra): address CodeRabbit review feedback
- api_url output now honors the public_url override (local.public_url)
- disable xtrace around SSM secret fetch + .env write so secret values
never reach the bootstrap log
- add retry/timeout flags to the docker compose download
- validate snapshot_retain_count (1-1000) at plan time
- add language tags to fenced tree blocks (MD040)
Co-Authored-By: OpenClaude <[email protected]>
* fix(infra): address PR #27 review feedback
- security group now derives its VPC from the selected subnet, so
subnet_id overrides into non-default VPCs work
- s3_access_key_id moves to SSM SecureString (same path as the secret
key) instead of being embedded in user-data and state
- ignore user_data drift on the instance: it only runs at first boot,
so re-rendering caused pointless stop/starts; README documents the
-replace workflow for config changes
- DLM snapshot targeting is stack-specific (Snapshot + Name tags), not
any Snapshot=true volume in the account
- comment in fly.toml that it targets the shared test instance
- user-data compose up gains --remove-orphans (consistent with the
upgrade SSM document)
- node service gets an HTTP /health healthcheck in compose
Co-Authored-By: OpenClaude <[email protected]>
* feat(infra): optional customer-managed KMS key for SSM secrets
Add ssm_kms_key_id (default null = AWS-managed aws/ssm key). When set,
all five SecureString parameters are encrypted with the CMK and the
instance role gains kms:Decrypt scoped to that key — without it the
boot-time secret fetch would fail.
Co-Authored-By: OpenClaude <[email protected]>
* fix(infra): scope CMK kms:Decrypt to this stack's SSM parameters
The grant on the customer-managed key was unconditioned, allowing the
instance role to decrypt any ciphertext under the same CMK. Restrict it
with kms:ViaService = ssm.<region>.amazonaws.com and
kms:EncryptionContext:PARAMETER_ARN limited to the stack's parameter
ARNs.
Co-Authored-By: OpenClaude <[email protected]>
---------
Co-authored-by: OpenClaude <[email protected]>