IAM: User, Role e Policy do jeito correto

1. Visão geral

IAM (Identity and Access Management) é o serviço fundamental da AWS, que define “quem pode fazer o quê com o quê”. Muitos times cometem erros desde o início: criar usuários com permissões de admin para tudo, vincular policy *:* para ser rápido, ou confundir user com role. Este guia ajuda você a distinguir claramente os três conceitos: user, role, policy, escrever JSON de policy com sintaxe correta, aplicar o princípio de least privilege, e testar o assumir role usando AWS SDK para Java.

Depois de ler, você estará confiante em projetar um conjunto IAM enxuto, seguro, sem “permissões god-mode” para todos os serviços.

2. Três componentes principais

IAM funciona em torno de três conceitos:

  • User: identidade para uma pessoa ou aplicação específica, com credenciais de longa duração (password, access key).
  • Role: identidade temporária, sem credenciais fixas, que pode ser “assumida” (assume) por um user, serviço, ou outra conta.
  • Policy: texto JSON que descreve permissões (allow/deny) anexado a um user, role, group, ou recurso.

Lembrete: Role não é “um user sem senha”. Role gera credenciais temporárias (com prazo de validade, via STS), enquanto user usa credenciais fixas até que você as rotacione.

3. User: quando usar, quando não usar

User é adequado para:

  • Pessoas que precisam fazer login no Console para trabalho manual.
  • Casos extremos onde role não pode ser usado (raro, por exemplo, sistemas legacy fora da AWS que precisam de access key fixo).

User não é adequado para:

  • Workloads rodando em EC2, Lambda, ECS — esses devem usar role.
  • Compartilhar um user com várias pessoas (perde rastreabilidade no CloudTrail).

Criar um 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

O primeiro comando cria o user. O comando create-login-profile concede permissão para fazer login no Console com uma senha temporária, forçando uma alteração na primeira vez — evita deixar a senha padrão por muito tempo.

Erro comum: criar access key para o user e fazer hard-code no código, commitando no git. Se for obrigatório usar access key, ative rotação periódica e use Secrets Manager para armazenar.

4. Role: identidade temporária no lugar certo

4.1. EC2 instance role

Quando uma instância EC2 precisa chamar S3, não coloque access key no .env. Crie uma role e anexe instance profile:

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

O arquivo trust-policy.json define quem pode assumir essa role:

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

Principal.Service especifica que o serviço EC2 pode assumir essa role através de instance profile. Isso é chamado trust policy — diferente da permission policy descrita depois.

4.2. Cross-account role

Role também é usada para permitir que outra conta AWS acesse sua conta sem compartilhar credenciais:

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

Condition com sts:ExternalId protege contra confused deputy attack — a terceira parte precisa saber o ExternalId secreto para assumir a role, evitando que um quarto agente explore o ARN público.

5. Policy: estrutura e como escrever

Uma policy JSON consiste em Version, Statement — cada statement tem Effect, Action, Resource, e opcionalmente Condition.

Exemplo: permitir que PaymentService leia/escreva um bucket S3 específico, sem poder deletar:

{
  "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/*"
    }
  ]
}

O primeiro statement Allow as duas ações GetObject e PutObject apenas no caminho payment-receipts-bucket/*. O segundo statement Deny torna explícito a operação de deleção — mesmo que depois alguém anexe uma policy s3:*, o deny explícito sempre vence.

Anexar essa policy à role:

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

5.1. Policy baseada em identidade vs baseada em recurso

  • Identity-based policy: anexada a user/role/group, responde “o que essa identidade pode fazer”.
  • Resource-based policy: anexada diretamente a um recurso (bucket policy do S3, queue policy do SQS, key policy do KMS), responde “quem pode acessar esse recurso”.

Exemplo de bucket policy (resource-based) que permite apenas uma role específica:

{
  "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/*"
    }
  ]
}

Esses dois tipos de policy podem ser combinados — a permissão final é a interseção de ambas (tanto identity-based quanto resource-based devem permitir, se um não permitir é bloqueado, exceto em cross-account onde ambos devem permitir).

6. Princípio de least privilege

Em vez de escrever "Action": "s3:*" por pressa, liste as ações exatas que você precisa. Use Condition para restringir ainda mais.

Exemplo: permitir leitura de objeto apenas se a requisição vem de uma VPC específica:

{
  "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 é uma chave de condição global, permite apenas se a requisição vem do VPC endpoint da VPC especificada — útil quando você não quer que dados saiam da rede interna.

Abordagem prática para alcançar least privilege:

  1. Comece com uma policy vazia (deny por padrão).
  2. Execute o workload, veja IAM Access Analyzer ou logs do CloudTrail para ações negadas.
  3. Adicione exatamente essas ações à policy, sem excesso.
  4. Repita até que o workload rode normalmente sem bloqueios.

Dica: Use aws iam generate-service-last-accessed-details para ver quais ações um user/role realmente usou nos últimos 90 dias — assim você pode remover ações desnecessárias da policy atual.

7. Lógica de avaliação de policy para lembrar

A AWS avalia permissões nesta ordem — memorize isso para debugar rápido:

  1. Por padrão tudo é negado (implicit deny).
  2. Se houver qualquer statement com Deny explícito que corresponda → resultado final é deny, nada pode salvar.
  3. Se não houver deny explícito, procure um statement Allow que corresponda → allow.
  4. Se nenhum allow corresponder → ainda é deny (implicit deny por padrão).
Requisição chega → Tem Deny explícito que corresponde? 
  ├─ Sim → DENY (fim)
  └─ Não → Tem Allow que corresponde?
       ├─ Sim → ALLOW
       └─ Não → DENY (implicit)

Entender essa ordem certa evita bugs clássicos: anexar AdministratorAccess a um user mas ainda ser bloqueado em uma ação, porque há um SCP (Service Control Policy) no nível Organization que nega explicitamente a ação.

8. Testando assumir role com AWS SDK para Java

Adicione a dependência SDK v2:

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

Um serviço chama STS para assumir a role e obter credenciais temporárias:

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 especifica a role a assumir, roleSessionName nomeia a sessão para facilitar rastreamento no CloudTrail, durationSeconds limita o tempo de vida das credenciais temporárias — quanto mais curto, mais seguro.

Teste com JUnit 5 e Mockito para verificar que a lógica chama a role ARN correta, sem fazer chamadas AWS reais:

@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");
}

Esse teste faz mock do StsClient, captura a requisição real enviada, depois faz assert que o roleArn é o esperado. Assim você detecta cedo se alguém acidentalmente muda o ARN hard-coded no código.

assumePaymentServiceRole_shouldRequestCorrectRoleArn() PASSED

9. Erros comuns para evitar

  • Anexar AdministratorAccess a todo service account porque “vamos otimizar depois”. Na prática ninguém volta a otimizar, o risco fica para sempre.
  • Usar access key para workloads em EC2/Lambda em vez de role — access key vaza fácil, não se rotaciona automaticamente, sem prazo de expiração.
  • Esquecer de revisar trust policy da role: Principal muito amplo (ex: "AWS": "*") permite que qualquer um assuma.
  • Não usar Condition com MFA para ações sensíveis (deletar bucket, modificar policy IAM). Adicione "Bool": {"aws:MultiFactorAuthPresent": "true"} para ações de alto risco.
  • Misturar muitas responsabilidades em uma policy grande dificultando a revisão. Separe por recurso/serviço, com nomes claros (PaymentBucketReadWrite, não MyPolicy1).

Quando NÃO precisa de IAM complexo: Se está fazendo sandbox pessoal, aprendendo um novo serviço, criar role/policy detalhada desde o início pode desacelerar o aprendizado. Nesse caso, usar um user temporário com policy managed existente (AmazonS3ReadOnlyAccess) é o suficiente, desde que você o delete quando terminar.

10. Conclusão

User é para pessoas que precisam fazer login manual, role é para workloads e acesso temporário, policy é um conjunto de regras JSON que define allow/deny. Memorizar a ordem de avaliação (deny explícito sempre vence, implicit deny é o padrão) ajuda a debugar rápido quando permissões “estranhas” acontecem. Aplique least privilege começando vazio, adicionando gradualmente baseado em logs reais, e sempre escreva testes para a lógica de assumir role no código — essa é a maneira de manter sua infraestrutura AWS segura sem dor de cabeça de manutenção depois.

awsiamsecuritycloudjava