Hardening Checklist¶
A checklist for operators of public Doable instances (SaaS-style, anonymous signups). For private/team deployments most of these are optional.
Operating system¶
- [ ] OS fully patched (
unattended-upgradesenabled). - [ ] Non-root user for application processes (Docker does this; for bare-metal, run
doable.serviceas a dedicated user). - [ ] SSH: key-only login, root login disabled, fail2ban watching.
- [ ] UFW: deny incoming except 22, 80, 443.
- [ ] Time sync (
systemd-timesyncdorchrony) — JWT validation needs accurate clocks. - [ ] Swap configured (Doable's installer does 2 GB by default).
Network¶
- [ ] All app services bound to
127.0.0.1— verify withss -tlnp. - [ ] One TLS-terminating proxy (nginx/Caddy) is the only publicly reachable port besides SSH.
- [ ] HSTS header enabled in the proxy (
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload). - [ ] HTTP/2 enabled; HTTP/3 if your proxy supports it.
- [ ] Rate limits at the proxy on
/auth/*(much stricter than other routes). - [ ] Egress firewall — block outbound to RFC1918, link-local, and metadata IPs (
169.254.169.254). - [ ] Optional: WAF in front (Cloudflare proxied, or AWS WAF) for L7 abuse.
Secrets¶
- [ ]
JWT_SECRET,ENCRYPTION_KEY,INTERNAL_SECRETare unique 32-byte random hex strings. - [ ]
.envanddocker/.envchmod 600, owned by the app user. - [ ] Backups encrypt these env files separately (don't restore secrets to a new host without rotating).
- [ ] No
NEXT_PUBLIC_*variable contains a secret (audited at build time).
Database¶
- [ ] PostgreSQL bound to
127.0.0.1. - [ ] Database user has access only to the
doabledatabase; not a superuser. - [ ] Daily
pg_dumpto off-host storage; encrypt at rest. - [ ] Connection limits sized so
DATABASE_POOL_SIZE × replicas + admin_overhead < max_connections.
AI sandbox¶
- [ ] Default policies kept strict (
DEFAULT_DANGEROUS_COMMANDSnot weakened). - [ ] URL allow-list reviewed — remove any "fetch from anywhere" overrides for public workspaces.
- [ ]
dovaultresource limits set:memoryMax: 150M,cpuQuota: 30%,tasksMax: 32. - [ ] Process isolator backend =
nsjailorsystemd(notdirect) on Linux. - [ ] Per-workspace credit caps configured to prevent runaway AI spend.
Account abuse¶
- [ ] Email verification required on signup (
/auth/signupendpoint behavior). - [ ] Strict password policy (≥ 8 chars; bcrypt cost ≥ 12).
- [ ] Rate limits on signup, login, password reset (already configured by default — verify in
services/api/src/index.ts). - [ ] Captcha on signup if you see automated abuse (not built in — wire up Turnstile or hCaptcha).
- [ ] Disposable-email block list if needed.
Outbound abuse from user projects¶
- [ ] Each project's Vite dev server runs jailed (default).
- [ ] User project
fetch()calls go through your egress firewall. - [ ] Published sites (
SITES_DIR/*) are static-only; no server-side execution.
Updates¶
- [ ] Subscribe to the Doable repo's Security Advisories.
- [ ] Run
pnpm audit(or your favorite vulnerability scanner) periodically; rebuild containers. - [ ] Test upgrades on a staging instance before production.
Observability¶
- [ ] Centralize logs (
journalctl→ Loki / CloudWatch / Datadog). - [ ] Alert on: 5xx rate, auth failures spike, audit-log denies spike, disk usage, AI provider errors.
- [ ] Periodically review Workspace Settings → Audit for suspicious tool-call patterns.
Incident response¶
- [ ] Documented playbook for rotating secrets in an emergency.
- [ ] Documented playbook for revoking a workspace / user.
- [ ] Recent backup tested for restore (don't trust untested backups).
- [ ] Out-of-band contact info for Stripe, AI providers, and your DNS provider.
Compliance hooks (if applicable)¶
- [ ] Data retention policy for chat history & audit log.
- [ ] DPA in place with the AI provider.
- [ ] DSAR process — exporting and deleting a user's data.
If you check off this list, you're well past the security level of most commercial AI app builders.