Rotating application-managed keys¶
PrairieLearn accepts either a scalar string or an ordered, nonempty array for secretKey, databaseEncryptionKey, prairieTestSharedAuthSecret, and stripeWebhookSigningSecret. Empty arrays and invalid array members are rejected during configuration validation.
For credentials PrairieLearn uses to create signatures or ciphertext, the first key is active and every configured key is accepted for verification or decryption. stripeWebhookSigningSecret is verification-only, but its order remains deterministic. Continue to use scalar configuration until array-compatible application code has been deployed everywhere that reads a shared credential.
Standard rollout¶
Given an old key and a new key:
- Deploy array-compatible code while retaining the scalar old key.
- Configure
[old, new]and refresh every application instance. - Configure
[new, old]and refresh every application instance. New signatures and ciphertext now use the new key. - Satisfy the credential-specific old-key retirement conditions below.
- Configure
[new]and refresh every application instance. This step is not yet supported fordatabaseEncryptionKey; leave it configured as[new, old]until the rekey and audit follow-up described below is available.
Do not skip the [old, new] stage. It ensures every instance can verify with the new key before any instance begins signing with it. During the subsequent rolling change to [new, old], updated instances can sign with the new key while instances still using [old, new] can verify signatures from either key.
secretKey¶
secretKey signs session cookies and the signed tokens used for CSRF protection, assessment-password cookies, load testing, workspaces, jobs, variants, actions, and trace sampling. Session cookies have a maximum lifetime of sessionStoreExpireSeconds (30 days by default); assessment-password, load-test, and variant checks are bounded at 24 hours or less in their verification paths, and workspace authorization cookies default to one minute.
Job-sequence tokens accepted through the Socket.IO join path, along with some page-scoped CSRF and other socket tokens, do not have a hard time limit. Keep the fallback key for at least the configured session lifetime and any longer operationally configured artifact lifetime. Retiring the fallback after that window can still invalidate an exceptionally old open form or socket page; a page refresh obtains a token signed by the new active key.
The trace-sampling cookie generator accepts a scalar or array-valued secretKey and explicitly signs with array index 0.
databaseEncryptionKey¶
The known persisted use of databaseEncryptionKey is course_instance_ai_grading_credentials.encrypted_secret_key. New ciphertext uses the first key and existing unversioned AES-256-GCM ciphertext is decrypted by trying every configured key.
Re-encryption and an explicit version/key-ID envelope are intentionally deferred. Untouched rows never age out, so do not remove a fallback key that may have encrypted existing rows. A follow-up must define the backwards-compatible envelope, provide a resumable operator command that locks and processes bounded batches, re-encrypts legacy or fallback-key ciphertext with the primary key, and then rereads every row to prove it is decryptable using only the primary key before retirement.
Database backups retain the ciphertext present when they were captured. Retain old keys under the same controls as retained backups; restoring a pre-rekey backup must restore the corresponding fallback keys and run the future rekey/audit operation before those keys are removed from the restored environment.
prairieTestSharedAuthSecret¶
PrairieLearn and PrairieTest both mint and verify JWTs with this credential. Do not switch either service to array configuration until array-compatible versions of both services are deployed. Apply each rollout configuration to both services and refresh all instances before moving to the next stage.
The longest current shared-auth token lifetime is five minutes for PrairieLearn's end-exam JWT; interactive authentication JWTs expire after one minute. After every instance of both services is using [new, old], wait at least five minutes plus deployment and clock-skew margin before removing old; ten minutes after the last old-primary instance is stopped is a conservative minimum.
stripeWebhookSigningSecret¶
PrairieLearn verifies each inbound Stripe webhook against every configured signing secret and does not use this credential to mint outbound signatures. Keep both secrets configured for the entire provider-side overlap, then remove the old secret only after Stripe no longer signs deliveries with it. This does not change the outbound stripeSecretKey.