Removing secrets from an App Service, a Function app, and GitHub Actions
Tailspin's customer portal is an App Service (Linux, .NET 8) plus a Function app that read and write Blob Storage and Service Bus. Today 14 connection strings and 3 storage account keys sit in App Service application settings and in a local.settings.json committed to the repository; a penetration test flagged this. Ten developers need to debug locally against a shared dev Key Vault. Deployments run from GitHub Actions on hosted runners, and security forbids long-lived secrets anywhere, including repository secrets. The vault currently uses the access-policy permission model. The platform team owns the subscription but will not grant application teams Owner or User Access Administrator at any scope.
1. Design the identity model for the App Service, the Function app, local debugging, and the GitHub Actions pipeline.
Read the worked answer
Four different execution contexts, four answers - the mistake is reaching for a service principal secret for all of them.
- App Service and Function app: system-assigned managed identities, one per resource, so the identity dies with the resource and cannot be reused laterally. Grant each the narrowest data-plane role it needs on each target (e.g.
Key Vault Secrets User,Storage Blob Data Contributor,Azure Service Bus Data Sender). Prefer separate identities over one shared user-assigned identity: the portal app has no business reading the queue. - GitHub Actions: workload identity federation. Create an app registration with a federated credential trusting the repository and branch/environment subject, grant it
azure/loginrights plus whatever deployment roles it needs, and usepermissions: id-token: writewithazure/login@v2and no client secret. This satisfies 'no long-lived secrets' because the token is short-lived and scoped to a workflow run. - Developers locally:
DefaultAzureCredentialwithAzureCliCredentialat the front of the chain. Each developer signs in with their own Entra ID account; assign them theKey Vault Secrets Userrole individually. No shared dev secret, and revocation is per-person. AddManagedIdentityCredentialexclusion on dev machines is unnecessary - on a workstation the chain simply falls through.
The anti-pattern to call out explicitly: a service principal with a client secret in repository secrets, and a single shared identity used by all four contexts.
2. The app team can assign Key Vault access policies but cannot create role assignments. Which permission model do you choose, and how does the app read secrets?
Read the worked answer
The constraint is decisive: RBAC requires a role assignment at the vault scope, which needs Owner or User Access Administrator. The team does not have it. Access policies can be managed by anyone with Key Vault Contributor-style rights on the vault.
So: keep access policies for now, with the platform team performing a one-off grant of the RBAC roles if and when they will - note in the design doc that RBAC is the strategic target because it supports PIM, ABAC conditions, and a single audit surface. Do not block the remediation on a governance negotiation.
Reading the secret, two patterns:
- Key Vault references in App Service configuration:
@Microsoft.KeyVault(SecretUri=https://vault.vault.azure.net/secrets/storage-conn). No code change, resolved by the platform using the managed identity, needs onlygeton the secret. Zero application code, and the value never appears in settings. This is the right answer for connection strings in configuration. SecretClientin code withDefaultAzureCredential, plus an in-memory cache with a TTL. Needed when the secret is used at runtime in a way configuration cannot express, or when you need to react to rotation.
Either way, stop using storage account keys entirely where you can: use the managed identity with Storage Blob Data Contributor and new BlobServiceClient(new Uri(...), new DefaultAzureCredential()). Three of those 17 settings disappear rather than moving to the vault.
3. Certificates and secrets are rotated every 90 days. How do you rotate without redeploying or breaking running apps?
Read the worked answer
Redeploying every 90 days is the thing to design out, so the question is what reads the new version and when.
- Use versionless secret URIs.
https://vault.vault.azure.net/secrets/db-password(no version segment) always resolves to the current version. A versioned URI pins the old value forever and forces a redeploy - that is the trap. - Key Vault references cache for roughly 24 hours. Any config-based consumer can be up to a day stale after rotation. If the down-time of a rotated credential is unacceptable, either keep the old secret valid for at least 48 hours after the new one is created (overlap window), or read via
SecretClientwith your own shorter TTL. - Event-driven invalidation: subscribe a function to the Key Vault
SecretNewVersionCreatedevent via Event Grid, and have it evict the application's secret cache or trigger a config refresh. This is the only pattern that gives minutes-level propagation without polling the vault. - For certificates, prefer managed certificates on App Service where the domain allows, removing the rotation entirely. Where a certificate must live in Key Vault,
App Service > TLS/SSL > Import Key Vault Certificategives the same versionless behaviour, but the binding must be re-verified after rotation. - For storage and Service Bus, remove the credential from the rotation problem by using managed identities with data-plane roles. You cannot rotate a secret you no longer have.
Order of preference: eliminate the secret > versionless reference with an overlap window > Event Grid invalidation > versioned URI plus scheduled redeploy.