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.ghCLI to dispatch workflows.
Rotation matrix
| Secret | Rotated by | Cadence | Trigger |
|---|---|---|---|
| Per-tenant PHI DEK | pnpm rcm-core rotate-phi-dek | Quarterly (90d) | .github/workflows/rotate-phi-dek.yml (cron 0 2 1 */3 *) |
| Postgres admin password | az postgres flexible-server update | Annually | Manual; document in .specs/rotation-log.md |
| JWT signing secret | az keyvault secret set + ACA revision restart | Annually | Manual |
| Service Bus SAS | az servicebus namespace authorization-rule keys renew | Annually | Manual |
| Trading-partner credentials | RotateCredentialDialog | Per-partner contract | Operator-driven |
Quarterly DEK rotation (automated)
The scheduled GitHub Action enumerates active tenants from
identity.tenant and runs rotate-phi-dek against each.
| Workflow input | Notes |
|---|---|
environment | `dev |
zeroize | When 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 foridentity.tenantenumeration.KEY_VAULT_URL— vault URL for the target environment.AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID— OIDC federated credential.
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
| Check | Expected |
|---|---|
.specs/rotation-log.md | Up to date with every manual rotation |
| Quarterly DEK workflow | Last run within 90 days |
| Apps healthy after restart | /health returns 200 |
| Old JWT after rotation | Rejected with 401 on next request |
Cross-references
- PHI encryption at rest for the per-tenant DEK details.
- Trading partner credential vault for partner-side rotations.