Sécurité Nexera Pay
Modèle de menaces adressé
| Menace | Mitigation |
|---|---|
| Vol de clé API | Bearer + HMAC signature obligatoire — clé seule ne suffit pas |
| Replay attack | X-Timestamp ±5 min + signature liée au timestamp |
| Double débit (double clic marchand) | Idempotency-Key obligatoire sur POST /payments, dédupe 24h |
| Brute-force signature | Rate limit 100 req/min/api_key, alerte >10 sig invalides/min |
| Man-in-the-middle | TLS 1.3 + HSTS preload + certificate pinning côté SDK |
| Injection SQL | Pydantic strict + SQLAlchemy paramétré, jamais de raw SQL avec user input |
| Injection JSON (bomb) | Body max 500 MB globalement (aligné avec KYC/catalog uploads), max 20 clés metadata, valeurs max 500 chars. /v1/payments reste typiquement <10 KB |
| CSRF | Pas de cookies sur l'API publique. Dashboard admin séparé avec token CSRF |
| Enumeration IDs | UUID v4 partout, jamais séquentiels |
| Timing attack sur secrets | hmac.compare_digest, réponses 401 uniformes |
| DDoS | Cloudflare WAF + rate limit L7 nginx + rate limit L7 app |
| PAN carte volé | Jamais stocké — saisi directement dans un iframe Cybersource Microform embedded dans la page Nexera. Tokenisation côté Cybersource, PCI DSS SAQ-A |
| Master key compromise | Fernet key isolée dans secret manager, rotation prévue |
| DB dump compromise | Secrets marchands chiffrés (Fernet), séparés dans table dédiée |
| Webhook replay chez marchand | Signature Stripe-style (t=ts,v1=sig), marchand vérifie timestamp |
| Provider down cascade | Circuit breaker 30s après 50% erreurs 1min, fail-fast |
| Marchand webhook down | Retry exponentiel 8 attempts, DLQ, circuit breaker 1h après 20 échecs |
Auth marchand — flow détaillé
POST /v1/payments HTTP/1.1
Host: pay.nexera.africa
Authorization: Bearer nex_live_a1b2c3d4...
X-Signature: e7f8a1b2c3d4e5f6... # HMAC-SHA256 hex, sans préfixe
X-Timestamp: 1786200000
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
Content-Length: 234
{ "amount": 10000, ... }Serveur exécute dans l'ordre :
Content-Length > 500 MB→ 413 immédiat (avant lire body). Ce plafond couvre les uploads KYC/catalog. Pour un/v1/payments, le body est typiquement <10 KB.Bearerprésent → sinon 401 auth-required- Lookup credential par
api_key(index unique) → sinon 401 api-key-invalid - IP client dans allowlist si configurée → sinon 403 ip-not-allowed
- Merchant status = ACTIVE → sinon 403 merchant-suspended
- Rate limit (INCR Redis clé bucket minute) → sinon 429 rate-limit-exceeded
X-Timestampprésent et |now-ts| < 5min → sinon 401 timestamp-expired- Signature =
HMAC-SHA256(secret, ts.method.path.body)en hex pur, sans préfixe (constant-time compare) → sinon 401 signature-invalid - Idempotency (POST /payments|/payouts) : si
Idempotency-Keydéjà vu → check request_hash match, sinon 409 idempotency-conflict, si match → return cached response - Traitement route
Chiffrement du secret marchand
Le secret doit être re-comparé au runtime avec HMAC (bcrypt one-way ne permet pas la vérif signature). Solution :
- Table
credential_encrypted_secretsséparée (principe de moindre privilège au niveau DB row) - Chiffrement Fernet (AES-128-CBC + HMAC-SHA256) avec clé maîtresse dans env
FERNET_MASTER_KEY - En prod : clé maîtresse dans secret manager (Vault, AWS Secrets Manager, GCP Secret Manager)
- Rotation : dual-key support planifié (Phase 4)
- Cache mémoire process (invalidé à chaque restart) pour éviter round-trip DB par requête
Un attaquant a besoin de 3 choses pour usurper une identité marchand :
- Dump de
merchant_credentials - Dump de
credential_encrypted_secrets - Accès au
FERNET_MASTER_KEY
Logs
- JSON structured (Loki-compatible)
- Redaction automatique : PAN →
123456******1234, MSISDN →243***45678, email →x***@x.xx - Aucun secret / API key / body jamais loggé
- Correlation ID (
X-Request-Id) propagé partout - Rétention : 90 jours logs, 10 ans transactions (obligation légale)
Headers HTTP sécurité (defense-in-depth)
App + Nginx :
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadX-Content-Type-Options: nosniffX-Frame-Options: DENYpartout, sauf/checkout/{id}/3ds-returnoù c'estSAMEORIGIN(nécessaire pour que l'iframe Cybersource StepUp puisse recevoir notre HTML après le challenge OTP)Content-Security-Policy: frame-ancestors 'self'sur/checkout/{id}/3ds-returnReferrer-Policy: no-referrerPermissions-Policy: geolocation=(), microphone=(), camera=()Server: Nexera(masquage version uvicorn)
Container
- Non-root user (
uid=10001) read_only: truefilesystem (sauf/tmpen tmpfs)no-new-privileges: true- Réseau Docker interne (pas d'exposition directe, Nginx en front)
Réponse incident
- Compromission suspectée api_key : révocation immédiate via admin
POST /admin/credentials/{id}/revoke, grace period 7 jours nouvelle clé - Compromission secret marchand : rotation Fernet master key (dual-key), re-chiffrement batch
- Attaque DDoS : Cloudflare "Under Attack" mode activable en 1 clic
- Bug critique : Feature flag global
MAINTENANCE_MODE→ renvoie 503 avecRetry-After
