211 lines
8.1 KiB
Markdown
211 lines
8.1 KiB
Markdown
# 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=<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.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
|