IAM : User, Role et Policy correctement

1. Aperçu

IAM (Identity and Access Management) est un service fondamental d’AWS, qui détermine « qui peut faire quoi avec quoi ». De nombreuses équipes l’utilisent mal dès le départ : créer un utilisateur avec des droits d’administrateur sur tout, attacher une policy *:* pour aller vite, ou confondre user et role. Cet article vous aidera à distinguer clairement les trois concepts user, role et policy, à écrire la policy JSON avec la bonne syntaxe, à appliquer le principe du moindre privilège, et à tester l’assomption de rôle avec le AWS SDK pour Java.

Après la lecture, vous serez confiant pour concevoir un ensemble IAM épuré, sécurisé, sans les droits « god-mode » pour chaque service.

2. Les trois composants essentiels

IAM tourne autour de trois concepts :

  • User : identité pour une personne ou une application spécifique, possède des credentials durables (mot de passe, clé d’accès).
  • Role : identité temporaire, n’a pas de credentials fixes, est « assumée » par un user, un service ou un autre compte.
  • Policy : document JSON décrivant les permissions (allow/deny) attachées à un user, rôle, groupe ou ressource.

Note : Un rôle n’est pas un « utilisateur sans mot de passe ». Un rôle génère des credentials temporaires (avec expiration, via STS), tandis qu’un user utilise des credentials fixes jusqu’à ce que vous les fassiez tourner (rotate).

3. User : quand l’utiliser, quand ne pas l’utiliser

Un user est approprié pour :

  • Les personnes qui ont besoin de se connecter à la Console pour un travail manuel.
  • Les cas inévitables où un rôle ne peut pas être utilisé (rare, par exemple un système legacy en dehors d’AWS qui a besoin d’une clé d’accès fixe).

Un user n’est pas approprié pour :

  • Les charges de travail s’exécutant sur EC2, Lambda, ECS — ces dernières doivent utiliser un rôle.
  • Partager un user entre plusieurs personnes (vous perdez la traçabilité par CloudTrail).

Créer un user via CLI :

aws iam create-user --user-name order-service-deployer

aws iam create-login-profile \
  --user-name order-service-deployer \
  --password 'TempPass123!' \
  --password-reset-required

La première ligne crée le user. La commande create-login-profile accorde le droit de se connecter à la Console avec un mot de passe temporaire, forcé à être modifié à la première connexion — pour éviter qu’un mot de passe par défaut persiste longtemps.

Erreur courante : créer une clé d’accès pour un user, la hard-coder dans le code et la commiter sur git. Si vous devez absolument utiliser une clé d’accès, activez la rotation périodique et utilisez Secrets Manager pour le stockage.

4. Role : l’identité temporaire au bon endroit

4.1. EC2 instance role

Quand une instance EC2 a besoin d’accéder à S3, ne mettez pas la clé d’accès dans .env. Créez un rôle et attachez un profil d’instance :

aws iam create-role \
  --role-name OrderServiceEc2Role \
  --assume-role-policy-document file://trust-policy.json

Le fichier trust-policy.json définit qui peut assumer ce rôle :

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Principal.Service spécifie que le service EC2 est autorisé à assumer ce rôle via le profil d’instance. C’est ce qu’on appelle une trust policy — différente de la permission policy décrite plus tard.

4.2. Cross-account role

Un rôle peut aussi être utilisé pour permettre à un autre compte AWS d’accéder à votre compte sans partager les credentials :

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "order-partner-2024" }
      }
    }
  ]
}

La Condition avec sts:ExternalId protège contre l’attaque du confused deputy — le tiers doit connaître le secret ExternalId pour assumer le rôle, ce qui évite qu’une autre partie abuse de l’ARN public.

5. Policy : structure et comment les écrire

Une policy JSON comprend Version et Statement — chaque statement a Effect, Action, Resource et optionnellement Condition.

Exemple : permettre à PaymentService de lire/écrire dans un bucket S3 spécifique, mais pas de supprimer :

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadWritePaymentBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    },
    {
      "Sid": "DenyDeleteObject",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    }
  ]
}

Le premier statement Allow deux actions GetObject et PutObject uniquement sur le chemin payment-receipts-bucket/*. Le deuxième statement Deny refuse explicitement la suppression — même si une autre policy s3:* est ajoutée plus tard, le deny explicite l’emporte toujours.

Attachez cette policy au rôle :

aws iam put-role-policy \
  --role-name PaymentServiceRole \
  --policy-name PaymentBucketAccess \
  --policy-document file://payment-bucket-policy.json

5.1. Identity-based vs resource-based policy

  • Identity-based policy : attachée à un user/rôle/groupe, répond à « que peut faire cette identité ».
  • Resource-based policy : attachée directement à une ressource (S3 bucket policy, SQS queue policy, KMS key policy), répond à « qui peut accéder à cette ressource ».

Exemple de bucket policy (resource-based) permettant à un rôle spécifique d’accéder :

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/PaymentServiceRole"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    }
  ]
}

Ces deux types de policies peuvent être combinés — la permission finale est l’intersection des deux (l’identity-based et la resource-based doivent toutes deux autoriser, si l’une refuse alors c’est refusé, sauf en cross-account où les deux doivent autoriser).

6. Le principe du moindre privilège

Au lieu d’écrire "Action": "s3:*" pour aller vite, listez exactement les actions nécessaires. Utilisez Condition pour restreindre davantage.

Exemple : permettre la lecture d’objets uniquement si la requête provient d’un VPC spécifique :

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::order-exports-bucket/*",
      "Condition": {
        "StringEquals": {
          "aws:SourceVpc": "vpc-0abc1234"
        }
      }
    }
  ]
}

aws:SourceVpc est une global condition key, autorisant seulement si la requête provient d’un VPC endpoint du VPC spécifié — utile quand vous ne voulez pas que les données quittent le réseau interne.

Approche pratique pour atteindre le moindre privilège :

  1. Commencez avec une policy vide (deny par défaut).
  2. Lancez la charge de travail, consultez IAM Access Analyzer ou les logs CloudTrail pour voir quelles actions sont refusées.
  3. Ajoutez exactement ces actions à la policy, pas plus.
  4. Répétez jusqu’à ce que la charge de travail s’exécute correctement sans être bloquée.

Note : Utilisez aws iam generate-service-last-accessed-details pour voir quelles actions un user/rôle a réellement utilisées au cours des 90 derniers jours — puis supprimez les actions inutiles dans la policy existante.

7. La logique d’évaluation des policies à retenir

AWS évalue les permissions selon l’ordre suivant, que vous devez retenir pour déboguer rapidement :

  1. Par défaut tout est refusé (implicit deny).
  2. S’il y a un statement avec Deny explicite → le résultat final est deny, rien ne peut le sauver.
  3. S’il n’y a pas de deny explicite, cherchez un statement Allow correspondant → allow.
  4. S’il n’y a pas d’allow correspondant → c’est toujours deny (implicit deny par défaut).
Request đến → Có Deny tường minh khớp? 
  ├─ Có → DENY (kết thúc)
  └─ Không → Có Allow khớp?
       ├─ Có → ALLOW
       └─ Không → DENY (implicit)

Comprendre correctement cet ordre vous évite le bug classique : attacher la policy AdministratorAccess à un user mais toujours être bloqué sur une action, parce qu’une SCP (Service Control Policy) au niveau de l’Organisation refuse explicitement cette action.

8. Tester l’assomption de rôle avec le AWS SDK pour Java

Ajoutez la dépendance du SDK v2 :

<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>sts</artifactId>
    <version>2.28.11</version>
</dependency>

Le service appelle STS pour assumer un rôle et obtenir des credentials temporaires :

public class RoleAssumer {

    private final StsClient stsClient;

    public RoleAssumer(StsClient stsClient) {
        this.stsClient = stsClient;
    }

    public Credentials assumePaymentServiceRole() {
        AssumeRoleRequest request = AssumeRoleRequest.builder()
                .roleArn("arn:aws:iam::123456789012:role/PaymentServiceRole")
                .roleSessionName("payment-service-session")
                .durationSeconds(900)
                .build();

        AssumeRoleResponse response = stsClient.assumeRole(request);
        return response.credentials();
    }
}

roleArn spécifie le rôle à assumer, roleSessionName nomme la session pour tracer facilement dans CloudTrail, durationSeconds limite la durée de vie des credentials temporaires — plus c’est court, plus c’est sûr.

Testez avec JUnit 5 et Mockito pour vérifier que la logique appelle le bon role ARN, sans appeler AWS réellement :

@Test
void assumePaymentServiceRole_shouldRequestCorrectRoleArn() {
    StsClient stsClient = mock(StsClient.class);
    Credentials fakeCredentials = Credentials.builder()
            .accessKeyId("AKIAFAKE")
            .secretAccessKey("secretFake")
            .sessionToken("tokenFake")
            .build();

    when(stsClient.assumeRole(any(AssumeRoleRequest.class)))
            .thenReturn(AssumeRoleResponse.builder()
                    .credentials(fakeCredentials)
                    .build());

    RoleAssumer assumer = new RoleAssumer(stsClient);
    Credentials result = assumer.assumePaymentServiceRole();

    ArgumentCaptor<AssumeRoleRequest> captor = ArgumentCaptor.forClass(AssumeRoleRequest.class);
    verify(stsClient).assumeRole(captor.capture());

    assertThat(captor.getValue().roleArn())
            .isEqualTo("arn:aws:iam::123456789012:role/PaymentServiceRole");
    assertThat(result.accessKeyId()).isEqualTo("AKIAFAKE");
}

Ce test mock StsClient, capture la requête réellement envoyée, puis affirme que roleArn est correct. Cela vous aide à détecter tôt si quelqu’un modifie accidentellement le hard-code du role ARN dans le code.

assumePaymentServiceRole_shouldRequestCorrectRoleArn() PASSED

9. Les erreurs courantes à éviter

  • Attacher AdministratorAccess à tous les comptes de service parce que « j’optimiserai plus tard ». En réalité, personne ne revient à l’optimisation, le risque persiste éternellement.
  • Utiliser des clés d’accès pour les charges de travail sur EC2/Lambda au lieu d’un rôle — les clés d’accès fuient facilement, ne se rotatent pas automatiquement et n’ont pas d’expiration.
  • Oublier de vérifier la trust policy d’un rôle : Principal trop large (par exemple "AWS": "*") permet à n’importe qui d’assumer.
  • Ne pas utiliser Condition avec MFA pour les actions sensibles (supprimer un bucket, modifier une policy IAM). Ajoutez "Bool": {"aws:MultiFactorAuthPresent": "true"} pour les actions à haut risque.
  • Mélanger plusieurs responsabilités dans une grande policy rend la revue difficile. Séparez par ressource/service, nommez clairement (PaymentBucketReadWrite, pas MyPolicy1).

Quand VOUS N’AVEZ PAS BESOIN d’IAM complexe : Si vous travaillez dans un sandbox personnel ou apprenez un nouveau service, créer des rôles/policies détaillés dès le départ peut ralentir votre apprentissage. Dans ce cas, un user temporaire avec une policy managed existante (AmazonS3ReadOnlyAccess) suffit, tant que vous le supprimez après.

10. Conclusion

Les users sont pour ceux qui ont besoin de se connecter manuellement, les rôles pour les charges de travail et les accès temporaires, les policies sont les règles JSON qui décident allow/deny. Comprendre l’ordre d’évaluation (le deny explicite gagne toujours, implicit deny par défaut) vous aide à déboguer vite quand des permissions « bizarres » se produisent. Appliquez le moindre privilège en commençant par rien, en ajoutant progressivement selon les vrais logs, et en testant toujours la logique d’assomption de rôle dans le code — c’est ainsi que vous gardez votre système AWS sécurisé sans frais de maintenance plus tard.

awsiamsecuritycloudjava