This issue has been assessed as high severity. Review affected configurations immediately.
Azure Container Apps (ACA) is Microsoft’s serverless container platform built on top of Kubernetes and Kubernetes-native tools including Dapr, KEDA, and Envoy. It removes the need to manage cluster infrastructure while retaining support for microservices patterns. What it doesn’t remove are the security decisions — it just makes some of them less visible, which is where misconfigurations emerge.
Three patterns appear consistently in ACA environments in the wild: ingress enabled where it shouldn’t be, Dapr component secrets reachable by any app in the environment, and managed identities granted permissions far beyond what any single container application needs. Each is addressable with specific configuration changes.
Misconfiguration 1: Public Ingress on Internal Services
ACA ingress controls whether your container app is reachable over HTTP/HTTPS. It has three settings:
- Disabled — no HTTP endpoint, traffic via Dapr service invocation or direct container-to-container only
- Internal — reachable only within the ACA environment (VNet-integrated)
- External — reachable from the public internet
The default for new Container Apps created via the Azure portal or quick-start templates is External. This means a service that should only be reachable by other microservices in the same environment is reachable from the public internet with only your application’s own auth (if any) standing in the way.
Check your current ingress configuration:
az containerapp show \
--name myapp \
--resource-group myresourcegroup \
--query "properties.configuration.ingress" \
--output json
A correctly configured internal service returns:
{
"external": false,
"targetPort": 3000,
"transport": "auto"
}
If "external": true appears on a service that has no business receiving public traffic, restrict it:
az containerapp ingress disable \
--name myapp \
--resource-group myresourcegroup
Or switch to internal-only:
az containerapp ingress enable \
--name myapp \
--resource-group myresourcegroup \
--target-port 3000 \
--type internal
Audit all apps in an environment for external ingress:
az containerapp list \
--resource-group myresourcegroup \
--query "[].{Name:name, External:properties.configuration.ingress.external, FQDN:properties.configuration.ingress.fqdn}" \
--output table
Any service returning True in the External column that doesn’t serve end-user traffic is a candidate for review.
Misconfiguration 2: Dapr Component Secrets Scoped to the Entire Environment
Dapr provides a secrets building block that lets your container apps reference secrets from Azure Key Vault, Kubernetes secrets, or other stores without hardcoding credentials. Dapr components in ACA are configured at the environment level, which means by default every application in that environment can access every Dapr component — including secrets stores.
If you configure a Dapr secrets component pointing at a Key Vault containing your payment service credentials, your frontend app and your logging service can also query that secrets store through Dapr’s API:
# From inside any container app in the environment with Dapr enabled:
curl http://localhost:3500/v1.0/secrets/my-keyvault-component/payment-service-api-key
No additional authorization required.
Scope Dapr components to specific apps using the scopes field in the component definition:
# component.yaml
componentType: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: "mykeyvault"
scopes:
- payment-service
Deploy with scope:
az containerapp env dapr-component set \
--name my-environment \
--resource-group myresourcegroup \
--dapr-component-name my-keyvault \
--yaml component.yaml
Without scopes, the component is available to every app. With scopes, only payment-service can access it.
Audit existing Dapr components for missing scopes:
az containerapp env dapr-component list \
--name my-environment \
--resource-group myresourcegroup \
--query "[].{Name:name, Scopes:scopes}" \
--output table
Any component returning None or an empty scopes list is globally accessible.
Misconfiguration 3: Overprivileged Managed Identities
Managed identities are the correct way to give Container Apps access to Azure resources — no credentials to store or rotate, Azure handles the token lifecycle. The problem is the scope of permissions attached to those identities.
Common patterns that emerge over time:
- A single managed identity assigned to multiple container apps with different privilege needs
- Managed identities with Contributor on the resource group rather than specific roles on specific resources
- User-assigned managed identities shared across microservices, meaning a compromise of any one service inherits the permissions of all
Check what role assignments a managed identity holds:
# Get the managed identity principal ID
PRINCIPAL_ID=$(az containerapp show \
--name myapp \
--resource-group myresourcegroup \
--query "identity.principalId" \
--output tsv)
# List all role assignments for this identity
az role assignment list \
--assignee $PRINCIPAL_ID \
--all \
--output table
Look for assignments on scopes higher than necessary — resource group or subscription scope when resource scope would suffice.
Right-size permissions to the specific resource:
# Get the Key Vault resource ID
KEYVAULT_ID=$(az keyvault show \
--name mykeyvault \
--resource-group myresourcegroup \
--query id \
--output tsv)
# Assign only Key Vault Secrets User (read-only) on the specific vault
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Key Vault Secrets User" \
--scope $KEYVAULT_ID
Instead of a single identity for all services, provision a separate user-assigned managed identity per container app:
# Create app-specific identity
az identity create \
--name payment-service-identity \
--resource-group myresourcegroup
# Assign to the specific app
az containerapp identity assign \
--name payment-service \
--resource-group myresourcegroup \
--user-assigned payment-service-identity
This limits blast radius if one service is compromised — its identity has only the permissions that service requires.
Misconfiguration 4: Environment Variables as Secrets
ACA supports secrets as a distinct concept from environment variables, with values stored encrypted and not visible in the portal or ARM template output. Despite this, teams frequently set sensitive values directly as environment variables — API keys, connection strings, and tokens exposed in plain text in any deployment manifest or portal view.
Wrong — credential as environment variable:
az containerapp create \
--name myapp \
--env-vars DATABASE_PASSWORD=supersecret123 # ← Exposed in logs, portal, ARM exports
Correct — use ACA secrets:
# Register the secret
az containerapp secret set \
--name myapp \
--resource-group myresourcegroup \
--secrets db-password=supersecret123
# Reference it as an environment variable
az containerapp update \
--name myapp \
--resource-group myresourcegroup \
--set-env-vars DATABASE_PASSWORD=secretref:db-password
For production, point ACA secrets at Azure Key Vault rather than storing values in ACA itself:
az containerapp secret set \
--name myapp \
--resource-group myresourcegroup \
--secrets "db-password=keyvaultref:https://mykeyvault.vault.azure.net/secrets/db-password,identityref:/subscriptions/.../managedIdentities/myapp-identity"
Audit Checklist
- All container apps not serving public traffic have
external: falseingress - All Dapr components have explicit
scopeslimiting access to specific apps - Each container app has its own managed identity, not a shared identity
- No managed identity holds Contributor or Owner at resource group scope
- Sensitive configuration is stored in ACA secrets or Key Vault, not environment variables
- ACA environment is VNet-integrated and not using the default public infrastructure subnet
- Container registry images are pulled from ACR with managed identity — no registry credentials in environment variables