Distribuée
Cloud & AI Security
← Tous les insights

Posture cloud

Escalade de privilèges IAM : les 8 patterns qu'on retrouve dans presque toutes les PME

iam:PassRole, CreatePolicyVersion, UpdateAssumeRolePolicy : 8 chemins d'escalade IAM courants, comment les bloquer (SCP, conditions, Access Analyzer en CI) et les prouver à un auditeur.

· 7 min · #iam#privilege-escalation#passrole#scp#access-analyzer#least-privilege#aws

Dans une revue IAM, la ligne qui inquiète rarement les équipes est celle d’un développeur avec « seulement » lambda:CreateFunction et iam:PassRole. Aucune des deux actions n’a l’air dangereuse isolément. Ensemble, elles permettent de lancer du code sous n’importe quel rôle du compte, y compris AdministratorAccess.

Une escalade de privilèges IAM est un chemin qui permet à une identité de s’attribuer des droits qu’on ne lui a pas donnés, sans exploiter de vulnérabilité : uniquement avec des permissions légitimes, mal combinées. C’est l’étape qui transforme une access key de développeur volée ou un workflow CI compromis en accès administrateur. La plupart de ces chemins sont documentés depuis des années (Rhino Security Labs, Hacking The Cloud) et des outils comme Pacu les automatisent. Ils restent pourtant présents dans presque tous les comptes que je revois en mission.

Cet article liste 8 patterns, puis donne le contrôle (SCP, conditions, vérification en CI) et la preuve pour chacun.

La menace : 8 patterns qui reviennent

Dans chaque cas, l’attaquant part d’une identité à privilèges limités (utilisateur IAM, rôle de CI, rôle d’un développeur).

#Permissions de départRésultat
1iam:PassRole sur * + lambda:CreateFunction + lambda:InvokeFunctionCode exécuté sous n’importe quel rôle, y compris admin
2iam:PassRole sur * + ec2:RunInstancesInstance avec un profil admin, credentials via IMDS
3iam:CreatePolicyVersionRéécrit une politique gérée déjà attachée à soi (--set-as-default)
4iam:AttachUserPolicy, AttachRolePolicy, PutUserPolicy, PutRolePolicyAttache AdministratorAccess à soi-même
5iam:UpdateAssumeRolePolicy + sts:AssumeRoleModifie la trust policy d’un rôle admin pour s’y ajouter
6Trust policy arn:aws:iam::<compte>:root sans condition, ou "AWS": "*"Toute identité du compte (ou pire) peut assumer le rôle
7iam:CreateAccessKey, iam:CreateLoginProfile, iam:UpdateLoginProfile sur d’autres utilisateursPrend l’identité d’un utilisateur plus privilégié
8lambda:UpdateFunctionCode (ou ecs:RegisterTaskDefinition, glue:UpdateDevEndpoint…) sur une ressource qui tourne avec un rôle adminDétourne une charge existante plutôt que d’en créer une

Deux remarques de terrain :

  • Les patterns 1 et 2 sont les plus fréquents parce que iam:PassRole avec "Resource": "*" est écrit « pour que ça marche » lors d’un premier déploiement et jamais resserré. La documentation AWS recommande explicitement de limiter les rôles passables et le service cible.
  • Le pattern 8 est le plus sous-estimé : une Lambda de « maintenance » avec un rôle large, modifiable par toute l’équipe, est un rôle admin déguisé.

Un détail sur le pattern 3 : une politique gérée a au plus 5 versions ; un attaquant avec iam:CreatePolicyVersion peut en créer une nouvelle avec Allow *:* et la définir par défaut. Un simple iam:SetDefaultPolicyVersion suffit aussi s’il existe une ancienne version permissive.

Contrôle 1 : limiter qui peut écrire dans IAM (SCP)

La règle la plus efficace est aussi la plus simple : seuls un ou deux rôles identifiés peuvent modifier IAM. Les développeurs et la CI applicative n’en ont pas besoin ; l’IaC qui gère IAM passe par un rôle dédié.

SCP à attacher aux OU de workload (pas à la racine tant que vous n’avez pas testé, et jamais sur le compte de management qui n’est pas soumis aux SCP) :

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyIamPrivilegeWritesExceptPlatform",
      "Effect": "Deny",
      "Action": [
        "iam:CreatePolicyVersion",
        "iam:SetDefaultPolicyVersion",
        "iam:AttachUserPolicy",
        "iam:AttachRolePolicy",
        "iam:AttachGroupPolicy",
        "iam:PutUserPolicy",
        "iam:PutRolePolicy",
        "iam:PutGroupPolicy",
        "iam:UpdateAssumeRolePolicy",
        "iam:CreateAccessKey",
        "iam:CreateLoginProfile",
        "iam:UpdateLoginProfile",
        "iam:AddUserToGroup",
        "iam:DeleteRolePermissionsBoundary",
        "iam:DeleteUserPermissionsBoundary"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/PlatformIamAdmin",
            "arn:aws:iam::*:role/TerraformIamDeploy"
          ]
        }
      }
    }
  ]
}

Adaptez les deux ARN à vos rôles. Si vos utilisateurs passent par IAM Identity Center, ajoutez le rôle SSO d’administration (arn:aws:iam::*:role/aws-reserved/sso.amazonaws.com/*/AWSReservedSSO_PlatformAdmin_*) et testez d’abord dans un compte bac à sable : une SCP trop large qui bloque aussi l’équipe plateforme est une panne, pas un contrôle. Les SCP ne donnent aucun droit : elles ne font que poser un plafond.

Cette SCP couvre les patterns 3, 4, 5 et 7. Il reste PassRole et les trust policies.

Contrôle 2 : resserrer iam:PassRole

iam:PassRole ne se supprime pas (la CI en a besoin pour déployer des Lambda, des tâches ECS). Il se contraint : liste de rôles par préfixe, et service cible.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::123456789012:role/app-lambda-*",
      "Condition": {
        "StringEquals": {
          "iam:PassedToService": "lambda.amazonaws.com"
        }
      }
    }
  ]
}

Avec cette règle, l’attaquant du pattern 1 ne peut passer que des rôles app-lambda-* à Lambda. Il faut en contrepartie que ces rôles n’aient jamais de droits d’administration, et que l’équipe n’ait pas iam:CreateRole libre (sinon elle crée un rôle app-lambda-admin). Dans la même logique, un permissions boundary imposé à tout rôle créé par les développeurs plafonne ce qu’un rôle créé peut faire.

Côté trust policies (pattern 6), la valeur sûre pour un accès inter-comptes dans une organisation est de conditionner sur l’organisation :

{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::210987654321:role/DeployFromTooling" },
  "Action": "sts:AssumeRole",
  "Condition": {
    "StringEquals": { "aws:PrincipalOrgID": "o-exampleorgid" }
  }
}

Préférez un principal précis (un rôle) à :root, qui délègue la décision à l’administrateur du compte distant.

Contrôle 3 : bloquer le problème avant le merge (Access Analyzer en CI)

Les SCP protègent le runtime ; il faut aussi empêcher qu’une politique dangereuse soit proposée. IAM Access Analyzer propose des vérifications de politiques personnalisées basées sur le raisonnement automatisé : CheckAccessNotGranted répond PASS/FAIL à la question « cette politique accorde-t-elle une de ces actions ? » (annonce AWS, juin 2024). Vérifiez la tarification d’Access Analyzer avant de l’appeler sur chaque pull request d’un gros monorepo.

Étape de CI sur le plan Terraform déjà converti en documents de politique (un fichier JSON par politique) :

#!/usr/bin/env bash
set -euo pipefail

FORBIDDEN='[{"actions":[
  "iam:CreatePolicyVersion","iam:SetDefaultPolicyVersion",
  "iam:AttachUserPolicy","iam:AttachRolePolicy",
  "iam:PutUserPolicy","iam:PutRolePolicy",
  "iam:UpdateAssumeRolePolicy","iam:CreateAccessKey",
  "iam:CreateLoginProfile","iam:UpdateLoginProfile"
]},{"actions":["iam:PassRole"],"resources":["*"]}]'

fail=0
for f in policies/*.json; do
  result=$(aws accessanalyzer check-access-not-granted \
    --policy-document "file://$f" \
    --policy-type IDENTITY_POLICY \
    --access "$FORBIDDEN" \
    --query result --output text)
  if [ "$result" != "PASS" ]; then
    echo "::error file=$f::Politique accordant une action IAM interdite"
    fail=1
  fi
done
exit $fail

Le rôle de CI qui exécute ce script n’a besoin que de access-analyzer:CheckAccessNotGranted : donnez-lui via OIDC, sans clé statique. Pour les politiques où ces actions sont légitimes (le rôle PlatformIamAdmin), gardez-les hors du dossier vérifié, avec une revue obligatoire par CODEOWNERS.

Contrôle 4 : détecter les tentatives

Même bloquées, les tentatives sont un signal. Les événements IAM sont globaux et apparaissent dans la région us-east-1 : la règle EventBridge doit y être créée.

resource "aws_cloudwatch_event_rule" "iam_privesc" {
  provider    = aws.us_east_1
  name        = "iam-privesc-attempts"
  description = "Écritures IAM sensibles ou tentatives refusées"

  event_pattern = jsonencode({
    source        = ["aws.iam"]
    "detail-type" = ["AWS API Call via CloudTrail"]
    detail = {
      eventName = [
        "CreatePolicyVersion", "SetDefaultPolicyVersion",
        "AttachUserPolicy", "AttachRolePolicy",
        "PutUserPolicy", "PutRolePolicy",
        "UpdateAssumeRolePolicy", "CreateAccessKey",
        "CreateLoginProfile", "UpdateLoginProfile"
      ]
    }
  })
}

resource "aws_cloudwatch_event_target" "iam_privesc_sns" {
  provider = aws.us_east_1
  rule     = aws_cloudwatch_event_rule.iam_privesc.name
  arn      = aws_sns_topic.security_alerts.arn
}

Ces appels doivent être rares et attribuables à PlatformIamAdmin ou au rôle Terraform. Toute autre identité qui apparaît, y compris avec errorCode = AccessDenied, mérite une question.

La preuve : ce qu’on montre à un auditeur

Pour un SOC 2 (CC6.1, CC6.3), une ISO 27001 (A.8.2, A.8.3) ou un questionnaire client, la démonstration tient en quatre éléments :

  1. La SCP attachée aux OU, exportée (aws organizations describe-policy) et versionnée dans Git.
  2. Le résultat de CheckAccessNotGranted sur chaque pull request, conservé comme artefact de CI : c’est la preuve continue que le contrôle fonctionne, pas seulement qu’il existait un jour.
  3. L’inventaire des rôles pouvant écrire dans IAM (deux, nommés), obtenu par une requête sur le simulateur ou Access Analyzer.
  4. Un test négatif daté : une tentative aws iam attach-role-policy depuis un rôle de développeur, qui renvoie AccessDenied explicite sur la SCP, avec l’événement CloudTrail correspondant.

Ce dernier point est le plus convaincant. Un contrôle qui n’a jamais été testé en échec ne vaut qu’une intention.

Par où commencer

Dans l’ordre de rapport efficacité/effort pour une PME :

  1. Lister les identités avec iam:PassRole sur * et les contraindre (une demi-journée).
  2. Déployer la SCP d’écriture IAM sur un compte de test, puis sur les OU de workload.
  3. Ajouter la vérification Access Analyzer dans la CI Terraform.
  4. Activer la règle de détection.
  5. Passer en revue les rôles de service (Lambda, ECS, Glue) qui ont plus de droits que nécessaire, avec les findings d’accès inutilisé d’Access Analyzer.

Ces cinq étapes sont le cœur d’un audit de posture cloud. Si vous voulez qu’on le fasse sur votre organisation, c’est l’objet de l’Audit Sécurité Cloud ; un retainer permet ensuite de garder les contrôles en place quand l’équipe et les comptes grossissent.

Cet article vous a été utile ? Partagez-le.

Aller plus loin

Un sujet, une mission, une question ?

Distribuée accompagne des PME exigeantes sur la sécurité cloud, la supply chain CI/CD et la sécurité IA.

Réserver 15 min