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 .specs/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 .specs/rotation-log.md
JWT signing secretaz keyvault secret set + ACA revision 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 ca-rcm-core-rcm-prod ca-edi-gateway-rcm-prod; do
az containerapp revision restart --name "$app" -g rg-rcm-prod
done

Log every rotation in .specs/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 containerapp revision restart --name ca-rcm-core-rcm-prod -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 ca-rcm-core-rcm-prod ca-edi-gateway-rcm-prod; do
az containerapp revision 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
.specs/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