collin/anvil
836274ecbf88554b93560d99bc4a953d7d5f6995 / TODO.md
| 1 | - [ ] don't expose a users email on their profile page |
| 2 | - [ ] don't include anvil before the breadcrumbs in the repo name i.e. anvil/collin/repo just do collin/repo |
| 3 | - [x] implement github action style CI feature _(core done — see "Session notes" below; the "isolated workers" idea is the (c) sandboxed broker, still TODO)_ |
| 4 | - I want to get this to a sufficient state that we could bootstrap this app using this CI and automate deployments if we wished |
| 5 | - probably might want to have other isolated anvil workers or something running for CI jobs |
| 6 | - [ ] implement github pages style hosting feature |
| 7 | - [ ] security audit _(started: see (b) threat-model below)_ |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | # Session notes / resume point |
| 12 | |
| 13 | _Last updated: 2026-06-10. Working state is clean: `cargo build`, `cargo clippy |
| 14 | --workspace`, `cargo fmt --all`, and `cargo test --workspace` all pass. |
| 15 | Everything below is UNCOMMITTED (repo convention: commit only when asked)._ |
| 16 | |
| 17 | Two of your top-of-file items are quick wins we noticed but did NOT do yet: |
| 18 | - **profile email** — `user_profile` in `crates/anvil-web/src/ui.rs` renders |
| 19 | `owner.email`; just drop that block. |
| 20 | - **breadcrumb `anvil/` prefix** — `repo_index` header in the same file starts |
| 21 | with `a href="/" { "anvil" } " / "`; remove the leading anvil link. |
| 22 | |
| 23 | ## Done this session |
| 24 | |
| 25 | - **CI UI** — runs list `/{owner}/{repo}/ci`, run-detail (status/timing/log), |
| 26 | per-commit status badges, "CI" nav link. (`crates/anvil-web/src/ui.rs`) |
| 27 | - **CD redeploy webhook** — on a green run of `[ci] deploy_branch` in the single |
| 28 | `[ci] deploy_repo`, POST to `[ci] deploy_webhook` (`X-Anvil-Deploy-Secret` |
| 29 | header). Scoped to ONE repo. `CiConfig` in `crates/anvil-core/src/config.rs`; |
| 30 | `deploy()` in `crates/anvil-ci/src/lib.rs`. Docs: `DEPLOY.md` §7, |
| 31 | `deploy/anvil.toml`. `reqwest` added with NO TLS feature (keeps musl |
| 32 | cross-compile aws-lc-free). |
| 33 | - **Docker socket on hagrid** — `deploy/run.sh` mounts it + `--group-add`s the |
| 34 | gid for the non-root user; caveat in `DEPLOY.md` §4. |
| 35 | - **Toasty ORM cleanup** — `ci.rs` `list_by_repo`/`latest_for_commit`/ |
| 36 | `queued_ids` now sort/limit/filter in SQL, not in memory. Verified by the new |
| 37 | `ordering_and_limit_run_in_the_database` test. (Sweep: these were the only |
| 38 | real instances; `repos::list_all_with_owner` sorts by a joined username and |
| 39 | needs all rows — intentionally left.) |
| 40 | - **(a) CSRF + cookie hardening** — |
| 41 | - Cookie: `HttpOnly` + `SameSite=Lax` + `Secure` (auto via |
| 42 | `Config::secure_cookies()` when base_url is https). |
| 43 | - Synchronizer token `HMAC-SHA256(server_secret, session)`; secret persisted |
| 44 | at `data_dir/csrf_secret` (`App::csrf_token` in `crates/anvil-core/src/lib.rs`). |
| 45 | Deps `hmac`, `sha2`. |
| 46 | - `Csrf` extractor + constant-time `verify_csrf` (`crates/anvil-web/src/auth.rs`). |
| 47 | Hidden `csrf` field + verification on add/delete SSH key, new repo, repo |
| 48 | settings. Login exempt; logout relies on SameSite. |
| 49 | - htmx insurance: `auth::csrf_context` middleware → request-scoped task-local; |
| 50 | `layout` sends the token via `hx-headers` on every htmx request. |
| 51 | |
| 52 | ## Next up (the agreed a/b/c plan — (a) done) |
| 53 | |
| 54 | ### (b) untrusted-mode threat-model doc ← START HERE |
| 55 | Write `docs/untrusted-mode.md` (or `SECURITY.md`). Capture the severity-ranked |
| 56 | analysis: |
| 57 | 1. **CI runner = root-equiv RCE via Docker socket** — the hard blocker; fix is (c). |
| 58 | 2. Stored XSS if we ever serve raw blobs → separate origin + `text/plain` + CSP. |
| 59 | 3. Git resource exhaustion (pack/decompression bombs, huge ref ads) → size/ |
| 60 | storage quotas + timeouts. |
| 61 | 4. Open registration anti-abuse (email verify, rate limit, CAPTCHA, quotas, ban). |
| 62 | 5. Authorization granularity → collaborator roles + per-repo tokens / deploy keys. |
| 63 | 6. Webhook SSRF (when webhooks land) → block private IPs / metadata / localhost. |
| 64 | Already-right: private repos 404 (no leak), reserved usernames + `/-/`, CI |
| 65 | tar-upload (not bind-mount), single-repo CD gate, argon2, CSRF + Secure cookies. |
| 66 | Recommendation to record: single-tenant/owner-operated stays the supported |
| 67 | stance; untrusted is gated behind (c). |
| 68 | |
| 69 | ### (c) sandboxed CI broker |
| 70 | Make anvil the only Docker client; the job container gets NO socket. Forbid bind |
| 71 | mounts; `--cap-drop=ALL`, `--security-opt=no-new-privileges`, read-only rootfs, |
| 72 | non-root uid, `--pids-limit`, mem/cpu caps, wall-clock timeout, egress limits, |
| 73 | image allowlist + CI-minute quotas. Stronger tier: gVisor/Kata/Firecracker |
| 74 | microVMs (this is the "isolated workers" idea from the list above). Current |
| 75 | runner: `crates/anvil-ci/src/lib.rs::execute`. |
| 76 | |
| 77 | ## Loose ends |
| 78 | |
| 79 | - **CSRF header consumption:** `hx-headers` sends the token as a `csrf` header, |
| 80 | but `verify_csrf` only reads the form field. When we add a tokenless htmx |
| 81 | action (raw `hx-post`/`hx-delete`, no `<form>`), also read the `csrf` header. |
| 82 | - **Toasty migrations:** schema only pushed on a fresh DB (`db::connect`); new |
| 83 | columns won't apply to an existing DB until migrations land. (The |
| 84 | `data_dir/csrf_secret` file is created automatically — no DB change.) |
| 85 | - **Suggested commits when ready:** (1) CI UI, (2) CD webhook + deploy wiring, |
| 86 | (3) ci.rs ORM cleanup + test, (4) CSRF + cookies. Trailer: `Co-Authored-By: |
| 87 | Claude ...`. |
| 88 | |
| 89 | ## Remaining roadmap (plan milestones beyond a/b/c) |
| 90 | |
| 91 | 8. Issues · 9. Pull requests (gix merge) · 10. Webhooks (mind the SSRF item) · |
| 92 | github-pages-style static hosting (your list item). |