feat(security): DB-backed rotating OIDC global secret (SecretRotator)#300
Conversation
Replace the hard-coded fosite GlobalSecret in the OIDC authenticator with a randomly generated, database-persisted secret that rotates on the configured cookie key-rotation interval. Older secrets stay valid for verification for the session lifetime so in-flight artifacts keep working across a rotation. - lib/security/secretrotator.go: HKDF-derived rotating secrets, interval rotation + overlapping verification window + optional legacy-static-secret migration (port of etherpad-lite's SecretRotator). - Dedicated secret_rotation table (migration 006) + SecretMethods on the DataStore across SQLite/MySQL/Postgres/Memory via squirrel. MySQL declares the prefix index inline (no CREATE INDEX IF NOT EXISTS). - Wire into lib/api/oidc/authenticator.go: rotation rebuilds the fosite provider under an RWMutex with a fresh Config each time (race-free); endpoints read via currentProvider(); Stop() on shutdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Code Review by Qodo
1. Unvalidated params can panic
|
PR Summary by QodoRotate OIDC fosite GlobalSecret via DB-backed SecretRotator WalkthroughsDescription• Replace hard-coded OIDC HMAC secret with DB-persisted rotating secrets. • Add secret_rotation table + datastore methods across SQLite/MySQL/Postgres/Memory. • Rebuild fosite provider on rotation under RWMutex; stop rotator on shutdown; add tests. Diagramgraph TD
R["OIDC Endpoints"] --> A["Authenticator"] --> P{{"fosite Provider"}} --> GS["Rotated secrets"]
SR(["SecretRotator"]) --> RB["rebuildProvider()"] --> A
SR --> DS["SecretMethods"] --> DB[("secret_rotation")]
subgraph Legend
direction LR
_m["Module"] ~~~ _p(["Background task"]) ~~~ _e{{"External"}} ~~~ _d[("Database")]
end
High-Level AssessmentThe following are alternative approaches to this PR: 1. Persist full derived secrets (not HKDF params)
2. Mutate a shared fosite.Config instead of rebuilding provider
3. Delegate rotation to external KMS/HSM-managed key versions
Recommendation: Keep the PR’s approach (HKDF-derived secrets with DB-persisted params + provider rebuild under RWMutex). It strikes a good balance between operational simplicity (single table, multi-instance cooperation), security (no hard-coded secret; overlapping verification window), and runtime safety (fresh fosite.Config per rotation avoids in-place mutation races). The main alternatives either increase secret exposure (persisting derived secrets) or increase concurrency risk (mutating shared config). File ChangesEnhancement (3)
Tests (1)
Other (7)
|
| func mod(a, n int64) int64 { return ((a % n) + n) % n } | ||
|
|
||
| func intervalStart(t, interval int64) int64 { return t - mod(t, interval) } | ||
|
|
||
| // deriveSecrets derives all relevant secrets for one parameter set as of now. | ||
| func (r *SecretRotator) deriveSecrets(p secretParams, now int64) ([][]byte, error) { | ||
| alg := algorithms[p.AlgID] | ||
| if p.Interval == nil { | ||
| s, err := alg.derive(p.AlgParams, "") | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return [][]byte{s}, nil | ||
| } | ||
| iv := *p.Interval | ||
| t0 := intervalStart(now, iv) | ||
| // Start of the first interval covered by these params, backdated by iv to |
There was a problem hiding this comment.
1. Unvalidated params can panic 🐞 Bug ☼ Reliability
SecretRotator derives secrets from DB-persisted JSON without validating AlgID or Interval, so a corrupted row (AlgID out of range or Interval=0) can trigger slice-bounds or divide-by-zero panics during update()/scheduled rotation and crash the process.
Agent Prompt
## Issue description
`SecretRotator` consumes persisted `secretParams` rows and uses `AlgID` and `Interval` directly. If a row is corrupted/unexpected (e.g., `algId` out of range, `interval` is 0/negative, or `keyLen` is nonsensical), the rotator can panic (index out of range, divide-by-zero in modulo, or an infinite loop risk when stepping by `iv`). Because `update()` runs on a timer, this can crash the service at runtime.
## Issue Context
The rotator reads JSON payloads from `secret_rotation` and attempts to derive secrets for all rows on every update.
## Fix Focus Areas
- Add defensive validation before using persisted params:
- Reject `AlgID < 0 || AlgID >= len(algorithms)`.
- If `Interval != nil`, require `*Interval > 0`.
- Consider clamping/validating HKDF `KeyLen` to an expected safe range (e.g., 32) to avoid empty secrets or huge allocations.
- On invalid rows: log a warning and skip deriving; optionally delete the bad row.
- Validate constructor inputs too (e.g., `r.interval > 0`) and return an error from `Start()` if invalid.
### Code pointers
- file: `lib/security/secretrotator.go`
- Validate interval/alg usage in `deriveSecrets()` and `intervalStart()` call sites.
- Validate DB-loaded params in `update()` before appending to `allParams` / calling `deriveSecrets()`.
#### Fix Focus Areas (exact)
- lib/security/secretrotator.go[199-241]
- lib/security/secretrotator.go[271-393]
- lib/security/secretrotator.go[151-165]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…sing Addresses Qodo review on #300: - Validate DB-persisted params before use: skip (and delete) rows with an out-of-range AlgID or non-positive Interval so a corrupted/tampered row can no longer panic a scheduled rotation (slice index / divide-by-zero). Guard HKDF keyLen too, and reject a non-positive interval in Start(). - Secrets() now deep-copies each inner []byte so callers cannot mutate the rotator's internal signing/verification secrets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the Qodo review in
|
What
Replaces the hard-coded fosite
GlobalSecretin the OIDC authenticator ("some-cool-secret-that-is-32bytes") with a randomly generated, database-persisted secret that rotates on the configured cookie key-rotation interval. Older secrets stay valid for verification for the session lifetime, so in-flight artifacts keep working across a rotation. Port of etherpad-lite'sSecretRotator.How
lib/security/secretrotator.go— HKDF-derived rotating secrets: array of active secrets (first = used to sign, all = accepted for verification), interval rotation + overlapping lifetime window, optional legacy-static-secret migration.secret_rotationtable (migration 006) +SecretMethodsonDataStore, implemented across SQLite/MySQL/Postgres/Memory via squirrel. MySQL declares the prefix index inline inCREATE TABLE(noCREATE INDEX IF NOT EXISTS).lib/api/oidc/authenticator.go: each rotation rebuilds the fosite provider under anRWMutexwith a fresh*fosite.Config(never mutated in place → race-free); endpoints read viacurrentProvider();Stop()on shutdown.Testing
White-box tests in
lib/security(rotation, overlapping window, multi-instance, expiry, legacy secret).go build ./...,go vet, and DB/OIDC tests green (incl. MySQL/Postgres containers).🤖 Generated with Claude Code