Hardened controls (what we run, end-to-end)
| Layer | Control | Where to verify |
|---|---|---|
| Edge | Cloudflare WAF, DDoS L3/4/7, rate-limits per route, bot management | HTTP cf-ray headers; Cloudflare dashboard |
| Transport | TLS 1.3 only, HSTS preload, OCSP stapling. Strict-Transport-Security: max-age=63072000; includeSubDomains; preload | SSL Labs A+ rating · ssllabs.com |
| Browser | Strict CSP, X-Frame-Options: DENY, Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp, Referrer-Policy: no-referrer, Permissions-Policy locked down | securityheaders.com |
| Auth (web) | Cookie session, PBKDF2-SHA256 600,000 iterations, SHA-256 session token hash, CSRF on every state-changing request | Source: fiduci/products/odahl/functions/_lib/auth-shared.ts |
| Auth (API) | Bearer tokens (fdc_live_…), SHA-256 hashed at rest, per-key class allowlist, per-key budget | Source: fiduci/products/odahl/functions/_lib/api-auth.ts |
| Founder auth | 3 layers: password → email OTP → TOTP. Founder-only path; everything inside /api/admin/* requires is_founder = TRUE + a TOTP-fresh session | Audit table: founder_admin_audit |
| Database | Postgres on a private internal docker network. Pages never connect directly — they speak to a single Bearer-gated pg-proxy service. /pg/migrate is admin-tunnel-only and explicitly 404'd on the public nginx. | Source: fiduci/services/pg-proxy/, fiduci/deploy/nginx-public.conf |
| Substrate at rest | Per-user envelope encryption (AES-256-GCM). The plaintext content is never co-located with the account identifier in any backup. | Source: fiduci/products/odahl/functions/_lib/substrate-envelope.ts |
| Secrets | Cloudflare Pages secrets + IONOS .env with file permissions 600. Quarterly rotation runbook. No secret in source. | Runbook: docs/SECRETS_ROTATION.md |
| Backups | Postgres logical backups every 6 hours, retained 30 days. Encrypted with a separate key from the live database. Restore tested quarterly. | Runbook: docs/BACKUP_RUNBOOK.md |
| Logging | Structured JSON, no message bodies, 30-day retention then aggregated. Founder admin actions logged with IP & UA into founder_admin_audit. | Schema: migration 016 |
Reporting a vulnerability
We want to hear from you. Acknowledged within 24 hours; substantive response within 5 business days; disclosure timeline negotiated with you.
- Email — [email protected] (PGP key fingerprint published in /.well-known/security.txt)
- Encrypted alternative — Signal: ask for the founder's number via the email above.
- Bounty range — $250 – $5,000 AUD depending on severity. Critical issues affecting auth, founder boundary, or substrate confidentiality top the range.
- Safe-harbour — We do not initiate legal action against researchers acting in good faith. Stay out of other people's accounts, don't exfiltrate or destroy data, give us reasonable time to fix.
Supported versions
Odahl is a single live service — there are no "versions" to keep on life-support. Every patch is rolled out to all users. Old API key formats (fdc_test_… beta keys issued before May 2026) remain valid; if we ever sunset a key format we will give 90 days' notice and roll all affected keys for free.
Threat model summary
We design against four kinds of attacker:
- Drive-by abuse — bots, scrapers, password-spray. Mitigated by per-IP rate limits at Cloudflare + nginx + the auth handler.
- Single-account compromise — stolen cookie, leaked password. Mitigated by short session TTL, per-session TOTP gates for sensitive paths, audit log, anomaly alerts on login from new IP/UA.
- Sub-processor compromise — one of our vendors is breached. Mitigated by per-user envelope encryption (we never co-locate plaintext + identifier), no third-party model API in the runtime path, secrets rotation, principle of least privilege on every vendor.
- Infrastructure compromise — full VPS takeover. Mitigated by zero-knowledge architecture (the substrate is encrypted; full disk read still doesn't yield plaintext substrate without live session keys), short-TTL JWTs, daily off-server backup, and an incident-response plan that triggers customer notification within 72 hours.
What is not in scope
Honesty up front:
- SOC 2 Type II — in progress; Type I targeted Q3 2026, Type II Q1 2027.
- ISO 27001 — targeting end of 2026.
- HIPAA — we are not currently a Business Associate; do not use Odahl for protected health information.
- FedRAMP — out of scope, not pursuing.
- Service-level agreements — Legendaria tier includes a 99.9% SLA with credits; lower tiers are best-effort.
Contact
Security: [email protected]. Privacy: [email protected]. Founder & DPO: [email protected].