Skip to content

Security controls — ZevID

Technical and organisational measures ZevID applies. NDPA §39 requires "appropriate technical and organisational measures."

Encryption

At rest

Category Mechanism Source
Passwords bcrypt cost 12 (one-way hash) auth.service.ts (BCRYPT_ROUNDS)
TOTP secrets AES-256-GCM via FieldCryptoService. Master key in FIELD_ENCRYPTION_KEY env (set in production with a strong value; Ops-only access) src/common/security/field-crypto.service.ts
Phone numbers (E.164) AES-256-GCM (same FIELD_ENCRYPTION_KEY) account_phones.phone_e164_encrypted
Recovery codes SHA-256 hash (irreversible) accounts.recovery_codes
BVN / NIN SHA-256 hash. Plaintext BVN/NIN never reaches ZevID — only the hash is sent by the KYC-running product kyc_verifications.bvn_hash, .nin_hash
Phone hash SHA-256 with pepper. PHONE_HASH_PEPPER env (set in production with a strong value; Ops-only access). Treated as set-once account_phones.phone_e164_hash
Database-level Neon-on-AWS at-rest encryption (provider-managed)

In transit

Channel Mechanism
User browser ↔ ZevID API TLS 1.2+; HSTS; Cloudflare-managed certificates
ZevID ↔ ZeptoMail / Termii / Cloudflare R2 / Neon-on-AWS TLS 1.2+
ZevID ↔ other Zev products (ZPIP) TLS 1.2+; JWT RS256 with audience binding

Authentication + MFA

  • User authentication: email + password (bcrypt cost 12) + email-OTP via ZeptoMail.
  • MFA factors supported: TOTP (any RFC 6238 authenticator app), SMS via Termii, recovery codes.
  • MFA enforcement on sign-in: enforced when the user has accounts.totp_enabled=true or any account_phones row with mfa_enabled=true AND verified_at IS NOT NULL.
  • MFA enforcement on OAuth "Continue as" sign-in: per-account accounts.mfa_enforce_on_oauth flag; user-controlled in Security → Preferences.
  • Session tokens: identity tokens RS256-signed via JwksService with audience-bound aud claim. Refresh tokens hashed (SHA-256) and stored in sessions. Refresh-token rotation with reuse-detection is enabled.
  • Token lifetimes: access token 15 minutes; refresh token 7 days.
  • Sensitive-action MFA re-prompt: changing password, regenerating recovery codes, disabling MFA, and several admin actions require fresh MFA. Frontend uses requireTotp wrapper.
  • Per-account lockout: LockoutService applies short-window account-level lockouts on repeated OTP / MFA failures.

Security hardening

The following hardening toggles are enabled in production:

  • CSRF protection (CSRF_ENFORCE) — double-submit cookie on cookie-auth state-changing routes.
  • PKCE required (PKCE_REQUIRED) — every /auth/token exchange requires a PKCE verifier.
  • Strict redirect URIs (REDIRECT_URI_STRICT) — session-check redirect_uri must be in some active client's registered redirectUris[].
  • Internal-API IP enforcement (INTERNAL_ENFORCE_IP=true) — /v1/internal/* endpoints require the caller's IP to be in the INTERNAL_ALLOWED_IPS allow-list. The allow-list itself is held in operator-controlled config; it is not published here.

Key + secret management

  • Master keys (FIELD_ENCRYPTION_KEY, PHONE_HASH_PEPPER, ADMIN_JWT_SECRET, JWT_PRIVATE_KEY_BASE64, JWT_PUBLIC_KEY_BASE64) are set in the production environment with strong values.
  • Access: Operations team only.
  • PHONE_HASH_PEPPER rotation: treated as set-once (rotating requires re-hashing every row). The other master keys are rotatable as needed.

Network + perimeter

  • Public surface: accounts.zevop.com (user-facing) — fronted by Cloudflare (CDN + TLS termination + DDoS protection).
  • Private surface: /v1/internal/* endpoints gated by service-key auth + scope (ServiceKeyGuard) plus IP allow-list (per the hardening section above).
  • Per-route rate limits: @Throttle decorators on sensitive endpoints (signup, login, MFA verify, phone add / resend, SMS dispatch).

Logging + monitoring

  • Application errors → Better Stack (Sentry-compatible ingest at SENTRY_DSN).
  • Structured logs → Better Stack (Logtail).
  • Admin actions → admin_audit_logs table.
  • Auth events → login_events table.
  • ZPIP tokens → zpip_token_log table.

Backup + recovery

  • Database backup: Neon's default configuration (continuous WAL-based point-in-time recovery on Neon-on-AWS).
  • R2: object storage durability provided by Cloudflare R2.

Incident response

For breach notification (the 72-hour clock under NDPA §40), see ecosystem-wide incident response.

Vulnerability management

  • Dependency updates via the GitHub-side scanner (Dependabot).
  • Security advisories triaged by the engineering lead.