Skip to content

Runbook — Secret Rotation & Redeploy

This runbook covers rotation of every credential the portfolio uses and the procedure to rebuild the entire infrastructure from scratch. Follow sections in isolation when rotating individual secrets; follow the full-redeploy section when recovering from a deleted subscription or wiped resource group.

Secret Inventory

Secret Name Storage Owner Expiry Breaks If Stale
EMAIL_ADDRESS GitHub Secret + KV email-address Google Account None (app password) Contact form silently falls back to demo mode
EMAIL_PASSWORD GitHub Secret + KV email-password Google Account Manual (revoke in Google settings) Contact form silently falls back to demo mode
E2E_CLIENT_SECRET GitHub dev environment secret Entra portfolio-e2e-ci app Set in Entra portal (max 24 months) Playwright E2E tests fail in CI
entra_app_client_secret (Bicep param) GitHub Secret + KV entra-app-client-secret + SWA appsettings Entra dev SSO app Set in Entra portal (max 24 months) Dev frontend SSO login broken
AZURE_CLIENT_ID GitHub Secret Entra OIDC app registration No expiry (federated credential) All GitHub Actions workflows fail to authenticate
AZURE_TENANT_ID GitHub Secret Azure AD No expiry All GitHub Actions workflows fail to authenticate
AZURE_SUBSCRIPTION_ID GitHub Secret Azure No expiry All GitHub Actions workflows fail

Email Credential Rotation

Email credentials are stored as GitHub Secrets and written to Key Vault on every infrastructure deploy via @secure() Bicep parameters.

Step 1 — Revoke the old Gmail app password

  1. Go to myaccount.google.com/apppasswords.
  2. Find the app password used for this project and click Remove.

Step 2 — Generate a new app password

  1. Still on the App Passwords page, select Mail as the app and the relevant device.
  2. Click Generate and copy the 16-character password that appears. Store it securely; Google will not show it again.

Step 3 — Update GitHub Secrets

bash
gh secret set EMAIL_ADDRESS --repo SvenRelijveld1995/portfolio
gh secret set EMAIL_PASSWORD --repo SvenRelijveld1995/portfolio

Paste the Gmail address and new app password when prompted.

Step 4 — Re-deploy infrastructure to write new values to Key Vault

Trigger deploy-infrastructure.yml for both environments so Bicep overwrites the KV secrets:

bash
# dev
gh workflow run deploy-infrastructure.yml --ref main \
  -f environment=dev -f deploy_containers=false

# prod
gh workflow run deploy-infrastructure.yml --ref main \
  -f environment=prod -f deploy_containers=false

Wait for both runs to succeed before continuing.

Step 5 — Force a Container App revision restart

The Container App caches KV secret values at startup. Restart the backend revision to pick up the new values:

bash
BACKEND_APP="dna-dev-portfolio-we-ca-be"
RG="dna-dev-portfolio-we-rg"
REVISION=$(az containerapp revision list \
  --name "$BACKEND_APP" --resource-group "$RG" \
  --query "[0].name" -o tsv)

az containerapp revision restart \
  --name "$BACKEND_APP" --resource-group "$RG" \
  --revision "$REVISION"

Repeat for prod (dna-prd-portfolio-we-ca-be in dna-prod-portfolio-we-rg).

Verify

Submit a contact form request on both environments and confirm the email arrives or check Application Insights logs for email sent (vs demo mode).


Entra ID Client Secret Rotation

Two separate Entra app registrations carry client secrets:

  • portfolio-e2e-ci (client ID 98ffb749-c54b-431a-82f3-df1bf271ed32) — used only by Playwright E2E CI via E2E_CLIENT_SECRET in the dev GitHub environment.
  • Dev SSO app (client ID b30b86ca-b372-4759-b02c-55a622dd19a0) — used by the dev frontend Container App and the dev SWA. Secret travels via Bicep parameter entra_app_client_secret → KV entra-app-client-secret (Container App) and directly into SWA appsettings (SWA does not support KV reference syntax).

Step 1 — Create the new secret (do not delete the old one yet)

  1. Go to portal.azure.com → Azure Active Directory → App registrations.
  2. Open the relevant app registration.
  3. Select Certificates & secretsNew client secret.
  4. Set an appropriate expiry (maximum 24 months) and click Add.
  5. Copy the Value immediately.

Step 2 — Update GitHub Secrets

For the E2E secret:

bash
gh secret set E2E_CLIENT_SECRET \
  --repo SvenRelijveld1995/portfolio \
  --env dev

For the dev SSO secret (Bicep parameter written to KV and SWA):

bash
gh secret set entra_app_client_secret \
  --repo SvenRelijveld1995/portfolio

Step 3 — Re-deploy infrastructure for dev and verify

bash
gh workflow run deploy-infrastructure.yml --ref main \
  -f environment=dev -f deploy_containers=true

Wait for the workflow to succeed, then:

  • Open the dev frontend and confirm Entra SSO login works.
  • Check that the Playwright E2E job in the next PR preview deploy passes.

Step 4 — Delete the old secret

Once the new secret is confirmed working, return to the app registration's Certificates & secrets page and delete the previous secret.

SWA appsettings note

The dev SWA receives the client secret via swa_auth.bicep as a plain appsettings property (AZURE_CLIENT_SECRET). It does not use a Key Vault reference — SWA does not support the App Service KV reference syntax. The Bicep deploy in step 3 handles both the KV write and the SWA appsettings update automatically.

For prod, the SWA does not use Entra auth, so no prod rotation is required for SSO secrets.


OIDC Federated Credential Refresh

GitHub Actions authenticates to Azure using OIDC — no rotating password exists. Federated credentials are attached to the github-actions-portfolio-oidc Entra app registration and expire only if the app registration itself is deleted.

If federated credentials are missing or corrupted, recreate them using the provided scripts:

Verify existing credentials

bash
az ad app federated-credential list --id $AZURE_CLIENT_ID

The following subjects must exist:

Credential name Subject
github-actions-main repo:SvenRelijveld1995/portfolio:ref:refs/heads/main
github-actions-pr repo:SvenRelijveld1995/portfolio:pull_request
github-actions-tags repo:SvenRelijveld1995/portfolio:ref:refs/tags/*
github-actions-environment-prod repo:SvenRelijveld1995/portfolio:environment:prod
github-actions-environment-dev repo:SvenRelijveld1995/portfolio:environment:dev
github-actions-environment-shared repo:SvenRelijveld1995/portfolio:environment:shared
github-actions-fix-branches repo:SvenRelijveld1995/portfolio:ref:refs/heads/fix/*
github-actions-fix-deploy repo:SvenRelijveld1995/portfolio:ref:refs/heads/fix/deploy

Recreate from scratch

If the app registration must be torn down and rebuilt entirely:

bash
cd infra/scripts
chmod +x teardown-oidc.sh setup-oidc.sh
./teardown-oidc.sh
./setup-oidc.sh

setup-oidc.sh will:

  1. Create (or reuse) the github-actions-portfolio-oidc app registration.
  2. Create a service principal and assign Contributor + Role Based Access Control Administrator + Key Vault Secrets Officer at subscription scope.
  3. Add all required federated credentials listed above.
  4. Prompt for EMAIL_ADDRESS / EMAIL_PASSWORD and set them as GitHub Secrets.
  5. Print AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID to copy to GitHub Secrets.

After running the script, update the three OIDC GitHub Secrets if the app registration client ID changed:

bash
gh secret set AZURE_CLIENT_ID --repo SvenRelijveld1995/portfolio
gh secret set AZURE_TENANT_ID --repo SvenRelijveld1995/portfolio
gh secret set AZURE_SUBSCRIPTION_ID --repo SvenRelijveld1995/portfolio

Full Redeploy from Scratch

Use this section when recovering from a completely wiped subscription, deleted resource groups, or a failed initial setup. Follow the steps in order — skipping stages will cause role-assignment or DNS propagation failures.

Prerequisites

  • Azure CLI installed and authenticated (az login)
  • GitHub CLI installed and authenticated (gh auth status)
  • Access to the Gmail account for email app password
  • Access to the Entra portal for SSO app secrets

Step 1 — Recreate OIDC infrastructure

bash
cd infra/scripts
./setup-oidc.sh

Update AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID in GitHub Secrets if the script created a new app registration.

Step 2 — Deploy shared infrastructure (DNS zone)

bash
gh workflow run deploy-infrastructure-shared.yml --ref main

This creates the Azure DNS zone. Wait for it to complete before proceeding — the DNS zone is referenced by the dev and prod infra deploys.

Step 3 — Deploy dev and prod infrastructure (containers off)

Run infrastructure for dev first, then prod. Pass deploy_containers=false on the first run to allow managed identity role assignments to propagate before Container Apps are created.

bash
# dev
gh workflow run deploy-infrastructure.yml --ref main \
  -f environment=dev -f deploy_containers=false

# prod — start after dev succeeds
gh workflow run deploy-infrastructure.yml --ref main \
  -f environment=prod -f deploy_containers=false

Step 4 — Wait for managed identity propagation

Role assignments for managed identities (ACR pull, Key Vault Secrets User) can take 5–10 minutes to propagate across Azure's RBAC cache. Wait before running step 5.

Step 5 — Re-deploy with containers enabled

bash
# dev
gh workflow run deploy-infrastructure.yml --ref main \
  -f environment=dev -f deploy_containers=true

# prod
gh workflow run deploy-infrastructure.yml --ref main \
  -f environment=prod -f deploy_containers=true

Step 6 — Deploy application images

Build and push container images for each environment:

bash
gh workflow run deploy-application.yml --ref main -f environment=dev
gh workflow run deploy-application.yml --ref main -f environment=prod

Step 7 — Deploy documentation (SWA)

bash
gh workflow run deploy-docs.yml --ref main

Custom domain caveat

Re-attaching a custom domain after a full redeploy requires three separate Bicep stages (as documented in container_apps.bicep):

  1. Stage 1 — Deploy with custom_domain='' and deploy_custom_domain=false to bring up the Container Apps Environment with no domain binding. This is covered by step 3 above.
  2. Stage 2 — Deploy the shared DNS zone (deploy-infrastructure-shared.yml), then re-run with custom_domain set and deploy_custom_domain=false. This registers the hostname in the Container Apps Environment and creates DNS records.
  3. Stage 3 — Re-run with deploy_custom_domain=true once DNS has propagated. Azure issues a managed certificate and binds it.

Do not attempt to skip to stage 3 directly — the managed certificate issuance will fail if the hostname was not pre-registered in stage 2.

Key Vault soft-delete caveat

Key Vault has softDeleteRetentionInDays: 7 and enablePurgeProtection: true. If a vault is deleted and then a redeploy attempts to create a vault with the same name within the 7-day window, the deployment will fail with a conflict.

To recover the existing vault instead of creating a new one:

bash
az keyvault recover --name <vault-name>

To check for soft-deleted vaults:

bash
az keyvault list-deleted --query "[].{name:name, deletedDate:properties.deletionDate}"

Alternatively, if the vault was deleted more than 7 days ago, purge protection will have expired and the name can be reused:

bash
az keyvault purge --name <vault-name>

To recover individual secrets that were soft-deleted within a recovered vault:

bash
az keyvault secret recover --vault-name <vault-name> --name <secret-name>

Next Steps

  • Security — KV RBAC, managed identities, email credentials flow
  • OIDC & Secrets — full GitHub Secrets reference and federated credential setup
  • Pipeline Overview — all workflows and their triggers

Page history

Field Value
Last updated 2026-04-09

Changelog

Date PRs Summary
2026-04-09 Initial runbook for secret rotation and full redeploy procedures