feat: Implement session secret rotation support
Adds support for gradual session secret rotation without forcing logout: - Add BANGUI_SESSION_SECRET_PREVIOUS config field for rotation window - Implement unwrap_session_token_with_rotation() to accept tokens signed with either current or previous secret - Update validate_session() to transparently accept old tokens during rotation - Update logout() to accept tokens from both secrets - Add comprehensive logging for rotation events and metrics - Add 8 new tests covering all rotation scenarios - Update documentation with step-by-step rotation strategy - Update .env.example with previous secret field Key features: - No forced logout: old tokens continue working during rotation window - Transparent validation: old tokens are automatically logged for monitoring - Production-safe: can rotate secrets without service interruption - Metrics-ready: logs track token rotation for observability Rotation workflow: 1. Generate new secret and set BANGUI_SESSION_SECRET 2. Set BANGUI_SESSION_SECRET_PREVIOUS to old secret 3. Wait for old tokens to expire (≥ session_duration_minutes) 4. Unset BANGUI_SESSION_SECRET_PREVIOUS to complete rotation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2166,6 +2166,126 @@ BANGUI_SESSION_SECRET="your-32-character-minimum-secret-here"
|
||||
BANGUI_SESSION_SECRET="set-this-to-a-32-character-minimum-secret"
|
||||
```
|
||||
|
||||
### Session Secret Rotation
|
||||
|
||||
**Problem:** If a session secret leaks, all active sessions become compromised and an attacker can forge new tokens. Rotating the secret invalidates forged tokens but may require all users to log out if rotation is done all at once.
|
||||
|
||||
**Solution:** BanGUI supports gradual secret rotation without forcing logout. During rotation:
|
||||
|
||||
1. All new tokens are signed with the current secret
|
||||
2. Old tokens signed with the previous secret are still accepted
|
||||
3. Tokens using the previous secret are transparently validated and logged
|
||||
4. Once all old tokens expire naturally, disable the rotation by unsetting the previous secret
|
||||
|
||||
**Rotation Strategy (Step-by-Step):**
|
||||
|
||||
#### 1. Generate a New Secret
|
||||
|
||||
Before rotation, generate a fresh secret:
|
||||
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
#### 2. Start Rotation (Without Stopping the Service)
|
||||
|
||||
Update your configuration **simultaneously** on all deployment replicas:
|
||||
|
||||
```bash
|
||||
# .env (or ConfigMap in Kubernetes)
|
||||
BANGUI_SESSION_SECRET="<new-secret>" # Current (new) secret
|
||||
BANGUI_SESSION_SECRET_PREVIOUS="<old-secret>" # Previous (old) secret
|
||||
```
|
||||
|
||||
Or in Kubernetes:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: BANGUI_SESSION_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bangui-secrets
|
||||
key: current-secret
|
||||
- name: BANGUI_SESSION_SECRET_PREVIOUS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bangui-secrets
|
||||
key: previous-secret
|
||||
```
|
||||
|
||||
**Key Point:** All replicas must know both secrets to accept old tokens.
|
||||
|
||||
#### 3. Monitor Token Rotation
|
||||
|
||||
Tokens signed with the previous secret are automatically validated and logged:
|
||||
|
||||
```
|
||||
event=session_token_rotated_in_place session_id=42 old_secret_fragment=abc123 new_secret_fragment=def456
|
||||
```
|
||||
|
||||
These logs let you track how many sessions are still using old tokens.
|
||||
|
||||
#### 4. Wait for Old Tokens to Expire
|
||||
|
||||
Monitor the application logs and wait until:
|
||||
- No new `session_token_rotated_in_place` events appear (all old tokens have been used or expired)
|
||||
- Session duration (default: 480 minutes) + grace period has elapsed since the previous secret was enabled
|
||||
- Example: If sessions last 480 minutes, wait at least 8 hours from enabling the previous secret
|
||||
|
||||
#### 5. Complete Rotation
|
||||
|
||||
Once all old tokens have expired, remove the previous secret:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
BANGUI_SESSION_SECRET="<new-secret>"
|
||||
# BANGUI_SESSION_SECRET_PREVIOUS is now unset or empty
|
||||
```
|
||||
|
||||
**Important:** Keep the previous secret configured for at least `session_duration_minutes` (default 480 minutes / 8 hours) to avoid rejecting tokens that are still valid.
|
||||
|
||||
**Metrics & Logging:**
|
||||
|
||||
The auth service logs rotation events for observability:
|
||||
|
||||
- `session_token_rotated_in_place` — Logged when a token signed with the previous secret is validated during the rotation window
|
||||
- `session_token_re_signed_after_rotation` — Logged in `unwrap_session_token_with_rotation()` when the previous secret validates a token
|
||||
- `old_secret_fragment` / `new_secret_fragment` — First 6 characters of the SHA256 hash of each secret (for non-sensitive correlation without logging actual secrets)
|
||||
- `session_id` — Database ID of the rotated session
|
||||
|
||||
**Example Rotation Sequence:**
|
||||
|
||||
```
|
||||
Time Config Event Logged
|
||||
────────────────────────────────────────────────────────────────
|
||||
T=0 current=old (normal operation)
|
||||
previous=<empty>
|
||||
|
||||
T=5m current=new session_token_rotated_in_place
|
||||
previous=old (user session S1 validated with old secret)
|
||||
|
||||
T=30m current=new (no more old tokens, all new tokens use current)
|
||||
previous=old
|
||||
→ Still keep old secret set
|
||||
|
||||
T=500m Enough time passed (old session S1 has expired)
|
||||
(480 min session + grace)
|
||||
|
||||
T=510m current=new (rotation complete)
|
||||
previous=<empty>
|
||||
```
|
||||
|
||||
**Avoiding Common Mistakes:**
|
||||
|
||||
❌ **Don't:** Rotate the secret and immediately unset the previous one → Old tokens will be rejected, forcing logout
|
||||
✓ **Do:** Keep the previous secret for at least `session_duration_minutes`
|
||||
|
||||
❌ **Don't:** Rotate without updating all replicas → Some replicas reject old tokens, others accept them → Inconsistent behavior
|
||||
✓ **Do:** Deploy config to all replicas simultaneously (via ConfigMap, Helm, or orchestrator)
|
||||
|
||||
❌ **Don't:** Use the same secret for development and production → Leaked dev secret can compromise prod
|
||||
✓ **Do:** Generate unique secrets per environment
|
||||
|
||||
### Session Cookie Security
|
||||
|
||||
The `session_cookie_secure` configuration controls the `Secure` flag on the session cookie. This flag prevents browsers from sending the session cookie over unencrypted HTTP.
|
||||
|
||||
Reference in New Issue
Block a user