Most credential breaches in the cloud do not begin with a clever attacker. They begin with a connection string pasted into an appsettings.json file, a personal access token committed to a feature branch at midnight, or a storage key copied into a pipeline variable so a deployment would finally go green. Azure secrets management is the discipline of making sure none of those moments can end with an attacker holding a working key to your data. The threat is concrete: a single leaked credential with broad scope hands an outsider the same access your application has, and because that credential was static, it keeps working long after the person who leaked it has forgotten it exists. The exposure a misconfiguration creates is not theoretical either. Public code search engines index newly pushed repositories within minutes, automated scrapers harvest tokens continuously, and a connection string that grants account-level access to a storage account is worth real money to whoever finds it first.

Azure secrets management with Key Vault, managed identity, and OIDC federation - Insight Crunch

The reason this problem persists is that the easy path and the safe path point in opposite directions. The easy path is to take a value the application needs, paste it somewhere the application can read, and move on. The safe path asks you to question whether the application should hold that value at all. Doing secrets management well in Azure is less about finding a better hiding place for credentials and more about removing as many of them as you can, then guarding the small set that genuinely has to persist. That reframing is the whole argument of this guide, and it changes how every downstream decision gets made: where a value lives, who can read it, how it rotates, and how you find out when one has escaped into a place it should never have been.

This guide walks the full surface an engineer has to cover. It starts with the mental model that organizes every other choice, then works through Key Vault as the durable store, references that keep values out of configuration files, managed identity that removes credentials from application code entirely, federated identity that removes them from continuous integration and continuous delivery pipelines, automated rotation that bounds the lifetime of the ones that remain, and scanning that catches the leaks the first five controls did not prevent. Along the way the recurring patterns that engineers report show up by name: the hard-coded connection string that should have been a reference, the pipeline still authenticating with a stored password, the rotation job that quietly stopped running, the credential a scanner found in a repository, and the connection string that managed identity made unnecessary in the first place. Each one is a rung you can climb, and the ladder has a top.

The single sentence to carry through the rest of this article is the one that defines the destination. The best secret is the one that does not exist. Every control described here is in service of that idea, either by eliminating a credential outright or by shrinking the blast radius and lifetime of one you could not eliminate. Hold that rule in mind and the rest of secrets management stops being a grab bag of features and starts being a single coherent climb from the worst possible posture to the strongest defensible one.

The remove-the-secret rule and the Azure secrets management ladder

The organizing idea behind Azure secrets management is a single rule that ranks every option you have for handling a sensitive value. The remove-the-secret rule states that the best secret is the one that does not exist, so your first move with any credential is to ask whether you can delete it rather than where to keep it. A storage account key can often be replaced by a managed identity that authenticates with the platform directly. A pipeline password can be replaced by a federated token minted at run time. A database password can give way to token-based authentication against the identity provider. When elimination is genuinely impossible, and for a meaningful set of values it is, the rule’s second clause takes over: store the survivor in a purpose-built vault, reference it instead of copying it, give it the shortest practical lifetime, and watch for any copy that leaks out anyway.

That rule produces a ladder, and the ladder is the findable artifact of this article. Each rung represents a way of handling a credential, ordered from the worst posture at the bottom to the strongest at the top, with the specific improvement that each step up delivers. Engineers find the ladder useful because it turns an abstract aspiration into a concrete next action. You locate your current rung, you read the step up, and you climb. You do not have to reach the top of the ladder for every workload on day one. You have to know which rung each credential sits on and to move it up at least once.

Rung Posture Where the credential lives The step up from here
0 Secret in code Hard-coded literal in source, committed to history Move the value out of source into configuration so a change does not require a rebuild and a commit
1 Secret in configuration Plaintext in app settings, environment variables, or a pipeline variable Replace the plaintext with a Key Vault reference so the application reads from the vault, not from config
2 Key Vault reference The vault holds the value; config holds only a pointer Remove the stored value entirely where the target service supports identity-based access
3 Managed identity No credential at all for in-Azure resource access; the platform issues tokens Extend the same model to systems outside Azure with federated trust
4 OIDC federation A short-lived token minted at run time for pipelines and external workloads Bound the lifetime and rotation of the residual secrets that none of the above could remove
5 Rotated and scanned The few unavoidable secrets are short-lived, auto-rotated, and watched for leaks This is the top; maintain it and audit drift

Read the ladder from the bottom and the failure modes name themselves. A secret in code is the worst rung because source history is permanent, widely cloned, and frequently published, so a credential committed once is exposed to everyone who ever pulls the repository and to every scanner that indexes it. A secret in configuration is better only because changing it no longer requires a code change, but the plaintext is still sitting in a place a process dump, a log line, or an over-broad role assignment can read. The Key Vault reference rung is the first genuine improvement, because the application now holds a pointer rather than the value, and the value never appears in the deployment artifact at all. The managed identity rung is where the credential stops existing for a large class of resource access, and the OIDC federation rung extends that same disappearance to the pipeline and to workloads that run outside Azure. The top rung accepts that a residual set of secrets cannot be removed and applies the strongest treatment to them: a short lifetime, automatic rotation, and continuous scanning for leaks.

Where does this rule come from, and why is removal better than storage?

Removal beats storage because a credential that does not exist cannot be stolen, logged, screenshotted, committed, or forgotten in a variable group. A stored secret, no matter how well guarded, is a standing liability whose value to an attacker grows with its lifetime. Identity-based access replaces the standing secret with a token issued on demand, scoped narrowly, and expired within an hour, which collapses both the lifetime and the blast radius of any single compromise. That is the whole reason the ladder points up toward managed identity and federation rather than toward a cleverer vault.

The series thesis running through this guide is the habit of the strongest default. Just as a network design defaults to private access and a data design defaults to encryption, a credential design defaults to nonexistence. You do not start by asking how to store a secret safely. You start by asking whether the workload needs to hold one at all, and you treat every credential that survives that question as a measured exception rather than a normal state of affairs. The deep dive on the store itself, the complete guide to Azure Key Vault, covers the vault mechanics that the higher rungs depend on, and it is worth reading alongside this article because the ladder’s middle rungs assume you understand how the vault models secrets, keys, and certificates.

A common objection at this point is that removal sounds good in a slide but that real systems are full of credentials that cannot be removed, so the rule is aspirational rather than practical. The counter-reading is fair, and the rest of this guide engages it directly. The honest answer is that the set of genuinely irremovable secrets is far smaller than most teams assume. The connection string to a first-party Azure service almost never needs to be a secret once you reach for managed identity. The pipeline password almost never needs to exist once you configure federation. What remains tends to be third-party API keys, a handful of legacy system credentials, and bootstrap values, and those are exactly the ones the top rung is designed to contain. The rule is not that every secret disappears. The rule is that you climb until only the genuinely irreducible ones are left, and then you treat those with the strongest available controls.

Key Vault as the durable store, and references that keep values out of config

For the credentials that genuinely have to persist, Key Vault is the answer to where they live. The vault is a managed service whose entire purpose is to hold sensitive values behind a strong access boundary, to log every read, and to expose those values only to identities you have explicitly authorized. It stores three kinds of object that engineers conflate at their peril: secrets, which are arbitrary strings such as connection strings and API keys; keys, which are cryptographic keys used for signing and encryption and which can be backed by hardware; and certificates, which bundle a key with its public certificate and a lifecycle. Secrets management in the narrow sense is mostly about the first category, but the access model and the rotation machinery are shared across all three, so understanding the vault as a whole pays off.

The first decision a vault forces is the access model. Older vaults used access policies, a flat list that grants a principal a set of permissions across the entire vault. Newer guidance favors Azure role-based access control, which lets you grant roles such as Key Vault Secrets User at the scope of the vault or even an individual secret, and which integrates with the same role assignments, Privileged Identity Management, and access reviews you use everywhere else in Azure. The practical difference is that role-based access control gives you per-secret granularity and a single auditable access surface, while access policies give you an all-or-nothing grant that is easy to over-provision. For any new vault the role-based model is the strongest default, and migrating an existing vault is a deliberate switch in the vault’s configuration rather than something that happens by accident.

# Create a vault that uses Azure RBAC for its data plane
az keyvault create \
  --name kv-payments-prod \
  --resource-group rg-payments \
  --location eastus \
  --enable-rbac-authorization true \
  --enable-purge-protection true \
  --retention-days 90

# Grant an application's managed identity read access to ONE secret, not the whole vault
SECRET_ID=$(az keyvault secret show --vault-name kv-payments-prod --name stripe-api-key --query id -o tsv)
az role assignment create \
  --assignee "$APP_PRINCIPAL_ID" \
  --role "Key Vault Secrets User" \
  --scope "$SECRET_ID"

Two flags in that command deserve attention because their defaults catch teams out. Purge protection, once enabled, cannot be turned off, and it guarantees that a deleted vault or secret cannot be permanently destroyed before its retention window elapses. That sounds like a nuisance until the day an automation script or a disgruntled account tries to purge a production vault, at which point the window is the only thing standing between you and irrecoverable loss. Soft delete is on by default now and works alongside purge protection to give you a recovery path. The retention window itself is a value to confirm against the current portal defaults, because the allowed range and the default have shifted over the service’s life and you should not assume a number that was true a year ago is still true today.

Why store a credential in Key Vault rather than in app settings?

Because app settings and environment variables are plaintext that many things can read: a process memory dump, an overly verbose log statement, a diagnostic export, or any principal with Contributor on the resource. Key Vault moves the value behind an access boundary that logs every read and grants access per identity and per secret, so a credential that lives in the vault has a smaller, auditable, and revocable exposure compared with the same value sitting in configuration. The vault also gives the value a version history and a lifecycle, which configuration does not.

That last point about reads is where Key Vault references earn their place on the ladder. A reference is a special configuration value that App Service and Azure Functions understand natively. Instead of putting the credential into an app setting, you put a pointer to the vault, and the platform resolves that pointer at startup using the app’s own managed identity. The credential never appears in your deployment artifact, never appears in source, and never appears in the configuration blade as plaintext. It appears only in the vault, and the running application sees the resolved value in memory.

# An app setting whose VALUE is a Key Vault reference, not the secret itself
DatabaseConnection = @Microsoft.KeyVault(SecretUri=https://kv-payments-prod.vault.azure.net/secrets/db-connection/)

# Versionless form: App Service resolves the current version and refreshes on rotation
StripeApiKey = @Microsoft.KeyVault(VaultName=kv-payments-prod;SecretName=stripe-api-key)

The choice between the versioned and the versionless form matters for rotation. A reference that pins a specific secret version keeps reading that exact version until you change the reference, which is predictable but means a rotation does not reach the application until you update the pointer. A reference written without a version resolves to the current version and picks up a rotated value on the platform’s refresh cycle, which is what you usually want when the whole point of rotation is that dependents follow along automatically. The exact refresh interval is a platform behavior to verify against current documentation rather than to hard-code into your mental model, because it has been tuned over time and the safe assumption is that propagation is eventually consistent rather than instantaneous.

Setting up references correctly is a configuration task in its own right, and the dedicated walkthrough on how to configure Key Vault references in your apps covers the managed identity grant, the reference syntax for both App Service and Functions, and the diagnostics that tell you why a reference failed to resolve. The most common failure is not a syntax error but a missing role assignment: the app’s identity can see the reference but lacks Key Vault Secrets User on the vault or the secret, so resolution fails and the setting shows an error state rather than the value. The fix is always the same, which is to grant the app’s identity read access at the narrowest scope that works, and the diagnostic that confirms it is the reference status the platform exposes for each setting.

A subtle but important property of references is that they keep the trust boundary where it belongs. The application does not authenticate to the vault with a credential of its own, because that would only move the secret rather than remove it. It authenticates with its managed identity, which is the next rung up and the subject of the following section. References and managed identity are designed to work together: the reference says which value to fetch, and the identity proves who is allowed to fetch it, with no stored credential anywhere in the chain.

Managed identity: removing the credential from application code entirely

The reference rung keeps a stored value out of configuration, but the value still exists somewhere in the vault. Managed identity goes further and removes the credential outright for a large class of resource access. The idea is that the platform gives your application its own identity in the directory, and Azure services trust that identity directly, so the application proves who it is and what it may do without ever holding a password, a key, or a connection string. When a function app reads from a storage account using its managed identity, there is no storage key anywhere: not in code, not in config, not in the vault. The credential has ceased to exist, which is precisely the destination the remove-the-secret rule points toward.

There are two kinds of managed identity, and the distinction shapes your design. A system-assigned identity is tied to the lifecycle of a single resource, created when you enable it and deleted when you delete the resource, which makes it ideal for a one-to-one relationship between a workload and its identity. A user-assigned identity is a standalone resource you create once and attach to many workloads, which suits a fleet of services that should share a single identity and a single set of role assignments, and it survives the deletion of any individual consumer. The decision rule is straightforward. Reach for system-assigned when the identity should live and die with one resource, and reach for user-assigned when several resources should share one identity or when you want the identity to outlive the compute that uses it. The dedicated guide on how to set up managed identities the right way goes through the assignment mechanics, the role grants, and the subtleties of the user-assigned model in depth.

# Enable a system-assigned identity on a function app and capture its principal ID
PRINCIPAL_ID=$(az functionapp identity assign \
  --name func-orders-prod \
  --resource-group rg-orders \
  --query principalId -o tsv)

# Grant that identity data-plane access to a storage account, scoped to one account
STORAGE_ID=$(az storage account show --name stordersprod --query id -o tsv)
az role assignment create \
  --assignee "$PRINCIPAL_ID" \
  --role "Storage Blob Data Contributor" \
  --scope "$STORAGE_ID"

The application code that consumes a managed identity is where the elimination becomes visible. The Azure SDKs expose a credential type that discovers the ambient identity automatically, so the same code that runs locally under a developer’s signed-in account runs in production under the managed identity without a branch or a configuration switch. The connection string with an embedded key gives way to a service endpoint and a credential object that carries no secret of its own.

// No key, no connection string, no stored credential anywhere.
// DefaultAzureCredential discovers the managed identity at run time.
using Azure.Identity;
using Azure.Storage.Blobs;

var credential = new DefaultAzureCredential();
var blobService = new BlobServiceClient(
    new Uri("https://stordersprod.blob.core.windows.net"),
    credential);

var container = blobService.GetBlobContainerClient("orders");
await foreach (var blob in container.GetBlobsAsync())
{
    Console.WriteLine(blob.Name);
}

How do I avoid putting secrets in code with managed identity?

Replace the stored credential with an identity the platform issues at run time. Enable a managed identity on the compute resource, grant that identity a data-plane role on the target service scoped as narrowly as the work requires, and use a credential type such as DefaultAzureCredential in your code so the SDK acquires a token for that identity automatically. The result is application code that names the service endpoint and the operation but holds no password, key, or connection string, which means there is nothing to leak, log, or rotate.

The credential discovery chain is worth understanding because it is what makes the local and production experience uniform. The default credential type tries a sequence of sources in order, starting with environment variables, then the managed identity endpoint when running in Azure, then the credentials of a developer signed in through the command line or an integrated tool. In production the managed identity step succeeds and the chain stops there. On a developer’s machine the managed identity step is skipped and the developer’s own signed-in identity is used instead, which is why the same code works in both places without a secret and without a special case. The cost of this convenience is that the chain can be slow or surprising if multiple sources are configured, so for production workloads where you know exactly which identity should be used, naming the specific credential type rather than the default chain removes ambiguity and shaves the token acquisition time.

Managed identity reaches well beyond storage. It authenticates to Key Vault itself, which is how references resolve. It authenticates to Azure SQL and the database services through token-based access, which retires the database password as a stored secret. It authenticates to Service Bus, Event Hubs, Cosmos DB through its data-plane role model, and to the resource management plane for control operations. Each integration follows the same shape: enable the identity, grant a data-plane role at the narrowest scope, and let the SDK acquire tokens. The recurring pattern engineers report is the connection string that managed identity made unnecessary, where a value that had been treated as an immovable secret for years turned out to be removable in an afternoon once the data-plane role was understood. That pattern is the single highest-value move on the entire ladder, because it converts a standing credential into no credential at all.

The boundary of managed identity is the boundary of Azure’s own trust. It works wherever the consuming compute runs inside Azure and the target supports identity-based access, which covers most first-party services. Where it stops is at workloads that run outside Azure, at pipelines hosted on external systems, and at third-party services that do not federate with the Microsoft identity platform. For the first two of those, federated identity extends the same secretless model across the boundary, and that is the next rung. For the genuinely external third party that only accepts an API key, you have reached an irreducible secret, and the top rung’s rotation and scanning apply. Knowing where managed identity stops is as important as knowing what it covers, because it tells you exactly which credentials you are still responsible for after you have climbed this far.

A frequent misconfiguration at this rung is to enable a managed identity and then grant it a broad role at the subscription or resource group scope because that is faster than working out the precise data-plane role and scope. The identity removes the stored secret, which is real progress, but an over-scoped role means a compromise of the workload yields far more access than the workload needed, which undercuts the benefit. The discipline that pairs with managed identity is least privilege applied to the identity’s role assignments, and that discipline is important enough to treat on its own.

OIDC federation: removing the stored credential from CI/CD pipelines

A deployment pipeline has the same problem an application has, only worse. It needs to authenticate to Azure to create, change, and delete resources, which means it needs privileged access, and the traditional way to give it that access was to create a service principal, generate a client secret or certificate, and paste that secret into a pipeline variable. The pipeline variable is the single most dangerous place a secret can sit, because pipelines run untrusted code in pull requests, echo values into logs when a step misbehaves, and inherit variables into forks. A leaked deployment credential is not a read on one storage account; it is the keys to provision and destroy infrastructure. Open ID Connect federation removes that credential entirely, and it is the rung that turns a pipeline from the weakest link in your credential chain into one of the strongest.

Federation works by establishing a trust relationship between the Microsoft identity platform and the pipeline’s own token issuer. Instead of storing a secret, you configure a federated credential on an app registration or a user-assigned managed identity that says, in effect, that a token issued by GitHub Actions for a specific repository and branch, or by Azure Pipelines for a specific service connection, is allowed to act as this identity. At run time the pipeline asks its own platform for a short-lived Open ID Connect token, presents that token to the Microsoft identity platform, and receives an Azure access token in exchange. No long-lived secret is ever created, stored, or rotated, because the only credential involved is the run-time token that expires within the hour. GitHub Actions has supported this flow since 2021, and Azure DevOps service connections reached general availability with workload identity federation in February 2024, both dates worth confirming against current documentation as the feature continues to evolve.

# GitHub Actions: log in to Azure with OIDC and no stored secret
name: deploy
on:
  push:
    branches: [ main ]
permissions:
  id-token: write   # required so the runner can mint the OIDC token
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Azure login (federated, secretless)
        uses: azure/login@v2
        with:
          client-id: $
          tenant-id: $
          subscription-id: $
      - name: Deploy
        run: az deployment group create -g rg-orders -f main.bicep

Three details in that workflow carry the whole security argument. The id-token: write permission is what allows the runner to request an Open ID Connect token from GitHub, and without it the federated login cannot work. The login step takes a client id, a tenant id, and a subscription id, none of which are secrets, because they are identifiers rather than credentials and are safe to store as plain repository variables. There is no client-secret field at all, which is the visible proof that the credential has been removed rather than relocated. The federated credential on the Azure side ties this together by naming the exact subject the token must carry, so a token issued for a pull request on a fork cannot satisfy a credential scoped to the main branch.

# Configure the federated credential on a user-assigned identity for one repo and branch
az identity federated-credential create \
  --name github-orders-main \
  --identity-name id-deploy-orders \
  --resource-group rg-orders \
  --issuer "https://token.actions.githubusercontent.com" \
  --subject "repo:contoso/orders:ref:refs/heads/main" \
  --audiences "api://AzureADTokenExchange"

How do I handle secrets in CI/CD pipelines without storing them?

Use workload identity federation. Configure a federated credential on an app registration or user-assigned managed identity that trusts tokens from your pipeline’s issuer for a specific repository, branch, or service connection, then have the pipeline acquire a short-lived Open ID Connect token at run time and exchange it for an Azure access token. The pipeline stores only non-secret identifiers such as the client and tenant id, holds no password or certificate, and produces an audit entry in the identity provider’s sign-in logs for every exchange. This removes the single most leak-prone credential in most engineering organizations.

The subject string is where federation’s least privilege lives, and getting it wrong is the most common federation mistake. A subject of repo:contoso/orders:ref:refs/heads/main trusts only the main branch of one repository, so a token minted for any other branch, any pull request, or any other repository does not match and the exchange fails. Teams that want a pipeline to deploy from an environment rather than a branch use the environment form of the subject, which adds a deployment approval gate on top of the federation. The discipline is to make the subject as specific as the deployment it authorizes, because a subject that is too broad reintroduces the over-scoping problem in a new place. The full treatment of pipeline credentials, including the environment subject form, multi-stage approval gates, and how this interacts with deployment stacks, lives in the dedicated guide on handling secrets in CI/CD pipelines done right, which is the companion to this section.

Azure DevOps follows the same model through its service connections. A workload identity federation service connection authenticates with a federated token rather than a stored service principal secret, and the pipeline tasks that use the connection acquire their access token at run time. The migration path for an existing pipeline is gentle precisely because the two authentication methods can coexist on the same subscription during the transition. You add the federated credential, switch the pipeline to use it, confirm the deployment succeeds, and only then delete the old stored secret from the variable group and the app registration. There is no hard cutover and no maintenance window, which removes the usual excuse for leaving a stored secret in place. The first concrete action this section recommends is to search your variable groups for any client secret used in an Azure login, because every result is a leak or an outage waiting for its moment.

Federation extends beyond pipelines to any workload that can present an Open ID Connect token. Kubernetes clusters, including Azure Kubernetes Service with its workload identity feature, federate pod identities the same way, so a pod authenticates to Key Vault or storage with a projected token rather than a mounted secret. Workloads on other clouds federate through the same mechanism, presenting their native identity token and exchanging it for an Azure access token. The unifying property across all of these is that the credential is minted on demand, scoped by the federated subject, and expired within the hour, which is the same disappearance managed identity delivers inside Azure now stretched across the trust boundary. After this rung, the only credentials you should still be holding are the ones that no identity-based mechanism could remove, and those are what rotation and scanning exist to contain.

Automated rotation: bounding the lifetime of the secrets that remain

After elimination and federation have done their work, a residual set of credentials survives because no identity-based mechanism could remove them. Third-party API keys, certain legacy database passwords, storage account keys that a non-federating client still needs, and bootstrap credentials all fall into this category. For these, the remove-the-secret rule’s second clause governs: give the survivor the shortest practical lifetime and rotate it automatically so a leaked copy stops working before an attacker can exploit it at leisure. A static credential that never changes is a permanent liability whose value to an attacker only grows. A credential that rotates on a schedule has a bounded window of usefulness, and automatic rotation removes the human step that, left manual, inevitably lapses.

Key Vault provides the rotation machinery, and the pattern is event-driven so that rotation and the refresh of every dependent happen without a person in the loop. The vault publishes a near-expiry event to Event Grid before a secret expires, the default window being thirty days ahead of the expiration date, and that event triggers the rotation logic. For cryptographic keys the vault can rotate the key material itself on a policy you define, generating a new version automatically at the interval you set. For secrets such as storage keys or database passwords, the established pattern routes the near-expiry event to an Azure Function that regenerates the credential at its source, writes the new value back to the vault as a new version, and lets the dependents pick up the change.

# Set an expiry and a rotation policy concept for a key, with near-expiry notification
az keyvault key rotation-policy update \
  --vault-name kv-payments-prod \
  --name signing-key \
  --value '{
    "lifetimeActions": [
      { "trigger": { "timeBeforeExpiry": "P30D" },
        "action": { "type": "Notify" } },
      { "trigger": { "timeAfterCreate": "P90D" },
        "action": { "type": "Rotate" } }
    ],
    "attributes": { "expiryTime": "P120D" }
  }'

The dual-credential pattern is the one to understand for resources that you cannot afford to break during rotation, because it solves the race between issuing a new credential and retiring the old one. A storage account exposes two keys precisely so that one can be in active use while the other is being rotated. Key Vault stores the keys as alternating versions of a single secret, and when the near-expiry event fires, the rotation function regenerates the key that is not currently in use, writes it as the new version, and leaves the active key untouched until the next cycle. At no point is there a window where every valid credential has been invalidated, which is what makes the rotation safe to run unattended. The same alternating approach works for any resource that supports two concurrent credentials, and where a resource supports only one, the rotation has to be sequenced more carefully and tested against the dependents before it is automated.

How do I automate secret rotation in Azure Key Vault?

Set an expiration on the secret and subscribe to the Key Vault near-expiry event in Event Grid, which the vault publishes by default thirty days before expiry. Route that event to an Azure Function that regenerates the credential at its source, such as a storage account key or a database password, and writes the new value back to the vault as a new version. For resources with two credentials, rotate the inactive one using the dual-credential pattern so the active credential keeps working throughout. Dependents that read the secret through a versionless reference pick up the new version on the platform refresh cycle, so rotation cascades without a manual update.

The cascade to dependents is the property that makes rotation worthwhile rather than merely tidy. A rotated secret that no consumer ever picks up has not improved your posture; it has just created a newer version sitting next to the old one. Two design choices make the cascade automatic. First, consumers should read the secret through a versionless Key Vault reference or a versionless key uri, so they resolve the current version rather than a pinned one and follow the rotation on the platform’s refresh cycle. Second, the rotation function should write the new value to the same secret name as a new version rather than to a new secret name, so the reference that points at the name keeps resolving without any change. When both choices are in place, the recurring pattern engineers want, where rotation cascades to dependents on its own, falls out naturally, and the manual rotation that lapses is replaced by an event-driven loop that does not.

The misdiagnosis to avoid here is treating manual rotation as good enough because it happens, in principle, on a calendar. Manual rotation is the rung-one posture wearing a process document. It depends on a human remembering, on the runbook being current, on the person who owned it not having left, and on the change not breaking a dependent that no one remembered consumed the value. Every one of those dependencies is a place the rotation quietly stops, and a rotation that has quietly stopped is indistinguishable from no rotation at all until the credential leaks. Automating the rotation removes the human dependency, and subscribing the dependents to the versionless reference removes the breakage. The combination is what lets you set a short lifetime on the residual secrets without turning every expiry into an incident.

A final consideration is the lifetime you choose. Shorter is safer but more operationally demanding, because a shorter lifetime means more frequent rotations and more chances for a dependent to miss the cascade. The right interval balances the value of the credential and the maturity of your rotation automation against the cost of a rotation going wrong. A high-value credential behind well-tested automation can rotate aggressively. A credential whose dependents are not all known or not all wired to the versionless reference should rotate more conservatively until the cascade is proven, because a rotation that breaks production teaches the organization to distrust rotation, which is the opposite of the lesson you want. The expiry intervals, notification windows, and policy fields named here are platform behaviors to confirm against current documentation, since the service tunes them and the safe practice is to verify rather than to assume.

Secret scanning: catching the leaks the other rungs did not prevent

The first five rungs reduce the number of credentials that exist and bound the lifetime of the ones that remain, but no set of controls is perfect, and the assume-breach posture demands a detection layer that finds the credential that escaped anyway. Secret scanning is that layer. It searches source repositories, both new commits and the full history, for patterns that match known credential formats, and it does two distinct jobs that are easy to confuse. Detection scanning looks across the repository and its history for credentials that are already committed and raises an alert for each one. Push protection works at the moment a developer tries to push, inspects the incoming commits for high-confidence credential patterns, and blocks the push before the credential ever reaches the remote, which prevents the leak rather than reporting it after the fact.

The two jobs map to the two states a leak can be in. A credential already in history needs detection, triage, and rotation, because once a value has been committed and pushed it must be treated as compromised regardless of whether the repository is private, since clones, forks, and backups multiply the copies beyond your control. A credential about to be committed needs prevention, which is what push protection delivers by refusing the push and telling the developer exactly which value triggered the block. The strongest posture runs both: detection to clean up the past and push protection to keep the past from repeating. GitHub Advanced Security provides this for repositories on GitHub, and GitHub Advanced Security for Azure DevOps, available as the standalone GitHub Secret Protection product, brings the same secret scanning and push protection to Azure Repos.

How do I detect leaked secrets before an attacker uses them?

Enable secret scanning with push protection on every repository. Push protection inspects incoming commits and blocks any push that contains a high-confidence credential, stopping the leak at the source. Detection scanning runs across the repository and its full history to find credentials that were committed before scanning was enabled, raising an alert for each. Surface the results in Microsoft Defender for Cloud’s DevOps security view so leaks across many repositories appear in one place alongside infrastructure-as-code and dependency findings. When a real credential is found, treat it as compromised, rotate it immediately, and only then remove it from history.

Defender for Cloud is where scanning stops being a per-repository feature and becomes an organizational posture. Its DevOps security capability unifies the findings from connected GitHub and Azure DevOps repositories into a single view, so exposed credentials, infrastructure-as-code misconfigurations, and dependency vulnerabilities appear together rather than scattered across individual repository tabs. The recommendation that push protection should be enabled for every secret-scanning-enabled repository shows up there as a posture item, which turns the question of whether scanning is on from something you hope is true into something you can see and enforce. For an organization with many repositories, this aggregation is the difference between knowing your leak exposure and guessing at it.

The response to a found credential is a procedure, not a deletion. The instinct on finding a secret in history is to rewrite the history and remove it, but that instinct is dangerous because it treats the symptom and ignores the disease. A credential that was committed and pushed has been exposed to everyone who pulled the repository and to every scanner that indexed it, so the value is compromised the moment it lands, and removing it from history does nothing to make a leaked value safe. The correct order is to treat the credential as burned, regenerate it at its source so the leaked value stops working, update every dependent to the new value, confirm the dependents are healthy, and only then clean the history if you wish. Rotating first and cleaning second is the discipline, because a clean history with a still-valid leaked credential is a false sense of safety, while a rotated credential is genuinely safe even if a copy of the old value persists in a fork forever.

Scanning also closes the loop back to the rest of the ladder. Every credential a scanner finds is a credential that should have been removed or stored rather than committed, so a scanner alert is a signal about which rung a workload sits on, not just an isolated incident. A repository that keeps tripping push protection on storage keys is telling you that those storage keys should be managed identities. A pipeline that leaks a client secret is telling you the pipeline should federate. Treated this way, the detection layer is not the end of the climb but a feedback mechanism that points you at the next credential to eliminate. The strongest secrets-management programs read their scanner findings as a backlog of secrets to remove, which is why scanning sits at the top of the ladder alongside rotation rather than off to the side as an afterthought.

Least privilege applied to the secrets you keep

Every rung above the bottom involves an access grant, and least privilege is the discipline that keeps those grants honest. Removing a credential and replacing it with an identity is only an improvement if the identity is granted exactly the access the workload needs and no more, because an identity with a broad role is itself a kind of standing liability. The concrete application of least privilege to secrets management runs along three axes: the role you grant, the scope at which you grant it, and the lifetime of the grant.

On the role axis, the principle is to grant the most specific data-plane role that covers the operation rather than a broad management role that happens to include it. An application that reads secrets needs Key Vault Secrets User, not Key Vault Administrator or Contributor on the vault, because the reader role grants exactly the read it needs while the administrative roles grant the ability to change access policy, delete secrets, and reconfigure the vault. The same logic applies to every target: a workload that reads blobs gets the blob data reader role, not Storage Account Contributor, and a workload that sends messages gets the sender role, not Owner. The temptation to grant a broad role because it is faster to reason about is the single most common erosion of least privilege, and it is worth resisting precisely because the workload that removed its stored secret has done the hard part and should not give back the benefit with an over-broad grant.

On the scope axis, the principle is to grant at the narrowest scope that the work requires, which for secrets often means an individual secret rather than the whole vault. Azure role-based access control lets you assign Key Vault Secrets User at the scope of one secret, so an application that needs one connection string gets read access to that one secret and to nothing else in the vault. Scoping to the secret rather than the vault means a compromise of the application yields one value rather than every value the vault holds, which is a dramatic reduction in blast radius for the cost of a slightly more specific role assignment. The same scoping applies up and down the resource hierarchy: prefer a resource over a resource group, a resource group over a subscription, and a subscription over the management group, always granting at the lowest level that still covers the work.

On the lifetime axis, the principle is that standing access should give way to time-bound elevation wherever the access is privileged. A human who needs to administer a vault does not need standing administrator rights; they need the ability to elevate to that role for a bounded window with justification and, where the access is sensitive, approval. Privileged Identity Management provides exactly that, turning a permanent role assignment into an eligible one that the person activates when they need it and that lapses automatically. Applied to secrets management, this means the identities that read secrets at run time have standing but narrowly scoped access, while the humans who manage the vault and its policies have eligible access they elevate into, so a compromised administrator account is not a standing skeleton key. The combination of narrow run-time identities and time-bound human access is least privilege expressed across both the machine and the human sides of the system.

The common misconfigurations and the breaches they enable

Each rung of the ladder has a characteristic failure, and naming the failure alongside the breach it enables makes the cost of staying put concrete. These are the recurring cases engineers report, described as patterns with the step up each one needs, because a misconfiguration you can name is a misconfiguration you can fix.

The hard-coded credential is the first and worst. A connection string or an access key written as a literal in source and committed to history is exposed permanently, because source history is cloned, forked, and frequently published, and the credential keeps working long after the commit scrolls off the screen. The breach it enables is direct: anyone who reads the repository, and many automated scrapers that index public pushes within minutes, gains the access the credential grants. The step up is to move the value out of source into configuration as a first improvement, and then immediately further, to a Key Vault reference, so the value never appears in the artifact at all. A team that finds a hard-coded credential should treat it as already leaked, rotate it, and climb, not merely delete the line and hope no one noticed.

The plaintext credential in application settings is the second pattern, and it is the one that masquerades as safe because it is at least not in source. The value sits in the configuration blade, in environment variables, or in a pipeline variable as readable text, and anything that can read configuration can read the credential: a process memory dump, a verbose log line, a diagnostic export, or any principal holding Contributor on the resource. The breach it enables is quieter than a public commit but just as real, because the exposure surface is every identity with management access to the resource and every artifact that captures the environment. The step up is the Key Vault reference, which replaces the plaintext with a pointer the platform resolves through the app’s identity, so the value lives only in the vault and the configuration holds nothing sensitive.

The pipeline that authenticates with a stored secret is the third pattern, and it is the highest-stakes one because the credential is privileged. A service principal password sitting in a variable group can provision and destroy infrastructure, and the pipeline that holds it runs untrusted code in pull requests, echoes values into logs when a step fails, and inherits its variables into forks. The breach it enables is not a read on one resource but control over the deployment surface, which is among the worst outcomes in a cloud environment. The step up is workload identity federation, which removes the stored secret entirely and replaces it with a run-time token scoped to the exact repository, branch, or service connection that should be allowed to deploy. Because the old and new methods coexist during migration, there is no operational reason to leave the stored secret in place once federation is configured.

The rotation that lapses is the fourth pattern, and it is insidious because it looks like a control while being none. A manual rotation process that depends on a human remembering, a runbook staying current, and the owner not having moved on will stop running, and a rotation that has stopped is indistinguishable from no rotation until the long-lived credential leaks and an attacker enjoys an unbounded window. The breach it enables is the slow one: a credential compromised months ago that still works because nothing ever changed it. The step up is event-driven automatic rotation, which removes the human dependency by triggering off the near-expiry event and writing the new version back to the vault, so the lifetime of the credential is bounded by the schedule rather than by anyone’s diligence.

The leaked credential found by scanning is the fifth pattern, and how a team responds to it reveals whether they understand the model. The dangerous response is to rewrite history to remove the value and consider the matter closed, which leaves a still-valid credential in every clone and fork that pulled before the rewrite. The breach it enables is the one the team thinks they have closed: the leaked value keeps working because removal from history does nothing to a credential an attacker already copied. The correct response is to treat the credential as burned, rotate it at its source so the leaked value dies, update the dependents, and only then clean the history if desired. The step up here is not a new control but a procedure, namely rotate first and clean second, because the order is the whole difference between a real fix and a comforting illusion.

The sixth pattern is the happy one, the connection string that managed identity made unnecessary, and it is worth naming because it is the move with the highest return on the ladder. A value that a team treated as an immovable secret for years, perhaps a storage key embedded in a dozen services, turns out to be removable once the data-plane role model is understood, because the services can authenticate with their managed identities and the key can cease to exist entirely. The breach this pattern prevents is every breach the storage key could have enabled, eliminated at the root by removing the credential rather than guarding it. The lesson engineers take from this pattern is to revisit their irreducible-secret list periodically, because the set of credentials that genuinely cannot be removed shrinks as identity-based access expands across the platform, and a secret that was irreducible last year may be removable today.

How to verify your secrets management posture

A posture you cannot verify is a posture you are guessing at, and secrets management is full of changes that look done but are not. The application that should use a managed identity might still carry a fallback connection string. The pipeline that federates might still have a stale client secret in its variable group. The vault that should use role-based access control might still have access policies granting broad permissions. Verification turns each of these from a hope into an observation, and the verification steps below are the ones that catch the gap between intended and actual posture.

Verifying the elimination of stored credentials starts in source and configuration. A scan of the repository and its history confirms whether any credential remains committed, and a review of application settings confirms whether any setting holds a plaintext value rather than a Key Vault reference. The signal you want is that no setting contains a credential-shaped value and that every reference resolves, which the platform reports as a status on each setting. A reference in an error state is a verification failure that points at a missing role assignment, and a setting holding plaintext is a verification failure that points at an unfinished migration. Neither shows up unless you look, which is why this check belongs in a pre-deployment gate rather than in a quarterly review.

# List app settings and flag any that hold a plaintext credential rather than a reference
az webapp config appsettings list \
  --name app-orders-prod --resource-group rg-orders \
  --query "[?!starts_with(value, '@Microsoft.KeyVault')].name" -o tsv

# Confirm a federated pipeline identity has NO password credentials remaining
az ad app credential list --id "$APP_ID" --query "[].keyId" -o tsv

How do I verify that a workload holds no stored secrets?

Check three places. In source, run a secret scan across the repository and its full history and confirm zero findings. In configuration, list the application settings and confirm every sensitive value is a Key Vault reference rather than plaintext, and confirm each reference resolves to a valid status. In the identity, list the credentials on the app registration or managed identity and confirm no password or certificate remains where federation should have replaced it. A workload passes only when all three are clean, because a single stale credential in any of the three places means the elimination is incomplete and the workload is still on a lower rung than its design claims.

Verifying access scope is the second discipline, and it answers whether the identities that read secrets are granted no more than they need. The check enumerates the role assignments for each workload identity and confirms that the role is a specific data-plane role rather than a broad management role, and that the scope is as narrow as the work allows, ideally the individual secret or resource rather than the vault, resource group, or subscription. The signal of a healthy posture is a short list of narrow assignments per identity, and the signal of an eroded one is a Contributor or Owner assignment at a broad scope that someone granted because it was faster than working out the precise role. This verification is where over-scoping gets caught, and it is worth automating as a recurring report because role assignments drift over time as people grant access to unblock an incident and forget to narrow it afterward.

Verifying rotation is the third discipline, and it confirms that the residual secrets are actually rotating rather than merely configured to rotate. The check looks at the version history of each rotating secret and confirms that new versions appear on the expected cadence, and it confirms that the near-expiry event subscription exists and that the rotation function has run successfully on its last trigger. A secret with an expiration set but no recent new version is a rotation that has silently failed, which is exactly the lapsed-rotation pattern wearing the appearance of a working control. The version history is the ground truth, because a new version is the observable artifact of a rotation that actually happened, while a policy and an expiration are only the intent.

Verifying detection is the fourth discipline, and it confirms that scanning is on everywhere it should be and that findings are being acted on. The check confirms that secret scanning and push protection are enabled across the repositories, which Defender for Cloud’s DevOps security view surfaces as a posture recommendation, and it confirms that open scanning alerts are being triaged and resolved rather than accumulating. An organization with scanning enabled on most but not all repositories has a gap exactly where someone forgot to turn it on, and the aggregated view is what makes that gap visible. Taken together, the four verification disciplines, elimination, scope, rotation, and detection, cover the four things that can quietly be wrong, and running them on a schedule rather than once converts secrets management from a project you finished into a posture you maintain.

Making the posture auditable and repeatable

A posture that lives in a series of manual portal clicks is a posture that drifts, because the next engineer does it slightly differently, the emergency change skips a step, and six months later no one can say with confidence how any given vault is configured. Making secrets management auditable and repeatable means expressing it as code and as logged events, so the configuration is reproducible and every access is recorded. This is the discipline that lets you answer, on demand, who read which secret when, and to recreate a vault’s entire access posture from a template rather than from memory.

Expressing the vault and its access as infrastructure code is the repeatability half. The vault, its role-based access setting, its purge protection and retention, the role assignments that grant each identity its narrow access, and the federated credentials that wire pipelines to identities all belong in a Bicep or Terraform definition checked into source. When the configuration is code, a review catches an over-broad role assignment before it ships, a new environment is provisioned identically to the existing ones, and a drift between the declared and the actual state is detectable rather than invisible. The same infrastructure-as-code that provisions the vault should provision the identities and their grants, because splitting them means the access posture lives half in code and half in someone’s portal session, which is the gap drift grows in.

// Vault with RBAC and purge protection, plus a scoped secret-reader assignment
resource vault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: 'kv-payments-prod'
  location: location
  properties: {
    sku: { family: 'A', name: 'standard' }
    tenantId: tenantId
    enableRbacAuthorization: true
    enablePurgeProtection: true
    softDeleteRetentionInDays: 90
  }
}

resource readerRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(vault.id, appPrincipalId, 'kv-secrets-user')
  scope: vault
  properties: {
    roleDefinitionId: subscriptionResourceId(
      'Microsoft.Authorization/roleDefinitions',
      '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
    principalId: appPrincipalId
    principalType: 'ServicePrincipal'
  }
}

Expressing access as logged events is the auditability half. Every read of a secret, every change to access policy or role assignment, and every administrative operation on the vault should land in a log that you retain and can query, which means routing the vault’s diagnostic logs to a workspace and keeping the activity log for control-plane changes. The audit question that matters in an incident is who read the credential and when, and that question is answerable only if the reads were logged before the incident, not after. The role-based access model helps here because it produces a single, consistent access surface that maps cleanly onto the logs, whereas a mix of access policies and broad roles produces an audit trail that is hard to reason about. The combination of access as code and access as logs is what turns a vault from a black box into a system you can reason about, reproduce, and investigate.

The natural next step for a reader who wants to run, reproduce, and harden what this guide describes is hands-on practice in a sandbox where mistakes are free. The interactive labs and tested command and template library on VaultBook let you build a vault with role-based access, wire an app to a Key Vault reference, replace a connection string with a managed identity, configure a federated pipeline credential, and stand up an event-driven rotation, all in an environment where you can break things and see exactly how each control behaves. You can run the hands-on Azure labs and command library on VaultBook to turn the ladder in this article from a diagram into muscle memory, working each rung until the secretless pattern is the one your hands reach for by default.

Local development and the inner loop without stored credentials

The reason credentials end up in code is rarely production. It is the inner loop, the moment a developer needs the application to talk to a real service on their own machine and reaches for the fastest thing that works, which is to paste a connection string into a local settings file. That local settings file then gets committed by accident, or copied into the deployment, or shared in a chat message, and the credential has leaked from the one place that felt safe. A secrets-management posture that ignores local development is a posture with a hole exactly where the most credentials are created, so the inner loop deserves the same secretless treatment the production path gets.

The credential discovery chain that powers managed identity in production is what makes the local loop secretless, because it falls through to the developer’s own signed-in identity when no managed identity is present. A developer who signs in once through the command line or an integrated development environment gives the default credential type an identity to use, and the same application code that runs in production under a managed identity runs locally under the developer’s account, with no branch and no local secret. The grant that makes this work is a role assignment on the developer’s own identity, or on a group the developers belong to, scoped to the development resources, so each developer authenticates as themselves against development data rather than sharing a service credential.

# Developer signs in once; DefaultAzureCredential then uses this identity locally
az login

# Grant the dev team's group read access to the development vault, scoped tightly
az role assignment create \
  --assignee-object-id "$DEV_GROUP_OBJECT_ID" \
  --assignee-principal-type Group \
  --role "Key Vault Secrets User" \
  --scope "$DEV_VAULT_ID"

How do I run an app locally without a stored connection string?

Sign in with your own identity using the command line or an integrated development tool, then let DefaultAzureCredential discover that identity at run time. Grant your developer identity, or a group you belong to, a narrowly scoped data-plane role on the development resources so you authenticate as yourself against development data. The application code names the service endpoint and uses the credential object exactly as it does in production, which means there is no local connection string, no shared service password, and nothing to commit by accident. The production path and the local path run the same code, differing only in which identity the chain resolves.

The benefit beyond removing the local credential is that development access becomes individually attributable and individually revocable. When every developer authenticates as themselves, a read of a development secret is logged against a person rather than against a shared service account that everyone uses, so an audit can tell who did what, and offboarding a developer removes their access by removing their identity from the group rather than by rotating a shared credential everyone has memorized. The shared service credential that a team passes around is its own quiet liability, because it is long-lived, widely known, and impossible to attribute, and replacing it with per-developer identity removes all three problems at once. The inner loop, treated this way, stops being the place credentials are born and becomes another rung where the secret does not exist at all.

A caveat worth stating is that the discovery chain’s flexibility can produce confusing behavior when several identity sources are configured at once, because the chain will use the first one that succeeds and that may not be the one the developer intended. A machine with environment-variable credentials set for one purpose and a signed-in account for another can authenticate as the wrong identity and produce a permissions error that looks like a missing role assignment. The remedy when the ambient identity matters is to be explicit about which source to use rather than relying on the full chain, which removes the ambiguity at the cost of a slightly less magical setup. For most development this is not needed, but when a token is being acquired as an unexpected identity, the chain order is the first thing to check.

Handling the irreducible secrets you cannot remove

After every rung has been climbed, a residual set of credentials remains because no identity-based mechanism reaches them, and being honest about this set is part of doing secrets management well. Pretending every secret can be removed leads to a posture that claims to be secretless while quietly holding credentials no one is treating with the appropriate care. The irreducible set typically contains third-party API keys for services that do not federate with the Microsoft identity platform, a small number of legacy system credentials that predate identity-based access, bootstrap values needed before any identity exists, and break-glass credentials kept for the scenario where the normal identity path is unavailable. Each of these has a treatment, and the treatment is the top rung applied with discipline.

Third-party API keys are the largest part of the irreducible set for most teams, and the treatment is to store them in Key Vault, reference them rather than copy them, scope read access to the one identity that needs each key, and rotate them on whatever cadence the third party supports. The key still exists, but it lives in exactly one place, it is read by exactly one identity, every read is logged, and its lifetime is bounded by rotation. That is the strongest posture available for a credential that genuinely cannot be removed, and it is a world away from the same key pasted into a configuration file. Where the third party supports its own short-lived token or its own federation, that is always preferable to a static key, and it is worth checking periodically whether a third party has added such support, because the irreducible set shrinks over time as providers modernize.

Bootstrap credentials are the chicken-and-egg case, the credential needed to acquire the first identity before any managed identity exists, and the treatment is to minimize their number, their scope, and their lifetime, and to remove them from the steady state as soon as the identity-based path is established. A bootstrap value that is used once to establish federation and then deleted is a bounded risk, while a bootstrap value that lingers in the environment indefinitely is a permanent one. The discipline is to treat bootstrap credentials as temporary by design, with an explicit step that retires them once the identity they bootstrapped is working, rather than leaving them in place because removing them feels risky after the fact.

Break-glass credentials are the deliberate exception, the highly privileged account kept for the scenario where the normal identity infrastructure has failed and someone needs to regain control. The treatment is the opposite of removal, because the entire point is that this credential works when nothing else does, so instead it is stored with the strongest protection, its use is alarmed so that any access generates an immediate alert, and its very existence is documented and reviewed. A break-glass credential that is used without an alert firing is a control failure, because the only legitimate use is a genuine emergency that the whole organization should know about. Treating break-glass credentials as monitored exceptions rather than as ordinary secrets keeps them available for the rare moment they are needed without turning them into a standing skeleton key that an attacker could use unnoticed. The unifying principle across the whole irreducible set is that you have stopped pretending these credentials away and started treating each one as the measured exception it is, which is exactly what the remove-the-secret rule asks of the secrets that genuinely cannot be removed.

The verdict on Azure secrets management

The strongest secrets-management posture in Azure is not a better vault or a cleverer hiding place. It is a climb up a ladder whose top rung holds almost no secrets at all. You start by refusing to store what you can remove, replacing connection strings and keys with managed identities that the platform trusts directly, and you extend that same removal across the trust boundary with federated identity that gives pipelines and external workloads run-time tokens instead of stored passwords. For the credentials that survive that climb, you store them in Key Vault, reference them rather than copy them, grant the narrowest access that works, bound their lifetime with automatic rotation, and watch every repository for the leak that slips through anyway. The deciding move, the one with the highest return, is the connection string that managed identity makes unnecessary, because eliminating a credential beats guarding it every time.

The counter-reading that real systems are full of irreducible secrets is true but smaller than it looks, and the honest response is to name the genuinely irreducible set, treat each member as a monitored exception with the top rung’s full discipline, and revisit the list as the platform’s identity-based access expands and the set shrinks. The decision rule for any new credential is a single question asked in order: can this be removed with an identity, can it be removed with federation, and if neither, can it be stored, referenced, scoped, rotated, and scanned. Walk that question for every secret your system holds and you will find the number you have to keep is far smaller than the number you started with. Hold the one sentence that organizes all of it, that the best secret is the one that does not exist, and Azure secrets management stops being a scramble to hide credentials and becomes a deliberate climb toward a system that holds as few of them as a real workload can.

What makes the climb durable rather than a one-time cleanup is the order in which the rungs reinforce each other. Elimination shrinks the population of credentials, scoping shrinks the reach of the ones that drive run-time identity, rotation shrinks the lifetime of the residual store, and scanning catches the escapes the first three did not prevent and feeds them back as the next removal target. None of the four stands alone. A vault full of perfectly stored credentials that never rotate is still a standing liability, and a fleet of managed identities granted broad roles has removed the stored password while keeping the broad reach a stolen token would inherit. The posture that holds over time is the one where every new credential is challenged at creation, every grant is reviewed for scope, every residual value is on a rotation schedule, and every repository is scanned, so the system trends toward fewer secrets each quarter rather than accumulating them. Measured that way, success is not a finished project but a downward line on the count of credentials your workloads are obliged to hold.

Frequently Asked Questions

Q: What is Azure secrets management and why does it matter?

Azure secrets management is the practice of controlling how sensitive values such as connection strings, API keys, passwords, and certificates are created, stored, accessed, rotated, and detected across your environment. It matters because a single leaked credential with broad access gives an outsider the same reach your workload has, and because static credentials keep working long after they leak. Done well, the practice does not focus on finding a safer hiding place. It focuses on removing as many credentials as possible by replacing them with managed identities and federated tokens, then guarding the small residual set in Key Vault with narrow access, short lifetimes, and continuous scanning. The payoff is a measurable reduction in both the number of credentials that can be stolen and the window during which any stolen one stays useful, which is the difference between a posture that hopes nothing leaks and one that limits the damage when something does.

Q: Should I store secrets in Key Vault or in application settings?

Use Key Vault, and keep application settings free of plaintext sensitive values. App settings and environment variables are readable by anything with management access to the resource, by process memory dumps, by verbose logs, and by diagnostic exports, so a credential placed there has a wide and poorly controlled exposure. Key Vault holds the value behind an access boundary that logs every read, grants access per identity and per individual value, and gives the value a version history and lifecycle that configuration cannot. The strongest pattern is to keep the value in the vault and put only a Key Vault reference in the app setting, so the platform resolves the pointer at run time through the workload’s managed identity. The credential then appears only inside the vault and in the running process memory, never in the deployment artifact, never in source, and never as plaintext in any configuration blade.

Q: How do I avoid hard-coding credentials in my application code?

Replace each hard-coded value with an identity the platform issues at run time. Enable a managed identity on the compute resource, grant that identity the narrowest data-plane role that covers the operation, and use a credential type such as DefaultAzureCredential so the SDK acquires a token automatically. Your code then names the service endpoint and the operation but carries no password, key, or connection string, which means there is nothing to leak or rotate. For the small set of values that cannot be turned into identity-based access, store them in Key Vault and read them through a reference rather than embedding them. The test of success is that a search of your source and its history finds zero credential-shaped values, because anything committed to history is exposed permanently to everyone who ever clones the repository and to the scanners that index public pushes within minutes of a commit.

Q: What is the difference between a system-assigned and a user-assigned managed identity?

A system-assigned managed identity is bound to the lifecycle of a single resource. It is created when you enable it on that resource and deleted when the resource is deleted, which suits a one-to-one relationship between a workload and its identity. A user-assigned managed identity is a standalone resource you create once and attach to many workloads, and it survives the deletion of any individual consumer, which suits a fleet of services that should share one identity and one set of role assignments. The decision rule is to choose system-assigned when the identity should live and die with one resource, and user-assigned when several resources should share an identity or when the identity must outlive the compute that uses it. For federated pipeline scenarios the user-assigned form is often preferred because the federated credential lives on the identity and can be reused across the workloads that share it.

Q: How do Key Vault references work in App Service and Azure Functions?

A Key Vault reference is a special app setting whose value is a pointer to a vault secret rather than the secret itself. You write the setting value in the reference syntax, naming either the full secret uri or the vault name and secret name, and the platform resolves the pointer at startup using the app’s own managed identity. The resolved value is injected into the application as if it were an ordinary setting, so application code reads it the same way it reads any configuration, while the actual value lives only in the vault. For resolution to succeed, the app must have a managed identity enabled and that identity must hold read access on the vault or the specific secret, typically the Key Vault Secrets User role. When resolution fails, the platform shows an error status on the setting, and the cause is almost always a missing role assignment rather than a mistake in the reference syntax.

Q: Why should I use a versionless Key Vault reference instead of pinning a version?

A versionless reference resolves to the current version of the secret and picks up a rotated value on the platform’s refresh cycle, which is what you want when the purpose of rotation is that dependents follow along without manual intervention. A reference that pins a specific version keeps reading that exact version until you change the pointer, so a rotation produces a new version that the application never sees until someone updates the reference, which defeats the automatic cascade. Pinning a version is occasionally useful when you need absolute predictability and are willing to manage the update yourself, but for the common case where rotation should reach dependents on its own, the versionless form is the right default. Pair it with a rotation function that writes new values as new versions of the same secret name, and the reference keeps resolving to the latest version without any change to the application or its configuration.

Q: How do I remove the client secret from my GitHub Actions deployment?

Configure workload identity federation. Create a federated credential on an app registration or user-assigned managed identity that trusts tokens from GitHub Actions for your specific repository and branch, using the issuer for GitHub’s token endpoint and a subject such as the repository and branch reference. In the workflow, grant the job the id-token write permission so the runner can mint an Open ID Connect token, then use the Azure login action with only the client id, tenant id, and subscription id, which are identifiers rather than credentials and are safe as plain repository variables. There is no client secret field because the credential has been removed rather than relocated. Because the old and new methods can coexist during migration, you add federation, confirm a deployment succeeds, and then delete the stored secret from both GitHub and the app registration with no downtime and no maintenance window.

Q: What is workload identity federation and how does it remove pipeline secrets?

Workload identity federation establishes a trust relationship between the Microsoft identity platform and a pipeline’s own token issuer, so the pipeline can authenticate without any stored password or certificate. At run time the pipeline asks its platform for a short-lived Open ID Connect token, presents that token to the Microsoft identity platform, and exchanges it for an Azure access token scoped to the federated identity. No long-lived credential is ever created, stored, or rotated, because the only credential involved is the run-time token that expires within the hour. The federated credential names the exact subject the incoming token must carry, such as a specific repository and branch or a specific service connection, so a token from anywhere else cannot satisfy it. GitHub Actions has supported the flow since 2021 and Azure DevOps service connections reached general availability with it in February 2024, both worth confirming against current documentation as the feature evolves.

Q: How do I write the federated credential subject correctly?

The subject is the claim the incoming token must match for the exchange to succeed, and it is where federation’s least privilege lives. For GitHub Actions deploying from a branch, the subject takes the form of the repository identifier followed by the branch reference, which trusts only that branch of that repository so tokens from other branches, pull requests, or forks do not match. For deployments gated by an environment, the environment form of the subject adds a deployment approval on top of the federation. The discipline is to make the subject exactly as specific as the deployment it authorizes, because a subject that is too broad reintroduces over-scoping in a new place, trusting more workflows than intended. A mismatch between the token’s actual subject and the configured one is the most common federation failure, and the fix is to read the subject the token actually carries and align the federated credential to it precisely.

Q: How do I automate storage account key rotation in Azure?

Use the event-driven dual-credential pattern. Store the storage account keys as alternating versions of a single Key Vault secret, set an expiration so the vault publishes a near-expiry event to Event Grid before the value expires, and subscribe an Azure Function to that event. When the event fires, the function regenerates the key that is not currently in use, calls the storage account to rotate it, and writes the new value back to the vault as a new version, leaving the active key untouched so nothing breaks during the rotation. Because the storage account exposes two keys, one is always valid while the other rotates, which is what makes the process safe to run unattended. Dependents that read the value through a versionless reference pick up the new version on the platform refresh cycle, so the rotation cascades to consumers without a manual update. The near-expiry window defaults to thirty days ahead and is worth confirming against current documentation.

Q: How often should I rotate the secrets I cannot remove?

Choose the shortest interval your rotation automation and your dependents can reliably support. Shorter lifetimes are safer because they bound the window during which a leaked value remains useful, but they demand more frequent rotations and more chances for a dependent to miss the cascade, so the right cadence balances the value of the credential against the maturity of your automation. A high-value credential behind well-tested, event-driven rotation with dependents wired to versionless references can rotate aggressively. A credential whose consumers are not all known, or not all subscribed to the current version, should rotate more conservatively until the cascade is proven, because a rotation that breaks production teaches the organization to distrust rotation. The goal is a lifetime short enough to limit exposure and an automation reliable enough that each rotation is a non-event rather than an incident, with the version history confirming that rotations are actually happening on the cadence you intended.

Q: What happens to my application when a referenced secret rotates?

If the application reads the value through a versionless Key Vault reference and the rotation writes the new value as a new version of the same secret name, the application picks up the rotated value on the platform’s refresh cycle without any code change or redeployment. The propagation is eventually consistent rather than instantaneous, so there can be a short window where the previous version is still in use, which is why the dual-credential pattern keeps the prior credential valid until the next cycle. If the application instead pins a specific secret version in its reference, it keeps using that version and never sees the rotated value until you update the pointer, which is the common reason a rotation appears to have no effect. The safe design is the versionless reference combined with same-name versioning on rotation, so the application follows along automatically and the rotation never requires touching the consumer.

Q: How do I detect credentials that were leaked into a Git repository?

Enable secret scanning with push protection on every repository. Detection scanning searches the repository and its full history for patterns that match known credential formats and raises an alert for each one it finds, which catches values committed before scanning was enabled. Push protection works at the moment of a push, inspects the incoming commits for high-confidence credential patterns, and blocks the push before the value reaches the remote, which prevents the leak rather than reporting it afterward. GitHub Advanced Security provides this for GitHub repositories, and the equivalent product for Azure Repos brings the same capabilities to Azure DevOps. Surface the results in Microsoft Defender for Cloud’s DevOps security view so findings across many repositories appear together with infrastructure-as-code and dependency issues, which turns scanning from a per-repository feature into an organizational posture you can see and enforce rather than one you hope is switched on everywhere.

Q: What should I do first when a scanner finds a real credential in my code?

Treat the credential as compromised and rotate it before anything else. A value that was committed and pushed has been exposed to everyone who pulled the repository and to every scanner that indexed it, so the only safe assumption is that an attacker already has a copy. Regenerate the credential at its source so the leaked value stops working, update every dependent to the new value, and confirm the dependents are healthy. Only after the leaked value is dead should you consider cleaning the history, because rewriting history to remove the value does nothing to a credential an attacker has already copied, and a clean history with a still-valid leaked credential is a false sense of safety. The order, rotate first and clean second, is the whole difference between a real fix and a comforting illusion. Use the incident as a signal about which rung the workload is on, because a leak is also a pointer to a credential that should have been removed.

Q: Which Key Vault role should I grant an application that only reads secrets?

Grant Key Vault Secrets User, scoped as narrowly as the work allows, ideally to the individual secret rather than the whole vault. That role grants exactly the read the application needs and nothing more, whereas administrative roles such as Key Vault Administrator or a broad Contributor assignment also grant the ability to change access, delete values, and reconfigure the vault, which a reader has no business doing. Scoping the assignment to a single secret means a compromise of the application yields one value rather than every value in the vault, a large reduction in blast radius for the cost of a slightly more specific assignment. This requires the vault to use Azure role-based access control rather than the older access policy model, which is the recommended default precisely because it supports per-secret granularity and a single auditable access surface. Resist the temptation to grant a broad role because it is faster, since over-scoping gives back the benefit the managed identity earned.

Q: Should I use Azure RBAC or access policies for Key Vault?

Use Azure role-based access control for any new vault. The older access policy model is a flat list that grants a principal a set of permissions across the entire vault, which is easy to over-provision and produces an audit trail that is hard to reason about. Role-based access control lets you grant specific roles at the scope of the vault or an individual secret, integrates with the same role assignments, Privileged Identity Management, and access reviews you use across the rest of Azure, and gives you per-secret granularity with a single, consistent access surface. The practical consequence is tighter least privilege and a cleaner audit story. Migrating an existing vault from access policies to the role-based model is a deliberate switch in the vault’s configuration rather than something that happens automatically, so plan the cutover, map each existing policy to the equivalent narrow role, and verify access before and after so no workload loses the read it depends on.

Q: Can I run my application locally without a stored connection string?

Yes. Sign in once with your own identity through the command line or an integrated development tool, and let DefaultAzureCredential discover that identity at run time. The credential discovery chain skips the managed identity step on your machine and falls through to your signed-in account, so the same application code that runs in production under a managed identity runs locally under your own identity, with no branch and no local credential. Grant your developer identity, or a group you belong to, a narrowly scoped data-plane role on the development resources so you authenticate as yourself against development data. The benefit beyond removing the local credential is that development access becomes individually attributable and individually revocable, replacing the shared service credential that teams pass around with per-developer identity. If a token is acquired as an unexpected identity, check the chain order, since multiple configured sources can cause the chain to resolve a different identity than you intended.

Q: Which credentials genuinely cannot be removed from a system?

The irreducible set is smaller than most teams assume and typically contains third-party API keys for services that do not federate with the Microsoft identity platform, a few legacy system credentials that predate identity-based access, bootstrap values needed before any identity exists, and break-glass credentials kept for when the normal identity path fails. Each has a treatment. Store third-party keys in Key Vault, reference them, scope read access to the one identity that needs each, and rotate them on whatever cadence the provider supports. Minimize bootstrap credentials and retire them once the identity they established is working. Store break-glass credentials with the strongest protection and alarm their use, since the only legitimate access is a genuine emergency. The unifying principle is to stop pretending these values away and treat each as a monitored exception with the top rung’s full discipline. Revisit the list periodically, because the set shrinks as the platform’s identity-based access expands and providers add federation.