Infrastructure

Azure Container Apps : diagnostiquer une identité managée avant d’élargir un rôle

Un runbook de production pour qualifier des 401/403 Azure Container Apps avec identité managée, token, scope RBAC, Key Vault, ACR, logs, validation et rollback avant d’élargir les permissions.

25 juil. 2026 azurecontainer-appsmanaged-identityidentityentra-idrbackey-vaultacrobservabilitykqlsecurityrunbookrollbackproduction

Quand une révision Azure Container Apps commence à recevoir des 401, des 403, des erreurs Key Vault, un refus ACR ou un appel API rejeté, la correction la plus rapide semble souvent être d’ajouter un rôle plus large à l’identité managée. C’est rarement le bon premier geste. Un rôle élargi peut faire repartir le service tout en masquant une dérive de révision, un mauvais client ID, un token demandé pour la mauvaise audience, un scope RBAC trop haut ou une configuration qui n’est plus alignée avec l’IaC.

Le cas d’usage est une Container App orders-worker-prod qui lit un secret Key Vault, tire une image depuis ACR et appelle une API interne protégée par Entra ID. Après un déploiement, une nouvelle révision démarre mais échoue sur ses dépendances. L’objectif du runbook est de décider s’il faut corriger l’identité attachée, le scope cible, le code de credential, la configuration de révision, ou rollbacker la révision avant d’élargir les permissions.

Figer le contrat d’identité

Commence par écrire quelle identité devait agir, sur quelle cible, avec quelle permission. Container Apps peut utiliser une identité système ou une identité assignée par l’utilisateur, et la révision active n’est pas toujours celle que l’équipe regarde dans le portail.

yaml container-apps-identity-contract.yml
container_app:
name: orders-worker-prod
environment: cae-platform-prod
resource_group: rg-platform-prod
active_revision: orders-worker-prod--20260725-1

expected_identity:
type: user_assigned
client_id: 00000000-0000-0000-0000-000000000000
principal_id: 11111111-1111-1111-1111-111111111111
token_audiences:
  - https://vault.azure.net
  - https://management.azure.com
  - api://internal-operations

expected_targets:
key_vault: read selected secrets only
acr: AcrPull on the registry
internal_api: app role operations.read

decision_needed:
fix_identity_or_scope
rollback_revision
temporary_exception_with_expiry

Sans ce contrat, l’équipe risque de traiter tous les 403 comme un manque de droit. Or un 403 peut aussi prouver que le mauvais principal agit correctement, mais sur la mauvaise cible.

Séparer token, autorisation et chemin applicatif

Le diagnostic doit classer l’échec avant toute modification RBAC.

text identity-failure-classes.txt
Token non obtenu
L'identité n'est pas attachée à la Container App
AZURE_CLIENT_ID pointe vers une identité absente ou de staging
Le code n'utilise pas ManagedIdentityCredential
Le runtime ne reçoit pas les variables d'identité attendues

Token obtenu mais rejeté par la cible
Le rôle est absent au scope exact
L'app role ou la permission API n'est pas assignée
L'audience du token ne correspond pas à la ressource
La propagation RBAC ou le cache service n'est pas terminé

Chemin ou configuration applicative
La révision appelle une URL de staging
Le secret ou le registry name a changé
ACR, Key Vault ou l'API reçoit un autre caller
Les logs applicatifs masquent la différence entre token et appel cible

Cette séparation évite d’ajouter Contributor sur un groupe de ressources alors que l’application demande simplement un token pour la mauvaise audience.

Lire l’identité attachée à la Container App

Vérifie d’abord l’état réel de la Container App, pas seulement la configuration prévue dans le ticket.

bash 01-container-app-identity-state.sh
SUBSCRIPTION="00000000-0000-0000-0000-000000000000"
RG="rg-platform-prod"
APP="orders-worker-prod"

az account set --subscription "$SUBSCRIPTION"

az containerapp identity show --resource-group "$RG" --name "$APP" --query "{type:type,principalId:principalId,tenantId:tenantId,userAssignedIdentities:userAssignedIdentities}" --output json

az containerapp revision list --resource-group "$RG" --name "$APP" --query "[].{name:name,active:properties.active,trafficWeight:properties.trafficWeight,createdTime:properties.createdTime}" --output table

az containerapp show --resource-group "$RG" --name "$APP" --query "properties.template.containers[].env[]" --output table

Regarde surtout AZURE_CLIENT_ID, les variables de cible, les secrets référencés et les révisions actives. Une révision avec 10 % de trafic peut suffire à produire un incident intermittent.

Tester le token depuis la révision

Un test depuis un poste administrateur ne prouve pas le runtime. Le test utile part de la révision ou d’un conteneur de diagnostic équivalent, avec les mêmes variables.

bash 02-runtime-token-probe.sh
# A exécuter depuis un shell de diagnostic contrôlé dans la même app
# ou depuis une révision temporaire qui porte la même identité.

RESOURCE="https://vault.azure.net"

curl -sS -H "Metadata: true" "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=$RESOURCE&client_id=$AZURE_CLIENT_ID" | jq '{client_id, resource, expires_on, token_present: (.access_token != null)}'

Le but n’est pas d’exposer le token. Le but est de prouver le client ID, l’audience et la disponibilité de l’endpoint d’identité. Si cette étape échoue, élargir un rôle cible ne corrigera rien.

Corréler les refus côté cible

Si le token existe, passe aux logs de la cible. Key Vault, ACR, API Management, l’API interne ou Storage doivent montrer le principal, l’opération et le résultat.

kusto 03-target-denials-container-apps.kql
let StartTime = datetime(2026-07-25T08:00:00Z);
let EndTime = datetime(2026-07-25T10:00:00Z);
let PrincipalId = "11111111-1111-1111-1111-111111111111";
AzureDiagnostics
| where TimeGenerated between (StartTime .. EndTime)
| where ResultType has_any ("401", "403", "Forbidden", "Unauthorized")
 or OperationName has_any ("SecretGet", "RepositoryPull", "GatewayLogs")
| extend Raw = tostring(Properties)
| where Raw has PrincipalId or Identity has PrincipalId or CallerIdentity has PrincipalId
| project TimeGenerated, ResourceProvider, Resource, OperationName, ResultType, Identity, CallerIPAddress, Raw
| order by TimeGenerated desc

Adapte les tables à la télémétrie réellement activée. La preuve importante est la même : la cible a-t-elle rejeté l’identité attendue, une autre identité, ou aucun appel n’arrive-t-il jusqu’à elle ?

Vérifier le scope minimal

Quand le refus est confirmé côté cible, vérifie le scope exact. Une identité peut avoir un rôle au mauvais registre ACR, au vault de staging, ou sur le groupe de ressources alors que la cible utilise une app role Entra.

bash 04-rbac-scope-check.sh
PRINCIPAL_ID="11111111-1111-1111-1111-111111111111"
TARGET_SCOPE="/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sec-prod/providers/Microsoft.KeyVault/vaults/kv-orders-prod"

az role assignment list --assignee "$PRINCIPAL_ID" --all --query "[].{role:roleDefinitionName,scope:scope,condition:condition}" --output table

az role assignment list --assignee "$PRINCIPAL_ID" --scope "$TARGET_SCOPE" --include-inherited --query "[].{role:roleDefinitionName,scope:scope}" --output table

Le correctif minimal est souvent un rôle précis au bon scope, une app role manquante, ou un retour à l’identité précédente. Le correctif maximal est rarement nécessaire.

Décider correction, rollback ou exception

La décision doit rester exploitable par l’astreinte et relisible après incident.

yaml container-apps-identity-decision.yml
fix_container_app_identity:
when:
  - identity_not_attached_to_app
  - wrong_user_assigned_client_id
  - active_revision_uses_staging_configuration
validation:
  - runtime_token_probe_returns_expected_client_id
  - active_revision_reaches_dependency

fix_target_authorization:
when:
  - token_is_issued_for_expected_audience
  - target_logs_show_denial_for_expected_principal
  - minimal_role_or_app_role_is_missing
validation:
  - target_log_shows_success_for_same_principal
  - permission_scope_matches_iac

rollback_revision:
when:
  - failure_started_with_new_revision
  - previous_revision_has_expected_identity_contract
  - identity_fix_cannot_be_validated_quickly
validation:
  - traffic_returns_to_previous_revision
  - dependency_calls_succeed

temporary_exception:
allowed_only_if:
  - user_impact_is_active
  - scope_is_same_or_narrower_than_expected
  - expiry_and_owner_are_recorded
blocked_if:
  - root_cause_is_unknown
  - broader_role_would_hide_wrong_identity

Une exception RBAC sans date d’expiration devient une dette de sécurité. Elle doit être traitée comme une mesure temporaire, pas comme une correction d’architecture.

Valider avant fermeture

La fermeture doit prouver le runtime, la cible et le rollback.

text identity-closure-checklist.txt
Validation finale
La révision active porte l'identité attendue
Le token est obtenu depuis le runtime pour la bonne audience
La cible accepte le principal attendu au scope minimal
Les logs montrent un succès avec le même principal
Les variables de staging ou secrets temporaires ont été retirés
L'IaC reflète l'état corrigé
Un rollback vers la révision précédente reste possible jusqu'à stabilisation

Si la correction a été appliquée au portail, elle reste incomplète tant que l’IaC, les alertes et le runbook ne décrivent pas le nouvel état.

Conclusion

Un 401 ou 403 dans Azure Container Apps ne doit pas déclencher automatiquement un élargissement de rôle. La bonne séquence consiste à prouver l’identité active, tester le token depuis la révision, lire les refus côté cible, vérifier le scope minimal, puis décider entre correction, rollback ou exception bornée.

La décision finale doit laisser une plateforme plus explicable qu’avant l’incident : une identité connue, un scope justifié, des logs corrélables et un chemin de rollback prêt si la nouvelle révision reste instable.