romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 17m ago
doc/security.md
# Security model | area | state | |---------------------|---------------------------------------------------| | path traversal | component whitelist + realpath confinement | | argument injection | argv exec, `-`-leading and `..` refs rejected | | env injection | child env rebuilt from scratch | | push abuse | Basic auth + per-repo ACL + `update` hook | | brute force | per-IP exponential backoff + ban (ratelimit.c) | | connection flood | cap of 128 concurrent connections -> 503 | | framing/sniffing | CSP `default-src 'none'`, XFO DENY, nosniff | | cross-site POST | session CSRF token + `Origin` host check | | history rewrite | non-fast-forward rejected on the default branch | | CSRF | session-bound token on every state-changing form | | merge races | `update-ref` compare-and-swap on the old tip | | passwords | scrypt RFC 7914 (N=2^15, r=8, p=1, ~32 MiB), | | | in-tree impl + `kdf-selftest`; pbkdf2$ legacy | | | rows verify and rehash on next login | | tokens | random 256-bit, stored as SHA-256 digest — a | | | memory-hard KDF would be wrong here: tokens are | | | unguessable; hashing only guards DB-read leaks | | | (and a slow KDF on every request is a DoS vector) | | sessions | random 256-bit cookie, HttpOnly, 30d expiry | | resource exhaustion | socket timeouts + rlimits on git children | | request smuggling | chunked bodies dechunked eagerly, trailers | | | drained, framing-capped by GIT_TRACE_BODY_MAX | | multipart uploads | positional parser, bounded boundary, ASan-fuzzed | The whole input surface is fuzzed under ASan/UBSan (`make fuzz`) — the campaign already caught real parser bugs, and a concurrent web-commit race hammer verifies the `update-ref` CAS under load. ## Access model | repo state | anonymous | authenticated, no ACL | ACL `r`/`w`/`a` or owner | |-----------------------|--------------------|-----------------------|--------------------------| | public (is_public=1) | pull | pull | per role | | private (is_public=0) | nothing (404/401) | nothing (403) | per role | | not in DB | pull (legacy M1) | pull | — | - Push requires HTTP Basic auth (password **or** token) plus rank ≥ `w`. - `401 WWW-Authenticate` makes git clients prompt for credentials. - The `update` hook embedded in every repo adds per-ref policy on top of HTTP-level ACLs (`REMOTE_USER` is propagated into hook environment). - Ownerless `_/` repos are platform territory: site admins hold implicit `a` on them (and only on them — a global admin still gets 404 on other users' private repos). `repo_perm` fails *closed*: a DB error yields `n`, never `r`. ## Branch protection `branch_rules` turns merge requests and patch sets from courtesy into the enforced path. Each rule is a glob on the branch name (`master`, `release/*`, `?`/`*` wildcards) with three switches: - **push requires** — minimum repo rank (`w` or `a`) for direct pushes; everyone else is told to use a merge request or patch set - **ff-only** — rejects non-fast-forward updates on matching branches - **no-delete** — rejects branch deletion over the wire Rules are managed on the `/settings` tab (rank ≥ `a`) and enforced by the `update` hook via `git-trace branch-check`, which resolves the pusher from `REMOTE_USER` and maps them through the same `repo_perm` lattice as the web and the git transports. Rules only ever tighten access: the built-in floor (default branch can't be deleted, no non-ff to the default branch) always applies. Local filesystem pushes bypass rules, same posture as quota. The hook script is baked into each repo at creation; after upgrading git-trace run `git-trace rehook` to reinstall it on existing repos. ## Storage quota Every account carries `users.quota_kb` (default 500 MiB, `0` = unlimited). The `update` hook calls `git-trace quota-check`, which sums the owner's `repos.size_kb` across all their repositories and adds the incoming `GIT_QUARANTINE_PATH` size — an over-quota push is rejected before any ref moves. `size_kb` is refreshed on every push; `git-trace quota <user> [MiB]` shows or sets the limit. ## Audit log An append-only `audit_log` journal records pushes (and quota rejections as `push_denied`), logins (success and failure), registrations, repo creation, ACL grants/revocations, MR merges and issue/MR/patch-set creation — actor, IP, action, repo, detail. The actor name is denormalized so entries survive user deletion; rows older than 90 days are pruned on insert. Every repo has an `/activity` tab (read permission); admins additionally get a global `/audit` view. ## Billing tiers (manual entitlement) The forge stores only the *entitlement* — `users.tier` (`free`, `pro`, `self`, `enterprise`) plus `tier_expires_at` — and records every change in the audit journal (`tier_change`, `tier_denied`). Payment itself is an operator workflow outside the system — see `portal.md` for the external-portal contract. ```sh git-trace tier alice pro +30 # 30 days of pro git-trace tier alice pro never # no expiry git-trace tier alice # show tier + expiry git-trace tier alice free # downgrade ``` Expiry is lazy: an expired tier reads as `free` and is swept on first use — no cron daemon needed. Enforcement is opt-in via `GIT_TRACE_TIER_GATE=1` (a self-hosted instance has no paywall): with the gate on, creating a *private* repository over the web requires tier ≥ `pro` (or admin). Quota stays an independent operator knob — tier gates features, `quota` gates bytes. ## Self-registration `GIT_TRACE_OPEN_REG=1` exposes `/register` (name, email, password ≥ 8, repeat). Sign-ups get the default 500 MiB quota; the endpoint is rate-limited per IP like the login path and protected by the Origin check. With the flag unset the route returns 404 — the default stays safe for private instances. The email doubles as the sender identity for mail-in submissions (`git-trace mailrecv`). ## `/account` — self-service settings Logged-in users manage their own credentials from the browser; no shell access required. Everything here is CSRF-bound to the session and audited. - **password** — rotate (requires the current one; verified with scrypt, so a stolen session alone cannot change it). - **email** — set/clear; feeds `mailrecv` `From:` auth. - **API tokens** — create (label, shown exactly once), list (label + creation date, never the secret), revoke. Same SHA-256 digest storage as `git-trace mktoken`; revocation is scoped to the owner's tokens. - **SSH keys** — paste a public key, list fingerprints, remove own keys. Shares `ssh_keys` with the `sshkey` CLI; a key added from the browser regenerates `authorized_keys` immediately — no operator involvement.