# 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 `kid` header 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`, optional `product_key`, optional `organization_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_by` set - **Revocation**: Instant via `/auth/logout` (blacklist + session revoke) ### Token Blacklist - **Mechanism**: `token_blacklist` app with `TokenBlacklist` model - **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_locked` security 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_ORIGINS` environment-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-Key` header - API Key = `client_id:client_secret` (from Application registration) - Validated against `Application` model (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 `scope` claim (audience-scoped) - mTLS as transport layer option ## Secret Management ### Secrets in Repository: NEVER - **No secrets committed**: `.env` files 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 ```bash # 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= # CORS CORS_ALLOWED_ORIGINS=https://bermooda.example.com,https://hamsoo.example.com CSRF_TRUSTED_ORIGINS=https://id.example.com ``` ### Development - `DATABASE_URL=sqlite:///dev.db` for local dev - `DJANGO_DEBUG=true` for local dev - `DJANGO_ENV=production` enforces `DJANGO_SECRET_KEY` must be set (no dev fallback) ## MFA (Multi-Factor Authentication) ### Current (v1.0) - **TOTP**: `totp_secret` field on User, `mfa_enabled` flag - **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 1. **Detection**: SecurityEvent with severity HIGH/CRITICAL → alert 2. **Triage**: Admin reviews event metadata (IP, UA, user, context) 3. **Response**: Revoke session, reset password, disable MFA, blacklist tokens 4. **Investigation**: Full audit log available via `/admin/` and `/v1/security/events/` 5. **Post-mortem**: Documented with root cause analysis ### Security Headers - `Content-Security-Policy`: Default-deny - `X-Content-Type-Options`: nosniff - `X-Frame-Options`: DENY - `Strict-Transport-Security`: max-age=31536000; includeSubDomains - `Referrer-Policy`: strict-origin-when-cross-origin (These are handled by Django middleware + nginx in production) ## Security Testing - [x] JWT signature verification (RS256) - [x] Token blacklist on logout - [x] Token blacklist on password change - [x] Rate limiting on login - [x] Brute-force lockout - [x] Session revocation - [x] 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