Why Azure Secrets and Keys Are Dangerous
Long-lived credentials — storage account keys, service principal client secrets, API keys — are dangerous not because of what they are, but because of how they get used. They get copy-pasted into configuration files, checked into repositories, logged by debug statements, and emailed between developers. Once a credential leaves a secure store, you cannot control where it goes. This page explains the specific risks, how incidents happen, and what to do when they do.
What makes long-lived credentials dangerous
Azure provides two types of credentials that are particularly risky: storage account keys and service principal client secrets. They share the same core property: they are bearer credentials. Anyone who possesses the credential can use it to authenticate, with no additional verification of identity.
Storage account keys
Each Azure storage account has two root keys. Whoever has either key has full, unrestricted access to everything in the storage account — all containers, all blobs, all queues, all tables, all file shares. There is no scope limitation, no action restriction, and by default no expiry.
These keys exist for legacy compatibility. In older Azure architectures, before RBAC data-plane support for storage existed, the keys were the only way to authenticate to storage. Modern applications should use RBAC with managed identities or service principals instead. Despite this, storage account keys continue to be used because they are convenient and widely documented.
Service principal client secrets
A client secret is effectively a password for an application identity. Unlike user passwords, client secrets are typically long-lived (often set to 1-2 year expiry) and are often copied to multiple places: CI/CD pipelines, container environment variables, deployment scripts, and developer workstations.
The risk is proportional to the roles assigned to the service principal. A client secret for a service principal with Reader on one resource group is a limited risk. A client secret for a service principal with Owner on a subscription is a catastrophic risk.
How secrets get leaked
The most common paths to credential exposure are remarkably mundane:
Committed to version control
A developer puts a storage account key in a configuration file for local testing, intending to remove it before committing. They forget. The file goes into git. Even if they notice immediately and delete the file in a new commit, the key is in the git history and has likely already been indexed by automated scanners.
This is the most common credential leak pattern across all cloud providers. GitHub’s secret scanning catches some of these, but not all credential formats are recognized, and private-repository scanning is a paid feature.
Printed in application logs
A developer adds verbose logging to debug a connectivity problem. The connection string — which contains the storage account key — is included in the log output. The logs are stored in Application Insights, Log Analytics, or a third-party logging service. Anyone with access to the logs now has the credential.
Stored in environment variables
Container workloads often receive secrets as environment variables. Environment variables in containers are readable by any process in the container, can be printed by crash dumps and diagnostic tools, and are often logged in orchestration platforms. printenv in a container shell shows every secret passed this way.
Checked into Terraform state
Terraform state files contain the full configuration of every resource Terraform manages. If a storage account key is referenced in Terraform and stored in the state, the state file contains that key in plaintext. Terraform state stored in a shared blob container with overly permissive access becomes an unintended credential store.
Shared in documentation or tickets
A developer pastes a connection string into a Jira ticket to ask for help debugging. The ticket is public or accessible to a broad audience. Connection strings for Azure Storage contain the account key in base64-encoded form.
A connection string in the format DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=BASE64KEY;EndpointSuffix=core.windows.net contains the full storage account key. Anyone with this string has unrestricted access to the storage account. Never paste connection strings into tickets, chat, or documentation.
Incident walkthrough: storage account key committed to GitHub
Here is a realistic step-by-step incident. The sequence of events, detection approach, and response steps are representative of how these incidents actually unfold.
What happened
A developer is building a data pipeline that processes files from an Azure storage account. During development, they create a local config.json file:
{
"storageConnectionString": "DefaultEndpointsProtocol=https;AccountName=companydatastore;AccountKey=AbCdEfGhIjKlMnOpQrStUvWxYz123456789AbCdEfGhIjKl==;EndpointSuffix=core.windows.net",
"containerName": "raw-uploads"
}The file is added to .gitignore — but the developer accidentally commits config.json.bak (a backup they created while refactoring), which contains the same connection string. The repository is public because it is an open-source project. The commit is pushed to GitHub at 14:32 UTC.
What happens next (without detection)
Automated secret-scanning bots continuously monitor GitHub for newly committed credentials. By 14:34 UTC — two minutes after the push — a scanner has found the storage account key and attempted authentication. The authentication succeeds. The scanner enumerates the storage account:
- Lists all containers (finds 4 containers including one named
customer-data) - Lists blobs in
customer-data(finds 47,000 files) - Begins downloading blobs
Over the next four hours, the storage account is exfiltrated. The developer does not notice until a monitoring alert fires about unusual egress bandwidth at 18:45 UTC. By that point, the data is gone.
Detection: how you find out
The faster you detect a compromised credential, the more you can limit damage. Detection sources:
- GitHub secret scanning alerts — if your repository has secret scanning enabled, GitHub may alert you when a known credential pattern is committed. Enable this under repository Settings → Security → Code security and analysis.
- Microsoft Defender for Cloud — Defender for Cloud can detect unusual storage access patterns including high-volume reads from unknown IP addresses and access from geographically anomalous locations.
- Azure Monitor alerts — set up metric alerts on storage accounts for unusual egress bandwidth or high read transaction counts.
- Storage Analytics Logs — every authenticated request to a storage account is logged in Storage Analytics. Review these in Azure Monitor Log Analytics with Kusto queries to look for unexpected access patterns.
Check storage access logs in Log Analytics:
StorageBlobLogs
| where AccountName == "companydatastore"
| where OperationName == "GetBlob"
| where TimeGenerated > ago(24h)
| summarize RequestCount = count(), UniqueIPs = dcount(CallerIpAddress) by CallerIpAddress
| order by RequestCount descResponse: what to do immediately
If you discover a storage account key has been leaked, this is the response sequence:
1. Rotate the key immediately
az storage account keys renew \
--account-name "companydatastore" \
--resource-group "data-rg" \
--key primaryThis immediately invalidates the old key. Any attacker using it will get authentication failures from this point forward. This is the single highest-priority action — do it before anything else.
Note: applications using the old key will also fail. Prepare to update them immediately after rotating. If the application uses the secondary key, rotate the primary first (to invalidate the leaked key), then update your app to use the new primary key, then rotate the secondary key.
2. Review access logs for the exposure window
StorageBlobLogs
| where AccountName == "companydatastore"
| where TimeGenerated between (datetime(2026-03-19T14:30:00Z) .. datetime(2026-03-19T18:50:00Z))
| where AuthenticationType == "AccountKey"
| project TimeGenerated, OperationName, CallerIpAddress, Uri, StatusCode
| order by TimeGenerated ascIdentify: when did access start, what IP addresses were used, which objects were accessed, and what operations were performed (read, write, delete)?
3. Review Storage Firewall settings
az storage account show \
--name "companydatastore" \
--resource-group "data-rg" \
--query networkRuleSetIf the storage account allows access from all networks (the default), consider restricting it to specific IP ranges or virtual networks. This limits future exposure even if another credential is compromised:
az storage account update \
--name "companydatastore" \
--resource-group "data-rg" \
--default-action Deny \
--bypass AzureServices4. Disable the storage account key entirely
If your applications can be migrated to use managed identities or RBAC-based access, disable the account keys entirely to prevent this class of incident in the future:
az storage account update \
--name "companydatastore" \
--resource-group "data-rg" \
--allow-shared-key-access falseAfter this, authentication with account keys or connection strings is rejected. Only RBAC-authenticated requests (from managed identities or service principals with appropriate roles) are accepted. This is the long-term fix.
5. Remove the credential from git history
Even after rotating the key, the old key exists in your git history. Remove it using tools like git filter-repo or BFG Repo Cleaner, then force-push the cleaned history. If the repository is public, assume the key has already been scraped and the cleanup is for hygiene rather than security. The rotation in step 1 is what actually stops the attacker.
6. Assess what data was accessed
Using the access logs from step 2, determine whether sensitive data was read. If customer PII or regulated data was in the storage account, you may have a breach notification obligation depending on your jurisdiction and industry regulations.
The managed identity alternative
The root problem with storage account keys and client secrets is that they are credentials that must be stored, distributed, and protected. Managed identities eliminate this problem entirely for workloads running in Azure.
With a managed identity, an Azure VM, App Service, Container App, or Azure Function can authenticate to a storage account without any credential in code or configuration. Azure handles the authentication token exchange internally. There is nothing to commit to git, nothing to paste into a ticket, and nothing to rotate.
To migrate a storage application from account key authentication to managed identity authentication:
# 1. Enable a system-assigned managed identity on the App Service
az webapp identity assign \
--name "myapp" \
--resource-group "app-rg"
# The command outputs the identity's principalId
IDENTITY_PRINCIPAL_ID=$(az webapp identity show \
--name "myapp" \
--resource-group "app-rg" \
--query principalId \
--output tsv)
# 2. Assign the identity the Storage Blob Data Reader role
az role assignment create \
--assignee-object-id "$IDENTITY_PRINCIPAL_ID" \
--assignee-principal-type "ServicePrincipal" \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/YOUR_SUB/resourceGroups/data-rg/providers/Microsoft.Storage/storageAccounts/companydatastore"
# 3. Update application code to use DefaultAzureCredential
# (In Python with azure-storage-blob SDK):
# from azure.identity import DefaultAzureCredential
# from azure.storage.blob import BlobServiceClient
# credential = DefaultAzureCredential()
# client = BlobServiceClient(account_url="https://companydatastore.blob.core.windows.net", credential=credential)
# 4. Disable account key access once migration is verified
az storage account update \
--name "companydatastore" \
--resource-group "data-rg" \
--allow-shared-key-access falseThe DefaultAzureCredential class in Azure SDKs automatically uses the managed identity when running in Azure, and falls back to your developer credentials (from az login) when running locally. No code changes are needed when moving between environments.
Preventing credential exposure
Beyond migrating to managed identities, several practices reduce the risk from any credentials that must remain:
Store secrets in Key Vault, not in application config
If you must use a client secret or API key, store it in Azure Key Vault and fetch it at runtime. The application needs only the Key Vault URI (not a secret) and a managed identity (not a credential) to retrieve the secret. Nothing sensitive is in the application’s configuration files.
Set up git pre-commit hooks for secret scanning
Tools like gitleaks and detect-secrets can be configured as pre-commit hooks to block commits that contain credential patterns before they ever reach the repository.
Enable Microsoft Defender for Storage
Defender for Storage provides anomaly detection on storage account access. It alerts on access from unusual IP addresses, unexpected access patterns, and known malicious IPs. Enable it per storage account or at the subscription level through Microsoft Defender for Cloud.
Use SAS tokens instead of account keys when sharing is unavoidable
If you must provide storage access to external parties, generate a Shared Access Signature (SAS) token with the minimum required permissions and a short expiry — hours or days, not years. A SAS token that expires limits the exposure window even if it is leaked.
az storage blob generate-sas \
--account-name "companydatastore" \
--container-name "reports" \
--name "2026-q1-report.pdf" \
--permissions r \
--expiry "2026-03-20T00:00:00Z" \
--output tsvCommon mistakes with secrets and keys
- Deleting the git commit and assuming the problem is solved. Git history is often indexed before you can delete it. Rotating the credential is the effective security response; cleaning the git history is hygiene for future auditors but does not protect you from the current exposure.
- Using the same storage account for both sensitive and non-sensitive data. A single storage account key grants access to everything in that account. Store sensitive data in a dedicated storage account so that a leaked key to the non-sensitive account cannot expose sensitive data.
- Not testing application behavior after key rotation. After rotating a storage account key, every application and pipeline using that key needs an updated connection string. Have a tested runbook before rotating, not a discovered-during-rotation scramble.
- Setting 2-year expiry on client secrets and treating them as permanent. A client secret with a 2-year expiry is effectively permanent from a risk perspective. Treat expiry as a maximum, not a target — rotate secrets more frequently, and use federated credentials where possible to eliminate them entirely. See service principals explained for the OIDC alternative.
Summary
- Storage account keys and client secrets are bearer credentials — whoever has them can use them, with no additional identity verification.
- Credentials most commonly leak through git commits, application logs, environment variables, and Terraform state files.
- Automated scrapers find credentials committed to public repositories within minutes.
- If a storage account key is compromised: rotate the key immediately, review access logs for the exposure window, tighten the storage firewall, and assess whether data was exfiltrated.
- The long-term fix is disabling account key access (
—allow-shared-key-access false) and migrating to managed identity authentication. - Use Key Vault to store any secrets that cannot be eliminated, and use short-lived SAS tokens instead of account keys for one-time sharing needs.
Frequently asked questions
How quickly can a leaked secret be found and abused on GitHub?
Automated scrapers continuously monitor GitHub for credentials. Research consistently shows that secrets committed to public repositories are often found within minutes — sometimes within seconds of the push. Deleted commits do not protect you; git history is often already indexed before the deletion. Assume a secret committed to a public repository is compromised the moment it is pushed.
What is the difference between a storage account key and an SAS token?
A storage account key is a root credential that grants unrestricted access to every resource in the storage account with no expiry. A Shared Access Signature (SAS) token is a time-limited, scope-limited token derived from the account key or a user delegation key. SAS tokens are better than account keys but the underlying problem remains: they are bearer credentials that grant access to anyone who holds them. Managed identities or service principals with RBAC are preferable to both.
How do I rotate a storage account key in Azure?
Use az storage account keys renew --account-name ACCOUNT_NAME --resource-group RG_NAME --key primary. This immediately invalidates the old primary key and generates a new one. Applications using the old key will fail immediately, so you need to update them before rotating — or use the secondary key to transition. Always maintain both keys: rotate primary, update applications to use new primary, then rotate secondary as a backup.