Skip to content

Security controls — ZevCloud

The high-level security posture. This page covers the technical controls; people-and-process controls (access reviews, on-call rotation, incident response) sit with the platform's security architecture and incident response pages.

Encryption at rest

Data Mechanism
Database storage (all of ZevCloud's tables) Neon-on-AWS encrypts the underlying storage volumes
Service environment variables marked isSecret: true AES-256-GCM via the field-crypto service; ENCRYPTION_KEY env var. Plaintext is write-only after the flag flips — every read path masks regardless of caller
Domain registrant contacts + EPP auth codes AES-256-GCM via the same field-crypto service
API keys Stored as bcrypt-style hashes; plaintext shown once at creation, never persisted

Encryption in transit

  • All inbound traffic terminates at Cloudflare with TLS 1.2+
  • Cloudflare → origin (Hetzner Ashburn backend) is TLS-encrypted
  • Backend → external vendors (Neon, NameSilo, NIRA EPP, Cloudflare API, Forward Email, ZeptoMail, Better Stack) is TLS by default; the EPP channel to NIRA uses mTLS

Authentication + authorisation

  • Customer auth — ZevID OAuth. ZevCloud never holds customer passwords. MFA enforcement is ZevID's responsibility (the customer's per-account security settings on accounts.zevop.com apply).
  • Admin auth — same ZevID OAuth flow, additionally gated by a server-side RBAC permission list. Permissions enumerate at admin-permissions.ts. Every admin endpoint declares its required permission via the @RequireAdminPermission(...) decorator.
  • Programmatic (API key) auth — per-key scope check. Hashed keys; plaintext shown once.
  • Cross-product (ZPIP) auth — service+user tokens issued by ZevID with per-scope user consent. Inbound ZPIP endpoints are host-isolated to service.zevcloud.net (the INTEGRATION_DOMAIN), separate from the public dashboard host. Wrong host returns 404, not 403 — the route's existence is not leaked.

Tenant isolation

  • Every customer-resource row is scoped by team_id at the query layer. The team-membership check happens in middleware before the controller runs.
  • Customer workloads (the apps they deploy) run in per-service Coolify containers on Hetzner Falkenstein. Each container has its own filesystem + env namespace.
  • Customer DNS zones are separate Cloudflare zones; cross-tenant DNS reads/writes are blocked by the zone-id boundary.

Platform self-protection

  • A hardcoded PROTECTED_DOMAINS list refuses any management operation (attach / register / DNS write / email-forwarding) on internal Zev infrastructure domains (zevop.com and subdomains today). The gate is checked at every write entry point AND in the DNS reconciler — a row that somehow exists can't bypass it.
  • A foreign-MX conflict guard refuses to enable email forwarding on a customer domain whose MX records already point at another mail service (Microsoft 365, Google Workspace, etc.) without explicit "yes, replace" confirmation listing the existing service.
  • Forward Email domain registration sends catchall: false to prevent the platform's auto-catchall behaviour silently routing every customer's mail to our internal inbox. A defence-in-depth sweep deletes any auto-created aliases on first toggle-on and alerts ops if it ever catches one.

Audit logging

  • Admin actions against customer resources write to admin_audit_logs (actor admin id, target resource, before/after where relevant).
  • Inbound ZPIP requests + outbound ZPIP webhook deliveries are recorded for replay-protection + audit.
  • Customer-facing security-event logging (login, MFA, etc.) is owned by ZevID — see ZevID's section.

Secret handling

  • All secrets are passed via environment variables; nothing is checked in. .env.example lives in the repo as a key catalogue (no values).
  • Once a setting carries isSecret: true, the plaintext is write-only end-to-end. The admin UI shows masked / "set" indicators; the API never returns plaintext, regardless of caller.

What's not yet in place

Honest about gaps:

  • No automated retention purge cron today. Data is retained for the lifetime of the team/service. See retention.md.
  • No formal pen-test schedule. We rely on internal review + targeted ultrareview passes when shipping security-sensitive surface. The DPO will work with engineering on a formal schedule when needed.
  • No SIEM beyond Better Stack's log aggregation. Detection-and-response is reactive against logs + Sentry events.

These gaps are expected at our current stage; they're listed so the DPO knows what to ask about as the company grows.