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=trueor anyaccount_phonesrow withmfa_enabled=true AND verified_at IS NOT NULL. - MFA enforcement on OAuth "Continue as" sign-in: per-account
accounts.mfa_enforce_on_oauthflag; user-controlled inSecurity → Preferences. - Session tokens: identity tokens RS256-signed via
JwksServicewith audience-boundaudclaim. Refresh tokens hashed (SHA-256) and stored insessions. 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
requireTotpwrapper. - Per-account lockout:
LockoutServiceapplies 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/tokenexchange requires a PKCE verifier. - Strict redirect URIs (
REDIRECT_URI_STRICT) —session-check redirect_urimust be in some active client's registeredredirectUris[]. - Internal-API IP enforcement (
INTERNAL_ENFORCE_IP=true) —/v1/internal/*endpoints require the caller's IP to be in theINTERNAL_ALLOWED_IPSallow-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_PEPPERrotation: 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:
@Throttledecorators 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_logstable. - Auth events →
login_eventstable. - ZPIP tokens →
zpip_token_logtable.
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.