Automation
Azure Automation : prouver l'identité du runbook avant d'élargir le RBAC
Un runbook de production pour identifier le principal qui exécute vraiment un job Azure Automation, borner ses droits, valider par canari et garder un rollback.
Un runbook Azure Automation qui fonctionnait hier renvoie soudain 403 Forbidden. Le job atteint Azure, l’identité managée semble active et un rôle existe sur la ressource. La correction la plus rapide paraît être d’ajouter Contributor au compte Automation. C’est aussi le meilleur moyen de masquer une confusion d’identité et d’accorder des droits au mauvais principal.
Le cas d’usage est un runbook PowerShell lancé tantôt dans le sandbox Azure, tantôt sur un Hybrid Runbook Worker. Une identité managée a été ajoutée au compte Automation ou à la VM, puis un job ne peut plus lire un coffre, redémarrer une VM ou modifier une ressource cible. Ce runbook doit aboutir à une décision explicite : corriger le principal ou le scope, conserver le refus, promouvoir par canari, ou revenir à la chaîne d’identité précédente.
Figer un job et l’action refusée
Ne commencez pas par la page IAM. Conservez un job précis, sa version publiée, sa cible d’exécution, l’heure UTC et l’opération refusée. Un 403 ARM, un refus Key Vault data plane et une erreur locale sur un Hybrid Worker n’impliquent pas les mêmes rôles.
incident:
automation_account: aa-ops-prod
runbook: rotate-app-certificate
job_id: <job-guid>
started_at_utc: 2026-08-30T14:20:00Z
execution_target: cloud-or-hybrid-worker-group
hybrid_worker: <worker-name-or-not-applicable>
published_runbook_version: <commit-or-version>
denied_action:
resource_id: <exact-resource-id>
operation: <provider/action-or-data-action>
http_status: 403
correlation_id: <arm-or-service-correlation-id>
expected_identity:
type: automation-account-system-assigned-or-user-assigned-or-worker-vm
client_id: <expected-client-id>
principal_id: <expected-object-id>
stop_conditions:
- execution target is unknown
- denied operation is not identified
- proposed role scope is subscription-wide
- rollback identity is already disabled Préservez les streams du job avant une relance. Une relance peut partir sur un autre worker, charger un autre contexte ou produire un nouvel identifiant de corrélation. L’objectif initial n’est pas de faire réussir le job, mais de rendre l’échec attribuable.
Dessiner la chaîne d’identité attendue
Dans un sandbox cloud, Connect-AzAccount -Identity utilise normalement l’identité managée du compte Automation. Une identité user-assigned peut être sélectionnée explicitement avec son client ID. Sur un Hybrid Runbook Worker Azure, le choix est plus subtil : l’identité du compte Automation peut prendre la priorité sur celle de la VM, et l’identité user-assigned du compte n’est pas utilisable de la même façon sur le worker. La VM peut porter sa propre identité seulement lorsque l’architecture du compte et le code laissent effectivement ce chemin disponible.
Documentez donc le chemin attendu avant de lire les attributions de rôles.
Cloud sandbox
Connect-AzAccount -Identity
-> system-assigned identity of the Automation account
Cloud sandbox with explicit user-assigned identity
Connect-AzAccount -Identity -AccountId <client-id>
-> selected user-assigned identity attached to the Automation account
Hybrid Worker on an Azure VM
Automation account identity enabled
-> verify that the account system-assigned identity is the effective principal
Hybrid Worker using VM identity
-> prove the account configuration permits this path
-> select the VM system-assigned or user-assigned identity explicitly
Never infer identity from
runbook name, worker name, resource naming or an existing role assignment Un rôle présent sur mi-runbooks-prod ne prouve rien tant que le job n’a pas démontré qu’il utilise ce principal. Inversement, ajouter le même rôle à trois identités candidates transforme un incident d’authentification en dette de sécurité durable.
Reinitialiser le contexte Az dans le processus
Sur un worker réutilisé, un contexte Az peut survivre à une exécution précédente. Un runbook qui omet la connexion explicite peut alors agir avec un contexte inattendu. Désactivez l’autosave au niveau du processus, connectez l’identité voulue et transmettez le contexte obtenu aux cmdlets sensibles.
$ErrorActionPreference = 'Stop'
Disable-AzContextAutosave -Scope Process
Clear-AzContext -Scope Process -Force -ErrorAction SilentlyContinue
$expectedSubscriptionId = '<subscription-id>'
$expectedTenantId = '<tenant-id>'
$userAssignedClientId = '<empty-or-user-assigned-client-id>'
if ([string]::IsNullOrWhiteSpace($userAssignedClientId) -or
$userAssignedClientId -eq '<empty-or-user-assigned-client-id>') {
$context = (Connect-AzAccount -Identity -Tenant $expectedTenantId).Context
} else {
$context = (Connect-AzAccount -Identity -AccountId $userAssignedClientId -Tenant $expectedTenantId).Context
}
$context = Set-AzContext -SubscriptionId $expectedSubscriptionId -Tenant $expectedTenantId -DefaultProfile $context
[pscustomobject]@{
AccountId = $context.Account.Id
AccountType = $context.Account.Type
SubscriptionId = $context.Subscription.Id
TenantId = $context.Tenant.Id
Environment = $context.Environment.Name
} | ConvertTo-Json -Compress
if ($context.Subscription.Id -ne $expectedSubscriptionId) {
throw 'Unexpected subscription context. Stop before any write.'
} Ne journalisez ni jeton, ni secret, ni contenu complet de claims. Les identifiants de compte, tenant, souscription, job et correlation suffisent pour établir la chaîne tout en limitant l’exposition.
Comparer principal attendu et principal configure
Récupérez séparément le principalId de l’identité system-assigned, les clientId et principalId des identités user-assigned, puis les identités attachées au worker. Ne confondez pas client ID, object ID du service principal et resource ID de l’identité managée.
SUBSCRIPTION_ID="<subscription-id>"
AUTOMATION_RG="rg-automation-prod"
AUTOMATION_ACCOUNT="aa-ops-prod"
TARGET_RESOURCE_ID="<exact-target-resource-id>"
az account set --subscription "$SUBSCRIPTION_ID"
az automation account show --resource-group "$AUTOMATION_RG" --name "$AUTOMATION_ACCOUNT" --query '{id:id,systemPrincipalId:identity.principalId,userAssigned:identity.userAssignedIdentities}' --output json
EXPECTED_PRINCIPAL_ID="<object-id-proved-by-runtime-path>"
az rôle assignment list --assignee-object-id "$EXPECTED_PRINCIPAL_ID" --scope "$TARGET_RESOURCE_ID" --include-inherited --query '[].{role:roleDefinitionName,scope:scope,principalType:principalType}' --output table Une liste vide au scope exact ne signifie pas automatiquement qu’aucun droit n’existe : vérifiez les scopes parents et les groupes Entra si votre modèle d’accès les utilise. À l’inverse, une attribution visible ne garantit pas qu’elle contient l’action ou la data action refusée. Comparez l’opération précise avec la définition du rôle.
Separer control plane et data plane
Le job peut lire une ressource ARM tout en etant refusé par son service de données. Reader sur un coffre ne donne pas accès aux secrets. Un rôle de données sur un Storage Account ne permet pas de modifier son pare-feu. Avant toute correction, classez le refus :
- opération Azure Resource Manager sur la configuration de la ressource ;
- data plane sur un secret, un blob, une file ou une base ;
- opération Microsoft Graph ou Entra avec un modèle d’autorisation distinct ;
- accès local ou réseau depuis le Hybrid Worker.
Cette classification empêche d’accorder un rôle ARM large pour réparer un refus de données, ou de modifier le RBAC quand le worker ne résout simplement pas le nom cible.
Correlier le refus avec les preuves d’exécution
Alignez l’heure UTC, le job ID, la cible, l’opération et le correlation ID. Pour les opérations ARM, l’Activity Log aide à confirmer le caller et le scope atteint. Pour un service de données, utilisez ses diagnostics et les traces du runbook. Ne concluez pas sur la seule présence d’un 403 dans le stream d’erreur.
let StartTime = datetime(2026-08-30T14:15:00Z);
let EndTime = datetime(2026-08-30T14:35:00Z);
let TargetResourceId = tolower("<exact-target-resource-id>");
AzureActivity
| where TimeGenerated between (StartTime .. EndTime)
| where tolower(_ResourceId) == TargetResourceId
| where ActivityStatusValue =~ "Failure" or tostring(Properties) has "403"
| project TimeGenerated, OpérationNameValue, ActivityStatusValue,
Caller, CallerIpAddress, CorrelationId, ResourceGroup, Properties
| order by TimeGenerated asc Si le caller observé ne correspond pas au principal attendu, arrêtez le changement RBAC. Corrigez d’abord la sélection d’identité ou la cible d’exécution. Si le caller est correct, poursuivez avec l’action refusée, la définition du rôle, le scope et le délai de propagation.
Construire le plus petit correctif testable
Le correctif doit être un diff, pas une élévation temporaire non bornée. Les options acceptables sont par ordre de préférence : sélectionner explicitement l’identité prévue, retirer une dépendance à un contexte persistant, corriger un scope trop étroit, puis choisir un rôle plus précis contenant l’action nécessaire.
candidate:
runtime: cloud-sandbox-or-hybrid-worker
expected_principal_id: <object-id>
denied_operation: <provider/action-or-data-action>
target_scope: <smallest-resource-or-resource-group-scope>
proposed_role: <least-privilege-role>
previous_identity_path: <documented-path>
canary:
runbook: identity-readonly-canary
input_target: <non-critical-resource>
allowed_action: <read-or-dry-run-opération>
forbidden_action: <write-opération-that-must-still-fail>
promote_when:
- runtime principal matches the expected object ID
- allowed action succeeds at the intended scope
- forbidden action remains denied
- production runbook uses an explicit Az context
- job and service logs share a correlation window
rollback_when:
- a different principal appears
- accèss extends to an unintended scope
- hybrid and cloud jobs resolve different identity paths
- the denied action cannot be explained Le canari doit prouver un accès autorise et un accès encore refusé. Un simple succes ne montre pas que la frontiere de permission reste fermee.
Promouvoir puis surveiller les effets lateraux
Appliquez le changement par l’IaC ou le mécanisme qui possède l’attribution. Attendez la propagation, puis relancez d’abord le canari sur la même cible d’exécution que le job de production. Ensuite, exécutez le runbook avec une action bornée et une seule ressource cible.
Surveillez les nouveaux 403, mais aussi les accès réussis hors scope, les jobs lancés sur un autre worker, les changements de contexte de souscription et les opérations auparavant impossibles. Une baisse des erreurs n’est pas une preuve si le principal a obtenu trop de droits.
Décider correction, maintien du refus ou rollback
Corrigez la sélection d’identité lorsque le caller observé n’est pas celui du contrat. Corrigez le rôle ou le scope seulement lorsque le principal est prouvé et que l’action nécessaire manque effectivement. Conservez le refus lorsque l’opération n’appartient pas au mandat du runbook ou lorsque la cible est ambiguë.
Rollbackez si l’identité effective varie entre sandbox et Hybrid Worker, si le canari révèle un scope plus large, ou si le changement rend impossible l’attribution des actions. Le rollback consiste à restaurer le chemin d’identité et les attributions versionnées précédentes, pas à empiler un second rôle dans l’urgence.
Conclusion
Un 403 Azure Automation n’est pas une demande de privilège supplémentaire. C’est d’abord une question d’attribution : quel environnement a exécuté le job, quel principal a obtenu le jeton, quelle opération a été refusée et sur quel scope.
Figez le job, réinitialisez le contexte Az, prouvez le principal runtime, distinguez control plane et data plane, puis testez le plus petit correctif avec un canari positif et negatif. La bonne sortie n’est pas seulement un job vert : c’est une chaîne d’identité explicable, observable et réversible.