gh_UserManager/docs/SECURITY.md
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

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