8.1 KiB
8.1 KiB
Security Policy
Overview
The Identity Platform implements production-grade security without false claims. This document describes the security model, controls, and operational requirements.
Cryptographic Design
Asymmetric Signing (RS256)
- Algorithm: RS256 (RSA Signature with SHA-256)
- Private key: Held ONLY by the Identity Platform (used for signing tokens)
- Public key: Distributed via JWKS endpoint (
/.well-known/jwks.json) - Products verify tokens using the public key — they cannot forge tokens
- Private key location: Environment-defined in development (
SIMPLE_JWT_SIGNING_KEY), mounted volume/file in production
Key properties:
- Products never have the signing key
- Adding a new product requires no key distribution — just JWKS discovery
- Key rotation is supported with an overlap window
Password Storage
- Backend: PBKDF2 with HMAC-SHA256 (Django default)
- Iterations: 722,768 (OWASP-recommended as of 2024)
- Salt: Per-password random salt via Django's password hasher
- Future: Migration path to Argon2id
Key Rotation (Planned)
- JWKS endpoint returns the current public key(s)
- Multiple keys can be published with
kidheader support - Overlap window allows old tokens to be validated during rotation
- Private key rotated via operational process (not code change)
Token Security
Access Token
- Algorithm: RS256 (JWT)
- Lifetime: 15 minutes (configurable)
- Claims:
user_id,user_status,identity_key, optionalproduct_key, optionalorganization_id,exp,iat,iss,jti - Storage: In-memory on client (NOT in localStorage); server never sees raw access token again after issuance
Refresh Token
- Algorithm: RS256 (JWT)
- Lifetime: 30 days (configurable)
- Storage: Hashed in database (not reversible)
- Rotation: New refresh token issued on each use; old one revoked with
replaced_byset - Revocation: Instant via
/auth/logout(blacklist + session revoke)
Token Blacklist
- Mechanism:
token_blacklistapp withTokenBlacklistmodel - When added: Logout, password change, password reset, session revocation
- Check: Middleware validates every incoming access token against the blacklist
- Cleanup: Background job removes expired blacklist entries (cron:
python manage.py shell— cleanup script)
Brute-force Protection
- Login rate limit: 5 attempts per 5 minutes per IP per email
- Auto-lockout: After threshold exceeded →
login_lockedsecurity event (severity: high) - Unlock: Time-based (15 min) or admin override
Network & API Security
CORS
- Config:
CORS_ALLOWED_ORIGINS(environment-configured) - Credentials:
CORS_ALLOW_CREDENTIALS=True(cookies for browser sessions) - Default:
http://localhost:3000,http://127.0.0.1:3000
CSRF
- API: JWT Bearer tokens are immune to CSRF (no cookies for auth)
- Django Session: CSRF middleware active for admin interface
- Trusted origins:
CSRF_TRUSTED_ORIGINSenvironment-configured
Rate Limiting
| Endpoint | Limit | Scope |
|---|---|---|
/v1/auth/login |
5/min | per IP + per email |
/v1/auth/refresh |
20/min | per user |
/v1/auth/logout |
10/min | per user |
/auth/password/reset |
3/hour | per IP |
| All API endpoints | 100/min | per authenticated user |
| Unauthenticated access | 20/min | per IP |
Rate limiter uses Redis cache (LocMemCache in dev). Key format: {endpoint}:{identifier}:{window}.
Audit & Monitoring
Security Events
Every security-relevant action creates an immutable SecurityEvent:
| Action | Event Type | Severity |
|---|---|---|
| Successful login | login_success |
info |
| Failed login | login_failed |
medium |
| Account locked | login_locked |
high |
| Logout | logout |
low |
| Password changed | password_change |
medium |
| Password reset | password_reset |
medium |
| Email verified | email_verified |
low |
| Phone verified | phone_verified |
low |
| MFA enrolled | mfa_enabled |
low |
| MFA disabled | mfa_disabled |
high |
| Session revoked | session_revoked |
medium |
| Token refreshed | token_refreshed |
low |
Events are immutable — no DELETE/UPDATE allowed via API. They are append-only audit records.
Event Retention
- Events: 2 years (compliance)
- Sessions: 90 days after expiration (debugging)
- Tokens: 30 days after expiry (token chain reconstruction)
- Blacklist entries: 15 minutes after access token expiry
Service-to-Service Authentication
Current (v1.0): API Key
- Products authenticate with
X-API-Keyheader - API Key =
client_id:client_secret(from Application registration) - Validated against
Applicationmodel (status=active) - Rate limited at service level
Future (v2.0): OAuth2 Client Credentials
- Products use
grant_type=client_credentials - Platform issues service-to-service JWT signed with RS256
- Service JWTs have
scopeclaim (audience-scoped) - mTLS as transport layer option
Secret Management
Secrets in Repository: NEVER
- No secrets committed:
.envfiles are gitignored - Private key: NOT in repository. Loaded from env or mounted file in production
- Database password: Environment (
DATABASE_URL) - Redis password: Environment (
REDIS_URL)
Environment Configuration
# Required in production
DJANGO_SECRET_KEY=<64-char random string>
DATABASE_URL=postgres://user:pass@postgres:5432/identity
REDIS_URL=redis://redis:6379/0
# JWT signing (production: load from mounted file)
DJANGO_SIGNING_KEY=<RSA private key PEM>
# CORS
CORS_ALLOWED_ORIGINS=https://bermooda.example.com,https://hamsoo.example.com
CSRF_TRUSTED_ORIGINS=https://id.example.com
Development
DATABASE_URL=sqlite:///dev.dbfor local devDJANGO_DEBUG=truefor local devDJANGO_ENV=productionenforcesDJANGO_SECRET_KEYmust be set (no dev fallback)
MFA (Multi-Factor Authentication)
Current (v1.0)
- TOTP:
totp_secretfield on User,mfa_enabledflag - Verification: 6-digit codes via RFC 6238
- Recovery: Not yet implemented (TODO)
Planned (v1.2)
- Backup codes: 10 single-use codes
- WebAuthn/Passkeys: Platform authenticator support
- MFA policies: Per-organization enforcement
- Step-up auth: Re-auth for sensitive actions (org deletion, secret rotation)
Threat Model
| Threat | Mitigation |
|---|---|
| Token forgery | RS256 — products can't forge without private key |
| Token replay | Short 15-min access tokens; refresh token rotation |
| Session hijacking | Session tied to IP+UA fingerprint; user-agent mismatch detection |
| Brute force | Rate limiting + auto-lockout |
| Account takeover | Password breach check (planned); MFA (available) |
| Privilege escalation | Contextual roles (org-level, not global); product-layer roles |
| Data leakage | No product-domain data in identity; separate databases/services |
| Insider threat | Audit log (immutable); admin actions all logged |
| CSRF | JWT Bearer tokens (no cookies for auth) |
| DoS | Rate limiting; pagination; health checks |
Security Operations
Incident Response
- Detection: SecurityEvent with severity HIGH/CRITICAL → alert
- Triage: Admin reviews event metadata (IP, UA, user, context)
- Response: Revoke session, reset password, disable MFA, blacklist tokens
- Investigation: Full audit log available via
/admin/and/v1/security/events/ - Post-mortem: Documented with root cause analysis
Security Headers
Content-Security-Policy: Default-denyX-Content-Type-Options: nosniffX-Frame-Options: DENYStrict-Transport-Security: max-age=31536000; includeSubDomainsReferrer-Policy: strict-origin-when-cross-origin
(These are handled by Django middleware + nginx in production)
Security Testing
- JWT signature verification (RS256)
- Token blacklist on logout
- Token blacklist on password change
- Rate limiting on login
- Brute-force lockout
- Session revocation
- Password complexity validation (min 10 chars)
- JWT payload contains no sensitive data
- Private key not accessible from Products
- All security events logged with IP + UA