78 lines
3.9 KiB
Markdown
78 lines
3.9 KiB
Markdown
# Security & Pre-Deployment Checklist
|
|||
|
|
|
||
|
|
> **⚠️ TREAT ALL SECRETS IN `.env.local` AS COMPROMISED.**
|
||
|
|
> This project was distributed in a transfer package, which means every secret
|
||
|
|
> that was present in `.env.local` — the `DATABASE_URL` / Postgres password,
|
||
|
|
> `BETTER_AUTH_SECRET`, and any Stripe / OpenAI / Resend API keys — has left a
|
||
|
|
> trusted boundary and **must be treated as leaked**. Rotate **all** of them
|
||
|
|
> before any production deployment or client handoff. Do not assume "it was only
|
||
|
|
> a zip" — assume the file is public.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. Rotate every secret before production / handoff
|
||
|
|
|
||
|
|
Work through this list and rotate each item. Do **not** reuse any value that
|
||
|
|
ever appeared in the distributed `.env.local`.
|
||
|
|
|
||
|
|
- [ ] **Postgres password** — change the database role's password (or provision a
|
||
|
|
brand-new role) and update `DATABASE_URL` everywhere it is configured.
|
||
|
|
Then revoke the old credential.
|
||
|
|
- [ ] **`BETTER_AUTH_SECRET`** — generate a fresh 32-byte secret:
|
||
|
|
```bash
|
||
|
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||
|
|
```
|
||
|
|
Rotating this invalidates existing sessions — expected and desired.
|
||
|
|
- [ ] **Stripe** — roll the secret key (and restricted keys), and rotate the
|
||
|
|
webhook signing secret in the Stripe Dashboard.
|
||
|
|
- [ ] **OpenAI** — revoke the leaked API key and issue a new one.
|
||
|
|
- [ ] **Resend** — revoke the leaked API key and issue a new one.
|
||
|
|
- [ ] **`CRON_SECRET`** — set a strong random value (the cron routes now fail
|
||
|
|
closed if it is unset):
|
||
|
|
```bash
|
||
|
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||
|
|
```
|
||
|
|
Configure the same value in Vercel so Cron sends
|
||
|
|
`Authorization: Bearer <CRON_SECRET>`.
|
||
|
|
- [ ] **Google OAuth** — if the client secret was present in the transfer,
|
||
|
|
rotate it in the Google Cloud Console.
|
||
|
|
|
||
|
|
## 2. Secret hygiene
|
||
|
|
|
||
|
|
- [ ] **Never commit `.env.local`** (or any real `.env*` with live values).
|
||
|
|
Confirm it is listed in `.gitignore`.
|
||
|
|
- [ ] Store production secrets in the deployment platform's encrypted env-var
|
||
|
|
store (e.g. Vercel Project Settings → Environment Variables), not in files.
|
||
|
|
- [ ] Use distinct secrets per environment (dev / preview / production).
|
||
|
|
|
||
|
|
## 3. Database / TLS
|
||
|
|
|
||
|
|
- [ ] Set **`DATABASE_SSL=require`** in production so the connection uses
|
||
|
|
**verified TLS** (encrypted + certificate-verified). `DATABASE_SSL=disable`
|
||
|
|
is **only** for local / unix-socket development.
|
||
|
|
If the provider uses a private/custom CA, supply it via `DATABASE_CA`.
|
||
|
|
- [ ] Use a **managed Postgres on a private network** (or the provider's private
|
||
|
|
endpoint) rather than a database exposed on a public IP.
|
||
|
|
|
||
|
|
## Resolved in code
|
||
|
|
|
||
|
|
The following hardening has already been applied in this codebase:
|
||
|
|
|
||
|
|
- **TLS enforcement** — `lib/db/index.ts` now defaults to verified TLS
|
||
|
|
(`rejectUnauthorized: true`) and never silently runs plaintext. Behavior is
|
||
|
|
controlled by the explicit `DATABASE_SSL` env var (`disable` / `no-verify` /
|
||
|
|
`require`), with optional `DATABASE_CA`.
|
||
|
|
- **Constant-time cron auth** — `lib/cron-auth.ts` performs a `timingSafeEqual`
|
||
|
|
bearer-token comparison that **fails closed** when `CRON_SECRET` is unset. All
|
||
|
|
cron routes (`daily`, `late-fees`, `lease-expiry`, `rent-reminders`) now use it
|
||
|
|
and share the standard `Authorization: Bearer` scheme.
|
||
|
|
- **Security headers** — `next.config.ts` sets a strict baseline on all routes:
|
||
|
|
`X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`
|
||
|
|
(protects the tenant-portal URL token), HSTS with preload,
|
||
|
|
`X-DNS-Prefetch-Control: off`, and a Content-Security-Policy.
|
||
|
|
- **Auth rate limiting** — `lib/auth.ts` enables better-auth's built-in rate
|
||
|
|
limiting (20 requests / 60s per IP) to slow brute-force and credential
|
||
|
|
stuffing.
|
||
|
|
- **Test-plan backdoor removed** — the development-only backdoor that bypassed
|
||
|
|
plan/subscription checks is disabled in production.
|