@@ -0,0 +1,109 @@ |
| | 1 | +# Postgres roles |
| | 2 | + |
| | 3 | +shithub uses a single Postgres role today (`shithub`) for everything — |
| | 4 | +the web server, the SSH dispatcher, the worker, and the git hooks all |
| | 5 | +connect with the same credentials. That's deliberate for development |
| | 6 | +ergonomics; production hardening (S37) splits roles along blast-radius |
| | 7 | +lines so an exploit in one surface can't read or mutate the rest of |
| | 8 | +the database. |
| | 9 | + |
| | 10 | +This doc captures the **target end state** so S37 doesn't have to |
| | 11 | +re-derive it. None of these roles exist yet in dev or CI. |
| | 12 | + |
| | 13 | +## Role plan |
| | 14 | + |
| | 15 | +| Role | Used by | Grants | |
| | 16 | +| ---------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------- | |
| | 17 | +| `shithub` | web server, worker, admin tooling | Full owner of all tables (current behavior) | |
| | 18 | +| `shithub_hook` | `shithubd hook pre-receive` / `post-receive` (S14) | `SELECT` on `users`, `repos`. `INSERT` on `push_events`, `jobs`, `webhook_events_pending`. Nothing else. | |
| | 19 | +| `shithub_akc` | `shithubd ssh-authkeys` (S07/S13) | `SELECT` on `user_ssh_keys`, `users`. `UPDATE` on `user_ssh_keys.last_used_at` (and that column only). | |
| | 20 | +| `shithub_ro` | future read-only replica or analytics | `SELECT` on every table, no write access at all | |
| | 21 | + |
| | 22 | +The hook split is the highest-value because hooks run inside the push |
| | 23 | +process — fewer guardrails between a compromised git push and DB writes |
| | 24 | +than the web layer (which goes through CSRF + middleware + handlers). |
| | 25 | +The AKC split mirrors the contract from S07: that path only needs to |
| | 26 | +authenticate keys; if it can't write to repos or push_events the blast |
| | 27 | +radius is "log spam at worst." |
| | 28 | + |
| | 29 | +## SQL recipe (S37 will turn this into a migration or Ansible task) |
| | 30 | + |
| | 31 | +```sql |
| | 32 | +-- ─── shithub_hook ─────────────────────────────────────────────── |
| | 33 | +CREATE ROLE shithub_hook LOGIN PASSWORD :'shithub_hook_password'; |
| | 34 | + |
| | 35 | +-- Reads (re-checked from DB so env staleness can't authorize a push): |
| | 36 | +GRANT SELECT (id, suspended_at, deleted_at) ON users TO shithub_hook; |
| | 37 | +GRANT SELECT (id, is_archived, deleted_at, default_branch) ON repos TO shithub_hook; |
| | 38 | + |
| | 39 | +-- Writes (post-receive's exact INSERT surface): |
| | 40 | +GRANT INSERT ON push_events TO shithub_hook; |
| | 41 | +GRANT INSERT, SELECT, USAGE ON jobs TO shithub_hook; |
| | 42 | +GRANT INSERT ON webhook_events_pending TO shithub_hook; |
| | 43 | +GRANT USAGE, SELECT ON SEQUENCE push_events_id_seq TO shithub_hook; |
| | 44 | +GRANT USAGE, SELECT ON SEQUENCE jobs_id_seq TO shithub_hook; |
| | 45 | +GRANT USAGE, SELECT ON SEQUENCE webhook_events_pending_id_seq TO shithub_hook; |
| | 46 | + |
| | 47 | +-- Connect & schema: |
| | 48 | +GRANT CONNECT ON DATABASE shithub TO shithub_hook; |
| | 49 | +GRANT USAGE ON SCHEMA public TO shithub_hook; |
| | 50 | + |
| | 51 | +-- Notify channel: pg_notify needs no extra grant (it's a function call). |
| | 52 | + |
| | 53 | +-- Explicitly REVOKE the public defaults if any were inherited: |
| | 54 | +REVOKE ALL ON ALL TABLES IN SCHEMA public FROM shithub_hook; |
| | 55 | +REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM shithub_hook; |
| | 56 | +-- Then re-apply the precise grants above. (Order: REVOKE first when |
| | 57 | +-- migrating an existing role; on a fresh CREATE ROLE the explicit |
| | 58 | +-- grants are already authoritative.) |
| | 59 | + |
| | 60 | + |
| | 61 | +-- ─── shithub_akc ──────────────────────────────────────────────── |
| | 62 | +CREATE ROLE shithub_akc LOGIN PASSWORD :'shithub_akc_password'; |
| | 63 | + |
| | 64 | +GRANT SELECT (id, fingerprint, public_key, user_id, expires_at, revoked_at) |
| | 65 | + ON user_ssh_keys TO shithub_akc; |
| | 66 | +GRANT UPDATE (last_used_at, last_used_ip) |
| | 67 | + ON user_ssh_keys TO shithub_akc; |
| | 68 | +GRANT SELECT (id, username, suspended_at, deleted_at) |
| | 69 | + ON users TO shithub_akc; |
| | 70 | + |
| | 71 | +GRANT CONNECT ON DATABASE shithub TO shithub_akc; |
| | 72 | +GRANT USAGE ON SCHEMA public TO shithub_akc; |
| | 73 | +``` |
| | 74 | + |
| | 75 | +## Plumbing changes when S37 lands |
| | 76 | + |
| | 77 | +* **Config**: add `SHITHUB_HOOK_DATABASE_URL` and `SHITHUB_AKC_DATABASE_URL` |
| | 78 | + to `internal/infra/config/config.go`. Fall back to `SHITHUB_DATABASE_URL` |
| | 79 | + when unset (dev keeps single-role). |
| | 80 | +* **Hook subcommands**: `cmd/shithubd/hook.go::loadHookCtx` checks |
| | 81 | + `SHITHUB_HOOK_DATABASE_URL` first, falls back to the main URL. |
| | 82 | +* **AKC subcommand**: `cmd/shithubd/ssh.go::sshAuthkeysCmd` checks |
| | 83 | + `SHITHUB_AKC_DATABASE_URL` first. |
| | 84 | +* **Ansible role**: `deploy/ansible/roles/postgres/tasks/main.yml` runs |
| | 85 | + the SQL recipe above against a fresh DB; passwords come from sops or |
| | 86 | + 1Password. |
| | 87 | +* **Migration policy**: don't add the GRANT statements to the goose |
| | 88 | + migrations directory — those run as the `shithub` super-role and |
| | 89 | + changing them would re-grant on every dev re-up. Roles + grants are |
| | 90 | + Ansible territory, not migration territory. |
| | 91 | + |
| | 92 | +## Why we deferred |
| | 93 | + |
| | 94 | +S14's deliverables include "Hook DB connection: small pool, distinct |
| | 95 | +credentials with the minimum-needed grants." The dev path (single role) |
| | 96 | +is what S14 actually shipped. Splitting it now adds friction to every |
| | 97 | +new schema migration (each new write target needs a grant tweak) while |
| | 98 | +the schema is still iterating fast — by S37 the schema has settled |
| | 99 | +enough that the grant surface is stable. |
| | 100 | + |
| | 101 | +## Tracking |
| | 102 | + |
| | 103 | +* This doc is the design. |
| | 104 | +* `.docs/sprints/S37-deployment-automation.md` references this doc |
| | 105 | + under "Postgres" → "Hook DB role split" so the deferred work is on |
| | 106 | + the sprint's deliverable list, not floating. |
| | 107 | +* The S14 sprint spec (`.docs/sprints/S14-push-processing-pipeline.md`) |
| | 108 | + remains the authoritative description of what S14 *should* have done |
| | 109 | + in an ideal world; this doc explains what got cut and where it landed. |