Skip to main content
Safe Online Exam supports file-based alternatives for every sensitive environment variable, allowing secrets to be supplied through Docker or Kubernetes secret mounts without embedding plaintext values in environment files or shell arguments. This page covers the _FILE pattern, the OAuth token encryption keyring format, the compat vs enforce mode, the Docker Compose secrets profile, and safe rotation procedures for each class of secret.
Never place secret values in shell command arguments, terminal history, screenshots, log output, or version-controlled files. File-based secrets exist specifically to keep sensitive material out of the process environment and system logs. Protect every secrets directory and file with restrictive filesystem permissions before writing any content into it.

File-Based Secret Alternatives

For each of the sensitive variables below, the application accepts a mutually exclusive _FILE alternative. Set either the direct variable or its file alternative — never both.
The application explicitly rejects a configuration where both a direct variable and its _FILE counterpart are present in the environment. It reports the conflict and exits without reading either value. Unreadable paths are also reported as errors without echoing any secret contents.
Files are read exactly once during configuration startup. Required-value validation treats a missing or empty file result as equivalent to a missing direct variable — startup fails before listening with a descriptive error message.

OAuth Token Encryption Keyring

Safe Online Exam encrypts every stored Canvas OAuth access token and refresh token using AES-256-GCM. The keyring is a JSON object whose keys are string key IDs and whose values are 32-byte AES keys encoded as canonical base64url strings (no padding, URL-safe alphabet).
Each key value must decode to exactly 32 bytes. The application rejects any keyring entry with an invalid length, non-canonical encoding, or an illegal key ID character. Key IDs must match the pattern [A-Za-z0-9][A-Za-z0-9._-]{0,63}. OAUTH_TOKEN_ENCRYPTION_ACTIVE_KEY_ID identifies which key ID in the keyring is used for new token writes. Every read operation accepts any key ID present in the keyring, so old records remain decryptable after a key rotation as long as the retired key is not yet removed.

Generating a New AES Key

Use Node.js to generate a cryptographically random 32-byte base64url key:
Write the output directly to a file rather than passing it through a shell variable:

Keyring File Format

When using OAUTH_TOKEN_ENCRYPTION_KEYRING_FILE, write the full JSON keyring object to the file. The file must contain valid JSON with no BOM:
After a key rotation with multiple keys present:

Encryption Mode: enforce vs compat

OAUTH_TOKEN_ENCRYPTION_MODE controls how the application handles stored token records. Set OAUTH_TOKEN_ENCRYPTION_MODE=enforce in every production environment. The compat mode is documented only so that operators understand it is not a supported long-term state.

The db:encrypt-oauth-tokens Maintenance Command

After rotating to a new active key ID, run the bulk rewrite command to update every stored record to the new key before removing the old key from the keyring:
This command scans all rows in the canvas_oauth_tokens table and re-encrypts any record whose stored key ID does not match OAUTH_TOKEN_ENCRYPTION_ACTIVE_KEY_ID. It reports counts only and fails closed — if any row cannot be decrypted with the keys in the current keyring, the command exits with an error and leaves all remaining rows untouched. Only remove a retired key from the keyring after two successive runs of this command both report zero updated rows. This guarantees that every stored record has been rewritten under the active key.

Docker Compose Secrets Profile

The checked-in compose.secrets.yaml override implements the full file-based secrets pattern for Docker Compose deployments. Start it with .env.compose.secrets.example as the environment file. The override does the following for every service that handles secrets (postgres, migrate, app, cleanup, and encrypt-oauth-tokens):
  • Clears the corresponding direct secret variable to an empty value.
  • Supplies the _FILE path pointing to /run/secrets/<name>.
  • Mounts only the named secret files into each container via Docker’s secrets: mechanism.
The PostgreSQL service also receives the database password through its native POSTGRES_PASSWORD_FILE input via the same mechanism.

Directory Permissions

The SECRETS_DIRECTORY (default ./secrets) is the host directory from which Compose reads each named file. On Linux hosts, set the containing directory to mode 0700 so that only the deploying user can list its contents:
Individual secret files within the directory can be mode 0644 — Docker mounts only the named files into each container, and the 0700 directory prevents enumeration from other accounts on the same host. Required files in SECRETS_DIRECTORY for the secrets profile:

Secret Rotation Effects on Live Sessions

Each secret class has a distinct impact when rotated. Rotate one secret at a time and follow the create → deploy → smoke-test → disable-old sequence.
Rotating SESSION_SECRET immediately invalidates all active user sessions. Every user will be redirected to re-authenticate with Canvas on their next request. There is no grace period. Schedule rotation during a low-traffic window and communicate the expected re-authentication to users in advance.
Rotating STATE_ENCRYPTION_KEY invalidates all outstanding opaque LTI and OAuth state tokens. Any user in the middle of an LTI launch or Canvas OAuth authorisation flow at the moment of rotation will see an error and need to restart that flow. Sessions that have already completed are unaffected.
Rotating the LTI signing key requires coordination with Canvas. The new public key must appear in the JWKS endpoint at ${TOOL_URL}/lti/jwks before Canvas can verify launches signed with it. After deploying the new key, re-register the tool in Canvas by fetching the configuration from ${TOOL_URL}/lti/config so Canvas stores the updated JWKS reference.
To rotate an OAuth token encryption key safely:
  1. Add a new 32-byte base64url key under a new key ID to the keyring JSON.
  2. Update OAUTH_TOKEN_ENCRYPTION_ACTIVE_KEY_ID to the new key ID.
  3. Deploy the updated keyring and active key ID.
  4. Run npm run db:encrypt-oauth-tokens:built to rewrite all stored tokens to the new key.
  5. Run the command a second time and confirm zero updated rows.
  6. Only after both runs report zero updates, remove the old key from the keyring JSON and deploy again.
The old key must remain in the keyring until all records have been rewritten. Removing it prematurely causes decryption failures for any record that has not yet been rewritten.
Rotating SEB_CONFIG_ENCRYPTION_CERT_PEM or SEB_CONFIG_ENCRYPTION_CERT_PATH requires distributing the matching new private identity to all managed SEB client devices before deploying the new certificate. Existing downloaded SEB configurations encrypted with the old certificate will no longer open after rotation. Issue fresh SEB configurations for all active assessments after deploying the new certificate.