The problem with the string in your config#
A connection string does not know who you are. Neither does an account key, a SAS token, or an API key. Whoever holds the string gets whatever the string allows — and for most Azure resources, that is full access to the data. Azure Storage says it plainly in its own documentation: “Access to the shared key grants a user full access to a storage account’s data.”
That single property — authorization with no identity attached — is the root of a governance problem, and it gets worse every year now that we are wiring these credentials into agents. A shared key fails four tests a security team cares about:
- No attribution. Logs can tell you the key was used, not who used it.
- No least privilege. The key is all-or-nothing; there is no “read this one container.”
- No clean revocation. You rotate, then chase down every service, pipeline, and teammate who had the old value.
- No containment. A leaked key works from anywhere the network allows — the blast radius is the whole account.
The scale is not theoretical. GitGuardian’s State of Secrets Sprawl 2026 found roughly 29 million new hardcoded secrets pushed to public GitHub in 2025 alone — a 34% jump year over year. Credentials tied to AI services rose 81% to over 1.27 million, and the report found 24,000 unique secrets sitting in MCP-related config files — a direct preview of what happens when agent tooling normalizes credentials in JSON.
The shift: stop protecting the key better. Remove it. Every workload gets its own identity; every developer uses their own identity; each is granted a scoped, auditable, revocable role. Microsoft states the goal directly: “Managed identities eliminate stored secrets entirely.”
This is Zero Trust applied to machine access — “never trust, always verify, and credential-free.” It is foundational to all Azure work, but agents raise the stakes: an agent is an autonomous process making its own calls to models, tools, and data. If it authenticates with a shared key, the key is the blast radius.
This is Part 1 of a two-part series, written for the people who own the mandate — security, network, and platform leadership, and the cloud teams who run the estate. Here we cover the risk, the model at a governance level, who can prove what happened, how identity and network controls reinforce each other, and how to make “no keys” an enforced default. Part 2 is the implementation guide — the exact role and switch for every Azure resource an agent touches.
One identity per workload#
The mechanism is a single idea: a workload proves who it is with a platform-issued token instead of a stored secret. In Azure that identity is a managed identity; the SDK acquires the token through DefaultAzureCredential, which resolves to the workload’s managed identity in Azure and to the developer’s own signed-in identity on a laptop. From the application’s side, the entire change is this:
// No key. In Azure this is the workload's managed identity;
// on a developer's machine it is their own `az login` identity.
var client = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());That is the whole developer-facing surface, and it is identical across .NET, Python, JavaScript, and Java — the keyless design is a property of Azure and Microsoft Entra, not of any language or agent framework. The production mechanics — pinning the credential, wiring the identity to compute, federating Kubernetes — are covered in Part 2. What matters at the governance level are two decisions.
Prefer user-assigned identities. A system-assigned identity is a distinct principal per resource — N services means N identities and N sets of role assignments, re-granted every time a resource is rebuilt. A user-assigned identity is created once, granted its roles once, and shared across many workloads; its permissions can exist before the compute does, which is exactly what a platform team wants. Microsoft calls it the “recommended managed identity type for Microsoft services.” The governance payoff is a smaller, stable set of identities to review and a cleaner ownership boundary.
Grant least privilege, not “access.” The point of the model is not just “no key” — it is that the identity gets a scoped role (read this container, query this index) instead of the account-wide reach a key confers. That is MCSB PA-7, “just enough administration”, applied to every workload.
The same resource, two authorization models: a bearer secret that anyone can carry, versus a per-identity grant that is scoped, logged, and revocable.
Who can prove what happened#
For a security or compliance team, the decisive advantage of keyless is not convenience — it is attribution. This is Microsoft’s own language, and it is the single most useful line to take into an audit:
The difference shows up in the logs. Every Azure Storage resource log carries an AuthenticationType — AccountKey, SAS, or OAuth. Only OAuth (Entra, including managed identity) requests carry a RequesterObjectId that resolves to a named principal. A shared-key request resolves to a caller IP and a user-agent string — not an identity. Microsoft Entra sign-in logs go further, separating interactive user, non-interactive user, service principal, and managed identity sign-ins, so each workload leaves its own trail.
That trail is exactly what an auditor asks for. A keyless posture lets a security team hand over a concrete evidence package instead of a promise:
| Evidence | What it proves | Where it comes from |
|---|---|---|
| Entra sign-in logs, filtered to the workload’s identity | Per-identity attribution — who/what accessed the resource | Entra monitoring |
Resource Graph query showing allowSharedKeyAccess = false across the fleet | No shared-key path exists, point in time | Azure Resource Graph |
| Azure Policy compliance history for “prevent shared key access” | Continuous, timestamped control testing | Azure Policy compliance |
| Defender for Cloud regulatory-compliance report (ISO 27001, PCI DSS, SOC 2, NIST) | Mapped control state for the named standard | Regulatory compliance dashboard |
| RBAC role assignments + PIM activation history | Least-privilege scoping of each identity | Entra PIM |
For the CISO: this is the difference between attestation by policy document and attestation by generated evidence. Azure Policy compliance and the Defender for Cloud regulatory dashboard produce the audit artifact continuously, not once a quarter from a manual sample.
Identity is the perimeter, the network is the wall#
Keyless is an identity control, and identity is now the primary perimeter — Microsoft’s Well-Architected guidance states it directly: “identity is always the primary perimeter.” But the network team is not off the hook, and this is the part worth aligning on explicitly, because each control covers the other’s gap.
Network isolation without keyless is not enough. A shared key is bearer material: whoever holds it can authenticate from anywhere the network path allows, including a host already compromised inside your VNet. Private networking limits where a call comes from; it does nothing about who or what is calling or whether the caller is authorized. The real-world reminder is Microsoft’s own 2023 incident, where a misconfigured SAS token in a public repo exposed 38 TB of internal data — a portable-credential failure with no network boundary involved.
Keyless without network isolation is not enough. A per-identity data plane left open to the internet is still an attack surface — subject to token theft, a poisoned dependency, or a misconfigured role. Identity controls who, not exposure.
Together they are defense in depth. A private endpoint plus a managed identity plus a scoped role means the caller is authenticated per identity, authorized by least privilege, and reachable only from the private network. A useful nuance for the network team: disabling public access does not hide that a resource exists — the privatelink DNS name stays resolvable by design — but the data plane refuses every request. Resource existence is enumerable; resource access is not.
public access disabled]:::infra FW[Egress control
Azure Firewall FQDN allow-list]:::infra end subgraph L2[Identity layer — the security/identity team owns] MI[Managed identity
no stored secret]:::core RB[Scoped RBAC role
least privilege]:::core end subgraph L3[Policy layer — the platform team owns] POL[Azure Policy
deny keys + deny public access]:::sec end L1 --> DATA[(Resource data)]:::data L2 --> DATA L3 -.enforces.-> L1 L3 -.enforces.-> L2
Three layers, three owners. Keyless is one of them, not all of them — the network team’s private endpoints and egress controls, and the platform team’s policy guardrails, complete the picture.
What the network team owns in a keyless estate is concrete: private endpoints and private DNS for each data service; disabling public network access (adding a private endpoint does not do this automatically); the trusted-service bypass that lets first-party Azure services reach a locked-down resource through their own managed identity with no key exchanged; and egress control for agents — Microsoft’s baseline agent architecture routes all outbound traffic through Azure Firewall with FQDN allow-listing so a compromised agent cannot exfiltrate to an arbitrary endpoint. Azure ships the network and identity policies as matched pairs — a “disable local auth” policy alongside a “disable public network access” one — meant to be assigned together per resource type. Part 2 covers the trusted-service bypass mechanics; the governance point is that these two policies belong to two teams and should ship as one guardrail.
When the agent is not acting as itself#
There is a distinction unique to agents that a plain web app rarely makes: is the agent acting as itself, or on behalf of a user? Get it wrong and you either block legitimate work or hand an agent access to every user’s data in the tenant.
Autonomous work uses the app’s own identity. A background job, a shared knowledge base, a scheduled summarizer — none have a user in the loop. The agent authenticates with its managed identity and its access is governed by its own role assignments. Microsoft’s rule: “For autonomous agents with no user context, use the client credentials flow with only the required app permissions.”
User-scoped work uses on-behalf-of. The moment an agent reads a specific person’s mailbox or files, it must act with that user’s delegated permission. The user signs in, the app passes their token, and the on-behalf-of flow exchanges it for a token carrying both the agent identity and the user’s delegated scope. The agent can only touch what the user already could.
The difference is not academic. A delegated Mail.Read grant lets the agent read only the signed-in user’s mail; an application Mail.Read grant lets it — or anyone holding its credential — read every mailbox in the tenant. Microsoft’s guidance is a one-line rule worth taping to the wall: “Avoid granting app permissions when delegated permissions would suffice.”
app permissions| G1[Downstream resource
governed by the agent's roles]:::ext end subgraph ATT[Attended — on-behalf-of] direction LR U[User signs in]:::ok --> A2[Agent]:::core A2 -->|user token exchanged
delegated permissions| G2[Only what the user can access]:::ext end
Two access models for one agent. The blast radius of a mistake here is the difference between one user’s mailbox and the whole tenant’s.
Governing the agent identity at runtime#
Keyless settles how an agent authenticates. It does not settle what the agent may do once it holds a token — and an unattended identity needs its own guardrails. Three are worth wiring up from the start.
Conditional Access, adapted for agents. An agent cannot satisfy an interactive control like MFA, so a blanket “all users must MFA” policy will break it. Audit existing policies to exclude agent identities, then add agent-specific policies — keyed on identity filters, named locations, and risk — in report-only mode first. Microsoft ships templates for autonomous and on-behalf-of agents.
Risk-based blocking. Entra ID Protection scores agent sign-ins for risk — unfamiliar resource access, sign-in spikes, repeated failures — and surfaces them in a Risky Agents report. A Conditional Access policy set to block on high agent risk cuts a compromised agent off automatically.
A kill switch. When something goes wrong you want one action, not a scramble. Entra Agent ID gives three scopes: disable a single agent identity, disable the blueprint it was minted from — which stops new agents and blocks existing ones — or block tenant-wide through Conditional Access. That is the governance the shared-key model never had: a per-identity switch instead of a key everyone has to stop using at once.
Making “no keys” the default, not the hope#
Everything above is something a diligent team can do. At enterprise scale, “can” is not enough — someone will provision a resource with keys on, and someone will copy a connection string. The answer is to make keyless a structural property of the platform, enforced before an application team deploys anything.
Azure ships built-in policy definitions that audit or deny local and shared-key authentication, per resource type. A representative set:
| Resource | Policy | Definition ID |
|---|---|---|
| Storage | Storage accounts should prevent shared key access | 8c6a50c6-9ffd-4ae7-986f-5fa6111f9a54 |
| Azure AI Search | Search services should have local auth disabled | 6300012e-e9a4-4649-b41f-a85f5c43be91 |
| AI Services / OpenAI | AI Services resources should have key access disabled | 71ef260a-8f18-47b7-abcb-62d0673d94dc |
| Cosmos DB | Cosmos accounts should have local auth disabled | 5450f5bd-9c72-4390-a9c4-a7aba4edfdd2 |
| Azure SQL | SQL DB should have Entra-only auth enabled | b3a22bc9-66de-45fb-98fa-00f5df42f41a |
| Service Bus | Namespaces should have local auth disabled | cfb11c26-f069-4c14-8e36-56c394dae5af |
| App Configuration | Stores should have local auth disabled | b08ab3ca-1062-4db3-8803-eec9cae605d6 |
The full set spans Event Hubs, Event Grid, SignalR, Web PubSub, Bot Service, and more (built-in policy reference). Most ship with Audit, Deny, and Disabled effects; several also ship Modify or DeployIfNotExists variants that auto-remediate drift. The safe rollout is the one Microsoft recommends: start in Audit to measure the estate without breaking anything, then flip to Deny once telemetry shows no live key traffic — at which point a resource simply cannot be created with keys on. Assigned at a management-group scope, this becomes a guardrail the platform team sets and application teams inherit but cannot override.
deny local auth + deny shared key]:::sec POL -->|Deny| N[New resource with keys on
blocked at create]:::sec POL -->|Modify / DeployIfNotExists| RM[Existing drift auto-remediated]:::core POL --> MCSB[Rolls up to Defender for Cloud
MCSB control IM-1]:::infra
The guardrail is set once, at the management-group scope, and every subscription beneath inherits it. “No keys” becomes a posture score in Defender for Cloud, not a line item on a checklist.
This belongs in the landing zone. The Cloud Adoption Framework’s identity and access design area codifies “use Microsoft Entra managed identities where possible” as a platform guardrail set before workloads arrive — and it maps to MCSB control IM-1, which says to “use managed identities and workload identity federation… to eliminate secrets.” The policy is what turns that recommendation into an enforced default.
The pattern is not Azure-specific#
Worth saying once, because it reframes this as an industry standard rather than an Azure quirk: the shape is the same on every major cloud. AWS has IAM Roles for Service Accounts; GCP has Workload Identity Federation. All three are the same three moves: a platform-issued, short-lived OIDC token; a federated exchange for a native access token; permissions scoped by the cloud’s own RBAC. Zero static keys stored anywhere. The shape transfers directly, even if the commands differ.
What Part 2 covers#
The governance model is settled: one identity per workload, least privilege by default, attribution you can hand an auditor, identity and network reinforcing each other, and keyless enforced by policy. What remains is the build. Part 2: the implementation guide covers the credential model in production — pinning the identity, wiring it to App Service, Container Apps, and AKS — and then the per-resource cookbook: for each Azure service an agent calls, the exact role, the switch that turns off key auth, the code to connect, and the gotcha that will cost you an afternoon if you skip it.

