This document describes the security properties enforced by the gitstate platform and the residual risks that must be addressed before a production deployment.
Mechanism. Every org-scoped table (repos, projects, issues, pull_requests,
commits, …) has PostgreSQL Row-Level Security enabled with the policy:
CREATE POLICY org_isolation ON <table>
USING (org_id = current_org())
WITH CHECK (org_id = current_org());current_org() reads current_setting('app.current_org', true)::uuid. The setting
is injected by db.WithOrg(ctx, orgID, fn) which opens a transaction and executes
SET LOCAL app.current_org = $1 before running fn. SET LOCAL is scoped to the
transaction so it cannot bleed across requests.
Proof. internal/store/rls_test.go::TestRLSCrossOrgIsolation creates two orgs and
one project each, then asserts that reading under org A's RLS context returns zero of
org B's rows. Run with a live database via go test ./internal/store/ -run RLS.
Invariant. No org-scoped query may run outside a WithOrg block. Application-level
bugs cannot produce cross-org reads because the database layer enforces isolation
independently.
Mechanism. Cross-org access is available only to super-admins via the EE admin
interface (ee/admin). Every cross-org operation must call store.WriteAudit before
performing work:
store.WriteAudit(ctx, pool, actorID, orgID, "super_admin.view_org", orgID, meta)WriteAudit writes to the audit_log table (platform table, not org-scoped, not
subject to RLS). The table records: actor_id, org_id, action, target, meta
(JSONB), and created_at.
Principle. Super-admin access is never ambient — there is no "god mode" session that bypasses RLS silently. Each org touched generates an explicit audit entry.
Mechanism.
- All secrets (JWT signing key, Paystack API/webhook keys, OAuth client secrets,
TOKEN_ENC_KEY) live in environment variables. .env/.env.devare listed in.gitignoreand are never committed..env.exampledocuments every variable with safe placeholder values.config.yaml(committed) holds only non-secret structure and flags; it contains no credentials.
Relevant env vars:
| Variable | Purpose |
|---|---|
DATABASE_URL |
Neon/Postgres connection string |
JWT_SIGNING_KEY |
HS256 access token signing key |
PAYSTACK_SECRET_KEY |
Paystack API key (EE only) |
PAYSTACK_WEBHOOK_SECRET |
Paystack webhook HMAC key (EE only) |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Google OAuth (optional) |
MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET |
Microsoft OAuth (optional) |
TOKEN_ENC_KEY |
AES-256-GCM key material for at-rest repo token encryption |
ANTHROPIC_API_KEY |
LLM provider key (optional) |
Mechanism. Paystack webhook events are verified in ee/billing/paystack.go using
HMAC-SHA512 over the raw request body, compared constant-time against the
X-Paystack-Signature header. Requests that fail verification are rejected with 401
before any processing occurs.
Idempotency. Processed event IDs are stored in paystack_events; duplicate deliveries
are detected and no-opped, preventing double-charges.
Mechanism. internal/middleware/RateLimit(perMin int) provides a token-bucket rate
limiter per client IP (in-memory, mutex-guarded, periodic idle-bucket cleanup).
- General API routes: configure a reasonable limit (e.g. 120 req/min) in the router.
- Authentication endpoints (
/auth/login,/auth/signup,/auth/refresh): usemiddleware.AuthRateLimit()(10 req/min) to slow brute-force credential attacks.
Note. The current implementation is in-process. For multi-region fly.io deployments (multiple VMs) replace with a shared Redis-backed rate limiter so limits are enforced globally, not per-instance.
Background. Repo access tokens (GitHub/GitLab PATs) were previously not persisted
(PROGRESS.md W3 note). They are now optionally stored in repos.token_encrypted (bytea,
added by migration 20260618_003_repo_tokens.sql) using AES-256-GCM.
Mechanism (internal/crypto).
- Key derived from
TOKEN_ENC_KEYenv var via SHA-256 → 32-byte AES key. Encrypt(plaintext, key)→ nonce (12 bytes) || ciphertext+GCM tag.Decrypt(ciphertext, key)→ plaintext (authenticated; tampered bytes return an error).- Pure stdlib:
crypto/aes,crypto/cipher,crypto/rand,crypto/sha256.
Store layer. store.SetRepoToken / store.GetRepoToken persist / retrieve the raw
encrypted bytes inside an org-scoped transaction (RLS enforced). Encryption/decryption
is the caller's responsibility (separation of concerns: the store does not know about keys).
- Multi-region rate limiting — replace in-memory limiter with Redis-backed store when running more than one fly.io VM.
-
TOKEN_ENC_KEYrotation — implement ciphertext re-encryption procedure when the key must be rotated (current implementation is single-key; no key version header). - RLS on
audit_log— currently unscoped (intentional for super-admin). Consider a read policy limiting non-super-admins to their own org's rows. - Super-admin authentication hardening — add MFA requirement and short-lived
session tokens for super-admin sessions in
ee/admin. - Content-Security-Policy header — add CSP, X-Frame-Options, and related headers in the middleware chain for the admin HTML pages.
- Dependency audit — run
govulncheck ./...as a CI step; pin all Go module checksums ingo.sum. - Webhook replay window — add a timestamp check on Paystack webhook events to reject replayed events older than N minutes.
- Org invite token entropy — confirm invite token length is ≥ 128 bits of entropy before the feature is exposed in production.