--- paths: - "src/app/api/**" - "src/lib/auth*" - "src/server/**" - ".env*" - "next.config.*" - "Dockerfile" - "compose.yaml" --- # Security Rules ## Authentication: Better Auth with passkeys (mandatory) Whenever a project needs authentication, it uses **Better Auth** with the **passkey plugin**. Do not substitute another auth library, and do not build a custom session/credential layer. ```bash npm install better-auth # passkey support ships as a Better Auth plugin — enable it in the auth config ``` **Passkeys are the primary method.** The sign-in screen offers passkey first and presents it as the default path. A password flow is permitted only as a fallback for users whose device cannot do WebAuthn. Requirements whenever auth is present: - Passkey registration is offered during sign-up, not hidden in settings - Existing password users are prompted to add a passkey after signing in - The WebAuthn relying-party ID must match the deployment hostname. It is derived from `NEXT_PUBLIC_APP_URL` / `BETTER_AUTH_URL` — a mismatch makes every passkey fail verification, and it fails silently. - Passkeys require HTTPS in production (localhost is exempt). The reverse proxy terminates TLS — see `docs/DEPLOYMENT.md`. - If the password fallback exists: rate-limit it, never reveal whether an account exists, and store hashes only through Better Auth's own primitives ## Authorization - Verify the session in every route handler before any work - Authorization is per resource, not per route: having a session does not mean owning the record. Scope the query by the session user. - Never trust an ID coming from the client as proof of ownership ## Secrets - Never commit secrets. `.env` is gitignored; only `.env.example` is committed. - Every new variable is documented in `.env.example` with a dummy value - `NEXT_PUBLIC_` only for values safe to expose in the browser bundle - `BETTER_AUTH_SECRET` is generated per environment (`openssl rand -base64 32`) and never shared between staging and production ## Input Handling - Validate all input server-side with Zod, even when the client already did - Never interpolate user input into SQL, shell commands or file paths - Escape or avoid `dangerouslySetInnerHTML` ## Port exposure (mandatory) **Only the app service publishes a port. Nothing else. Ever.** Databases, caches, queues, search indexes, admin UIs and migration containers get **no `ports:` entry at all**. Services reach each other by service name over the Compose network — `postgres://user:pass@db:5432/app` works without publishing anything. Use `expose:` if you want the port documented; it changes nothing about reachability from outside. ```yaml db: image: postgres:17-alpine expose: - "5432" # documentation only — not reachable from the host # ports: # NEVER. This would put the database on the public internet. ``` Why this is not a style preference: `ports: "5432:5432"` on a public server means the only thing between an attacker and the whole dataset is the database password. Worse, Docker writes its own nat/DOCKER-USER iptables rules that **bypass ufw** on a typical setup — the server looks firewalled while the port is wide open. This is one of the most common ways self-hosted stacks get breached. The app service itself binds to loopback (`${APP_BIND:-127.0.0.1}`) because a reverse proxy on the host terminates TLS and forwards to it. Binding `0.0.0.0` is only correct when there is no proxy, and it means serving plain HTTP — which also breaks passkeys. If a port genuinely must be reachable for local debugging, bind it explicitly to loopback and never bare: ```yaml ports: - "127.0.0.1:5432:5432" # ok for an SSH-tunnelled session - "5432:5432" # WRONG — this is 0.0.0.0, the whole internet ``` Verify what is actually published before deploying: ```bash docker compose config | grep -A3 "ports:" # what the merged config declares docker compose ps --format "{{.Service}}\t{{.Ports}}" # what is really bound ``` Anything other than the app service showing a `0.0.0.0:` binding is a Critical finding — treat it as such in `/qa` and block the deploy. ## Transport and Headers Security headers are configured in `next.config.ts`: `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: origin-when-cross-origin`, `Strict-Transport-Security`. Verify they survive the reverse proxy after deploying. ## Changes That Need Explicit User Approval - Any change to the authentication flow - Weakening or removing the passkey requirement - Any change to authorization or ownership checks - New environment variables (must land in `.env.example` in the same commit)