Skip to main content

Secret rotation cadence

Outcome​

Every secret has a documented rotation owner, cadence, and procedure. Quarterly DEK rotation is automated via GitHub Actions; manual rotations are logged in docs/operations/rotation-log.md.

Prerequisites​

  • PLATFORM_ADMIN + Azure RBAC.
  • gh CLI to dispatch workflows.

Rotation matrix​

SecretRotated byCadenceTrigger
Per-tenant PHI DEKpnpm rcm-core rotate-phi-dekQuarterly (90d).github/workflows/rotate-phi-dek.yml (cron 0 2 1 */3 *)
Postgres admin passwordaz postgres flexible-server updateAnnuallyManual; document in docs/operations/rotation-log.md
JWT signing secretaz keyvault secret set + App Service restartAnnuallyManual
Service Bus SASaz servicebus namespace authorization-rule keys renewAnnuallyManual
Trading-partner credentialsRotateCredentialDialogPer-partner contractOperator-driven

Quarterly DEK rotation (automated)​

The scheduled GitHub Action enumerates active tenants from identity.tenant and runs rotate-phi-dek against each.

Workflow inputNotes
environment`dev
zeroizeWhen true, appends --zeroize after re-encryption to NULL legacy plaintext columns. Default false — only flip after a decrypt-spot-check confirms the new ciphertext is valid.

Required secrets:

  • DATABASE_MASTER_URL — connection for identity.tenant enumeration.
  • KEY_VAULT_URL — vault URL for the target environment.
  • AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID — OIDC federated credential.

See PHI encryption at rest.

Manual rotation — Postgres admin password​

NEW_PW=$(openssl rand -base64 24 | tr -d '+/=')
az postgres flexible-server update \
--resource-group rg-rcm-prod \
--name pg-master-rcm-prod \
--admin-password "$NEW_PW"

# Update the bound Key Vault secret:
az keyvault secret set \
--vault-name kv-rcm-prod-... \
--name postgres-admin-password \
--value "$NEW_PW"

# Restart the apps so they pick up the new secret:
for app in app-rcmcore-prod-eus2 app-edigateway-prod-eus2; do
az webapp restart --name "$app" -g rg-rcm-prod
done

Log every rotation in docs/operations/rotation-log.md with timestamp, who ran it, and (for break-glass scenarios) why it had to happen out-of-band.

Manual rotation — JWT signing secret​

JWT rotation invalidates every active session. Plan a maintenance window unless the rotation is triggered by a confirmed leak.

NEW_JWT=$(openssl rand -hex 48)
az keyvault secret set \
--vault-name kv-rcm-prod-... \
--name jwt-secret \
--value "$NEW_JWT"

az webapp restart --name app-rcmcore-prod-eus2 -g rg-rcm-prod

After the restart, every existing JWT (access + refresh) is rejected on the next request. Users must re-authenticate.

Manual rotation — Service Bus SAS​

az servicebus namespace authorization-rule keys renew \
--resource-group rg-rcm-prod \
--namespace-name sb-rcm-prod \
--name RootManageSharedAccessKey \
--key PrimaryKey

NEW_CONN=$(az servicebus namespace authorization-rule keys list \
--resource-group rg-rcm-prod \
--namespace-name sb-rcm-prod \
--name RootManageSharedAccessKey \
--query primaryConnectionString -o tsv)

az keyvault secret set \
--vault-name kv-rcm-prod-... \
--name service-bus-conn \
--value "$NEW_CONN"

for app in app-rcmcore-prod-eus2 app-edigateway-prod-eus2; do
az webapp restart --name "$app" -g rg-rcm-prod
done

In-flight messages are not lost — Service Bus rotates the SAS key without dropping queue contents.

Trading-partner credentials​

Operator-driven via the Trading partner credential vault. The rotation flow updates the secret in Key Vault under trading-partner-cred-<partner-id> and records a rotated_at audit entry in rcm_master.trading_partner.

Validation​

CheckExpected
docs/operations/rotation-log.mdUp to date with every manual rotation
Quarterly DEK workflowLast run within 90 days
Apps healthy after restart/health returns 200
Old JWT after rotationRejected with 401 on next request

Cross-references​

Next​

9.5 — Env config viewer