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:
2026-05-01 18:01:11 +02:00
parent 67b26a3ef7
commit 8138857ee1
8 changed files with 359 additions and 42 deletions

View File

@@ -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.