Skip to content

Secrets Operations Runbook

Use this as the primary operator runbook for secret setup, bootstrap, rotation, and incident response.

Scope

This runbook covers:

  • API auth/session signing secrets
  • API <-> gateway shared trust secrets
  • Credential profile encryption key lifecycle
  • Admin bootstrap password handling
  • Postgres credential variables used by Compose deployments

Secret Inventory

VariableUsed byPurposeProduction requirement
JWT_SECRETAPISigns user auth JWTsRequired, strong value (>=32 chars)
JWT_GATEWAY_SECRETAPI + GatewaySigns/verifies datasource session tokensRequired, strong value (>=32 chars)
GATEWAY_SERVICE_TOKENAPI + GatewayAuth for gateway internal introspection/revocation callsRequired, strong value (>=32 chars)
CREDENTIAL_ENCRYPTION_KEYAPIEncrypts datasource credential profile secrets at restRequired, valid base64-encoded 32-byte key
ADMIN_PASSWORDAPI bootstrap onlyOne-time initial admin creation when CREATE_ADMIN=trueBootstrap-only; disable after first login
FREEBOARD_POSTGRES_URLAPI (containerized)App DB connection stringRequired for containerized runtime
POSTGRES_USER / POSTGRES_PASSWORDPostgres containerDB bootstrap account used for compose initRequired for compose postgres init
POSTGRES_DBPostgres containerDefault database nameRequired for compose postgres init

Env Precedence and Runtime Expectations

API env precedence (implemented)

  1. Existing process environment (shell/CI/orchestrator)
  2. packages/api/.env (optional local override file)
  3. Repo root .env
  4. Code defaults

Gateway env behavior

  • Gateway reads process environment first.
  • dotenv/config loads .env if present (without overwriting already-set process vars).
  • Missing values fall back to gateway defaults (development defaults are intentionally insecure and must not be used in production).

Docker Compose interpolation

  • Shell-exported vars override .env file values.
  • .env values are then used for ${VAR} interpolation.
  • Compose defaults (${VAR:-default}) are last fallback.
  • Required vars (${VAR:?error}) fail fast at startup.

Development vs Production Contract

Development:

  • Local .env is acceptable.
  • API/gateway may use development defaults.
  • CREATE_ADMIN=true may be used for first local bootstrap only.

Production:

  • Inject secrets from a managed secret store, not committed files.
  • Keep CREATE_ADMIN=false after initial bootstrap.
  • Use explicit strong values for JWT_SECRET, JWT_GATEWAY_SECRET, GATEWAY_SERVICE_TOKEN.
  • Use valid base64 32-byte CREDENTIAL_ENCRYPTION_KEY.
  • Use non-default Postgres credentials.

Initial Setup and Bootstrap

  1. Generate secrets:
bash
node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))" # JWT_SECRET
node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))" # JWT_GATEWAY_SECRET
node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))" # GATEWAY_SERVICE_TOKEN
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"    # CREDENTIAL_ENCRYPTION_KEY
  1. Set runtime env values (.env for local, managed secret store for production).
  2. For first admin bootstrap only:
    • CREATE_ADMIN=true
    • set ADMIN_EMAIL + strong ADMIN_PASSWORD
  3. Start stack and log in as admin.
  4. Set CREATE_ADMIN=false and remove bootstrap ADMIN_PASSWORD from active runtime secret material.

Rotation Playbooks

JWT_SECRET (user auth JWT signing key)

  1. Generate a new strong value.
  2. Deploy API with new value.
  3. Expect existing user sessions to become invalid (users re-authenticate).
  4. Verify login, registration/invite flows, and password reset flows.

JWT_GATEWAY_SECRET (datasource session token key)

  1. Generate a new strong value.
  2. Deploy API and gateway together with the same new value.
  3. Expect active datasource sessions/realtime streams to reconnect/re-auth.
  4. Verify HTTP datasource fetch and realtime subscriptions.

GATEWAY_SERVICE_TOKEN (gateway service auth token)

  1. Generate a new strong value.
  2. Deploy API and gateway together with matching token.
  3. Verify gateway introspection path (/internal/gateway/datasource-introspect) via normal datasource execution.

CREDENTIAL_ENCRYPTION_KEY (credential profile at-rest key)

Follow the dedicated runbook:

This rotation requires running npm run credentials:reencrypt with old/new keys before switching runtime key.

ADMIN_PASSWORD (bootstrap vs ongoing account maintenance)

  • ADMIN_PASSWORD env var is for bootstrap (CREATE_ADMIN=true) only.
  • Ongoing password changes should use the normal reset flows:
    • user self-service "Forgot Password"
    • admin-issued reset tokens from Admin Console for managed accounts

Postgres Credentials (FREEBOARD_POSTGRES_URL, POSTGRES_*)

For compose-backed Postgres:

  1. Create/update DB credentials in a maintenance window.
  2. Update FREEBOARD_POSTGRES_URL, POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB in secret store.
  3. Restart API and rerun schema status checks (npm run db:schema:status) against the updated connection.

Credential rotation should be validated with API login and datasource execution checks before closing the change.

Secret Storage Patterns (Optional Production Patterns)

Use the pattern that fits your deployment model. Local .env remains valid for development.

Docker Secrets Pattern

If you store secrets as files under /run/secrets/*, map them to env vars before service start (wrapper command pattern):

yaml
services:
  freeboard-api:
    secrets:
      - jwt_secret
      - jwt_gateway_secret
      - gateway_service_token
      - credential_encryption_key
    entrypoint: ["/bin/sh", "-ec"]
    command: >
      export JWT_SECRET="$(cat /run/secrets/jwt_secret)";
      export JWT_GATEWAY_SECRET="$(cat /run/secrets/jwt_gateway_secret)";
      export GATEWAY_SERVICE_TOKEN="$(cat /run/secrets/gateway_service_token)";
      export CREDENTIAL_ENCRYPTION_KEY="$(cat /run/secrets/credential_encryption_key)";
      exec node packages/api/dist/index.js
secrets:
  jwt_secret:
    file: ./secrets/jwt_secret.txt

Apply the same pattern for gateway service secrets (JWT_GATEWAY_SECRET, GATEWAY_SERVICE_TOKEN).

Kubernetes Secrets Pattern

Inject secrets as environment variables from Secret objects:

yaml
env:
  - name: JWT_SECRET
    valueFrom:
      secretKeyRef:
        name: freeboard-secrets
        key: jwt_secret
  - name: JWT_GATEWAY_SECRET
    valueFrom:
      secretKeyRef:
        name: freeboard-secrets
        key: jwt_gateway_secret

Ansible Vault Pattern

  • Keep secret values in encrypted group_vars/host_vars (ansible-vault encrypt).
  • Render runtime .env from templates with strict file permissions (0600).
  • Never commit decrypted vault output.

CI Secret Injection Pattern

GitHub Actions:

yaml
env:
  JWT_SECRET: ${{ secrets.JWT_SECRET }}
  JWT_GATEWAY_SECRET: ${{ secrets.JWT_GATEWAY_SECRET }}
  GATEWAY_SERVICE_TOKEN: ${{ secrets.GATEWAY_SERVICE_TOKEN }}

GitLab CI:

yaml
variables:
  JWT_SECRET: "$PROD_JWT_SECRET"
  JWT_GATEWAY_SECRET: "$PROD_JWT_GATEWAY_SECRET"
  GATEWAY_SERVICE_TOKEN: "$PROD_GATEWAY_SERVICE_TOKEN"

Use masked/protected CI variables and avoid logging secret values.

Incident Response (Secret Exposure or Suspected Leak)

  1. Identify exposed secret class and impacted boundary (user auth, gateway trust, db creds, encryption key).
  2. Contain quickly:
    • disable external routes/ingress for impacted surfaces.
    • move impacted dashboards to private visibility via admin/API controls.
  3. Rotate compromised secrets using the playbooks above.
  4. Verify authentication, datasource fetch, realtime flows, and admin access paths.
  5. Record rotation timestamp + scope in your operations log.

Cross-References