This check flags Azure Container Apps that run without a managed identity, which forces you into static secrets and connection strings for accessing other Azure services. Assign a system-assigned or user-assigned managed identity so your app authenticates to Key Vault, Storage, and databases without storing credentials in code or environment variables.
If your Container App talks to a database, pulls images from a private registry, or reads secrets from Key Vault, it needs a way to prove who it is. Without a managed identity, that proof usually comes from a long-lived secret sitting in an environment variable or a config file. This check catches that gap before it becomes the root cause of a breach.
What this check detects
The containerapps_nomanagedidentity check inspects each Azure Container App in your subscriptions and reports any that have no managed identity attached. A Container App can have one of two identity types (and can use both at once):
- System-assigned — an identity tied to the lifecycle of the Container App. It is created when you enable it and deleted when the app is deleted.
- User-assigned — a standalone Azure resource you create once and can attach to multiple apps. It survives independently of any single app.
If neither is present, the check fails. In practice this means the app has no Microsoft Entra ID identity of its own, so any outbound authentication has to rely on something else, usually a shared key or connection string.
Note: A managed identity is just a service principal in Microsoft Entra ID that Azure manages for you. There is no password to rotate and no secret to leak, because Azure handles credential issuance and rotation behind the scenes.
Why it matters
The absence of a managed identity is rarely a problem on its own. The danger is what teams do instead to make their app work. When there is no identity to authenticate with, engineers fall back on patterns that are far riskier:
- Hardcoded connection strings for SQL, Cosmos DB, or Service Bus baked into environment variables.
- Storage account keys passed in as secrets, which grant full control over the entire account.
- Registry credentials for pulling private images, often a service principal secret with no expiry.
- Static client secrets for app registrations that nobody remembers to rotate.
Every one of those is a credential that can leak. It can end up in a Git commit, a container image layer, a log file, or a misconfigured environment variable dump. Once leaked, it is valid until someone notices and rotates it, which in most organizations is measured in months.
Consider a realistic scenario. An app uses a storage account key in plaintext to write uploads. The key lands in the container image, the image gets pushed to a registry that a contractor has pull access to, and now that key is in the wild. Because storage keys cannot be scoped, the attacker has read and write access to every blob and queue in the account. With a managed identity and RBAC, that same app would have had read and write only to one container, and there would be no key to steal in the first place.
Warning: Connection strings and account keys grant broad, non-revocable access until rotated. A single leaked storage key exposes the entire account, not just the data your app touches. Managed identity combined with scoped RBAC limits the blast radius to exactly what the app needs.
There is an operational cost too. Static secrets need rotation policies, secret stores, and someone to own the rotation runbook. Managed identities remove that entire category of toil because there is no secret to rotate.
How to fix it
You have two options. Use a user-assigned identity when you want one identity shared across multiple apps or when you need the identity to outlive the app. Use a system-assigned identity for simple, one-to-one cases.
Option 1: Enable a system-assigned identity (Azure CLI)
az containerapp identity assign \
--name my-container-app \
--resource-group my-rg \
--system-assigned
This returns a principalId. Use it to grant the app access to the resources it needs. For example, to give it read access to a Key Vault's secrets:
PRINCIPAL_ID=$(az containerapp identity show \
--name my-container-app \
--resource-group my-rg \
--query principalId -o tsv)
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "Key Vault Secrets User" \
--scope "/subscriptions//resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/my-vault"
Option 2: Attach a user-assigned identity (Azure CLI)
First create the identity (or reuse an existing one), then assign it to the app:
# Create the identity once
az identity create \
--name my-app-identity \
--resource-group my-rg
IDENTITY_ID=$(az identity show \
--name my-app-identity \
--resource-group my-rg \
--query id -o tsv)
# Attach it to the Container App
az containerapp identity assign \
--name my-container-app \
--resource-group my-rg \
--user-assigned "$IDENTITY_ID"
Use the identity in your app
Update your application code to authenticate with the identity instead of a connection string. The Azure SDKs make this straightforward with DefaultAzureCredential, which picks up the managed identity automatically when running in Azure:
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
credential = DefaultAzureCredential()
client = BlobServiceClient(
account_url="https://mystorageacct.blob.core.windows.net",
credential=credential,
)
For a user-assigned identity, pass the client ID so the SDK knows which identity to use:
credential = DefaultAzureCredential(
managed_identity_client_id=""
)
Tip: After enabling the identity and granting roles, delete the old connection strings and keys from your app's secrets and environment variables. Leaving them in place defeats the purpose, and the check should be paired with a secret scan to confirm they are gone.
Infrastructure as Code (Bicep)
If you manage Container Apps with Bicep, set the identity block directly so new deployments are compliant from the start:
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
name: 'my-container-app'
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${userAssignedIdentity.id}': {}
}
}
properties: {
managedEnvironmentId: environment.id
configuration: {
registries: [
{
server: 'myregistry.azurecr.io'
identity: userAssignedIdentity.id
}
]
}
// ... template config
}
}
Note the registries block uses the identity to pull images, removing the need for registry username and password.
Warning: Changing the identity configuration triggers a new revision of the Container App. Plan the change during a maintenance window if your app does not handle revision transitions cleanly, and verify the new role assignments are in place before cutting traffic over.
How to prevent it from happening again
Fixing one app is not enough. The goal is to make identity-less Container Apps impossible to ship.
Azure Policy
Use Azure Policy to audit or deny Container Apps without a managed identity. A deny policy stops non-compliant resources at deployment time. Here is the policy rule logic:
{
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.App/containerApps"
},
{
"field": "identity.type",
"in": [ "None", "" ]
}
]
},
"then": {
"effect": "deny"
}
}
Start with audit instead of deny while you remediate existing apps, then switch to deny once your estate is clean.
CI/CD gates
Scan your Bicep or Terraform before it ever reaches Azure. A quick check in your pipeline can fail the build if the identity block is missing:
# Example: fail if a containerApp resource has no identity block
grep -A20 "Microsoft.App/containerApps" main.bicep | grep -q "identity:" \
|| { echo "Container App missing managed identity"; exit 1; }
For Terraform, tools like Checkov or tfsec can enforce the same rule with policy-as-code, and they integrate cleanly into pull request checks.
Tip: Lensix runs this check continuously across your subscriptions, so even resources created outside your IaC pipeline (manual portal changes, click-ops, third-party tooling) get caught. Combine the deploy-time gate with continuous monitoring to close both ends.
Best practices
- Prefer managed identity over secrets everywhere. If an Azure service supports Microsoft Entra authentication, use the identity instead of a key or connection string. This covers Storage, Key Vault, SQL, Cosmos DB, Service Bus, Event Hubs, and ACR.
- Use user-assigned identities for shared access patterns. When several apps need the same permissions, one user-assigned identity with the right role assignments is easier to audit than many system-assigned identities.
- Scope RBAC tightly. A managed identity is only as safe as the roles it holds. Grant the narrowest role and the smallest scope that works. Avoid Owner and Contributor at the subscription level.
- Authenticate to ACR with identity. Configure the Container App's
registriesblock to use the managed identity so image pulls do not depend on a stored registry password. - Remove the old secrets. Enabling an identity does not help if the connection string is still sitting in the app config. Treat secret removal as part of the same change.
- Review role assignments regularly. Identities accumulate permissions over time. Periodically check what each identity can access and trim anything it no longer needs.
Danger: Do not assign broad roles like Contributor or Owner to a managed identity as a shortcut. If the app is compromised, the attacker inherits every permission the identity holds. A leaked container running with Contributor at subscription scope is close to a full subscription takeover.
A managed identity is one of the cheapest security wins in Azure. It costs nothing, removes an entire class of credential leaks, and cuts the operational burden of secret rotation. If a Container App is failing this check, treat it as a signal to find and eliminate the static secrets it is probably using instead.

