Security model
This page answers the question we get most often: where does the DEK live, and can Ignisfox read my key material? Short version: every cert and key is sealed with a per-tenant key that's itself wrapped by a master key the application server never writes to disk.
Envelope encryption at a glance
We use two layers of keys, stored and unwrapped independently:
- Master KEK (Key Encryption Key) — 32 random bytes, base64-stored in the
MASTER_KEK_B64environment variable. It lives in Vercel's encrypted env store and is injected into the runtime at boot. It is never persisted to the database, never written to disk by application code, and never logged. - Tenant DEK (Data Encryption Key) — 32 random bytes, generated on the first API call that needs encryption for a new tenant. Stored wrapped (encrypted with the master KEK) in
tenants.wrapped_dek. Each tenant gets a distinct DEK; cross-tenant decrypt is impossible even with database read access.
sealPayload / openPayload
The primitives live in lib/crypto.ts. Every cert, key, bundle, PFX, API token, group password, and CA provider credential flows through the same two functions:
sealPayload(plaintext, dek)→ AES-256-GCM encrypt with a fresh 12-byte IV, returnIV || ciphertext || 16-byte GCM tagas a single Buffer. Goes into a Postgresbyteacolumn.openPayload(blob, dek)— the inverse. GCM authenticates as it decrypts, so a tampered ciphertext throws rather than returning garbage. We treat any auth failure as a hard error.
The DEK is unwrapped per-request from tenants.wrapped_dek using the master KEK, used for the decrypt, and the raw buffer is dropped out of scope immediately afterwards. It is never cached on disk and never written to a log.
Key rotation
The DEK can be re-wrapped with a new KEK without re-encrypting any payloads: decrypt the old wrapped_dek with the retiring KEK, re-wrap with the new one, overwrite the column. Payloads stay the same bytes. This keeps rotation cheap — rotating the master KEK is an O(tenants) operation, not O(certs).
Tenant DEK rotation re-wraps every payload with a fresh DEK. That's more expensive and we only run it on explicit request (compromise incidents, account transfers).
What the service role can and can't see
The Supabase service-role key bypasses row-level security — it's how the Next.js server reads and writes tenant data. Critically, the service role only ever sees ciphertext for payloads. Plaintext key material crosses process boundaries only during a single, explicitly authenticated request (download, PFX build, push pull with a valid token) and is never persisted in that form.
The master KEK is not accessible to anyone holding just the service-role key. Even with a full database dump, an attacker sees wrapped_dek and sealed payloads — useful kindling, not usable bytes.
Where secrets live
- Cert payloads —
certificates.payload_ciphertext,bytea, sealed with the tenant DEK. - Group passwords —
cert_groups.encrypted_password,bytea, sealed with the tenant DEK. Used as the default password when building a PFX from that group. - CA provider credentials — API keys / secrets / customer IDs for GoDaddy and friends, sealed with the tenant DEK before insert.
- Push tokens — opaque random strings, hashed (not sealed — we need constant-time equality on lookup) with SHA-256 before storage. The plaintext token is only ever shown once, at creation.
- Wrapped DEKs —
tenants.wrapped_dek, sealed with the master KEK. - Browser cookies for the build-PFX flow — when you save a PFX password in the builder, the cookie holding it is HMAC-signed with a server-only secret. It's first-party, HTTP-only, Secure, and scoped to your tenant session; it's also shorter-lived than the underlying group password on the server.
IP hashing
We log request IPs for abuse investigation and analytics, but we store the SHA-256 hash of (ANALYTICS_IP_SALT || raw_ip), not the raw IP. That gives us per-IP uniqueness (counting distinct visitors, detecting bursts from one source) without a reversible database of who visited what. The salt is a secret env var; rotating it invalidates historical hashes, which is the point.
The same salt doubles as a tenant-independent seed for some DEK-adjacent derivations, which is why you'll see it referenced near the encryption code. It never replaces or weakens the tenant DEK — both layers are required.
Data retention
- Active certs — kept indefinitely until you delete them.
- Audit log — 180 days rolling window. Trimmed nightly.
- Probe history — 90 days per monitored host. Older probes are pruned.
- Analytics events — 90 days at event granularity, indefinitely at aggregate counter granularity.
- Feedback submissions — kept until resolved + 365 days, then purged.
Enterprise tenants can configure their own retention windows — talk to us if compliance requires tighter limits.
Account deletion
If you need Ignisfox off your ledger entirely, go to /dashboard/settings/profile and use the Danger zone at the bottom. That triggers:
- Every cert, key, group, push target, CA provider, monitored host, and alert rule for your tenant is deleted from Postgres immediately.
- Your tenant row is deleted, which takes the
wrapped_dekwith it. At that point even if we wanted to recover a payload backup, there is no DEK to decrypt it. - Your Clerk user record is scheduled for deletion on the Clerk side (they manage the auth records separately from our DB).
- Supabase point-in-time backups age out on the standard rolling window. We do not retain a long-term archive of tenant data.
The action is irreversible and we require typed confirmation. If you want a data export before deleting, run the vault CSV export first — see Cert vault.
What we don't do
- No third-party key escrow. We do not ship your DEK anywhere — not to email providers, not to the mailer, not to analytics. The only place the unwrapped DEK exists is in RAM, during an authenticated request.
- No plaintext exports. The CSV export contains metadata only. There is no "dump everything" endpoint that returns unsealed keys.
- No support-staff decrypt path. Ignisfox operators can read the database via the service role, but the master KEK is scoped to the application runtime, not to operator workstations. We have no break-glass "decrypt this tenant's PFX" button.
Reporting a vulnerability
Email security@ignisfox.com with reproduction steps. We respond within one business day and we do not pursue legal action against good-faith researchers.