O PR mexe na identity policy da role app-reader e coloca Effect: Allow, Action: s3:*, Resource: arn:aws:s3:::amzn-s3-demo-bucket/*. O revisor vê o asterisco, conclui que a aplicação pode fazer qualquer coisa nos objetos do bucket e aprova. Depois do deploy, o PutObject continua falhando. Não existe Deny na role nem na bucket policy. O que existe é uma permissions boundary configurada na role, que fica em outro stack e que ninguém abriu, e ela só autoriza s3:GetObject no prefixo app/.
O diff é generoso. O teto continua onde estava.
A ideia aqui é sair com dois JSON locais, uma identity policy larga e uma boundary estreita, junto com a regra da documentação IAM que explica por que só uma das três requisições passa. A fonte é o IAM User Guide e o S3 User Guide, consultados em 22/10/2026. Nada rodou contra uma conta AWS nem no Policy Simulator.
A regra, uma vez só
A página de permissions boundaries define o mecanismo assim:
> AWS supports permissions boundaries for IAM entities (users or roles). A permissions boundary is an advanced feature for using a managed policy to set the maximum permissions that an identity-based policy can grant to an IAM entity. An entity's permissions boundary allows it to perform only the actions that are allowed by both its identity-based policies and its permissions boundaries.
Dessa definição saem três consequências práticas:
- Só users ou roles. A identity policy pode estar anexada a um grupo, mas a boundary é configurada no usuário ou na role. Grupo não tem boundary.
- A boundary não concede nada. Ela define o máximo que as identity policies podem conceder. A página de políticas e permissões diz isso com todas as letras: a boundary "defines the maximum permissions that the identity-based policies can grant to an entity, but does not grant permissions".
- O resultado é interseção. A página de lógica de avaliação diz que, ao avaliar identity policies e boundary, "the resulting permissions are the intersection of the two categories", e acrescenta que "an explicit deny in either of these policies overrides the allow".
Falta um detalhe que muda a forma como você lê o erro. Na ordem de avaliação, depois da varredura de Deny explícito, a AWS exige o Allow da identity policy e então verifica a boundary:
> If the policy that is used to set the permissions boundary does not allow the requested action, then the request is implicitly denied.
É implicit deny, um teto. Ninguém escreveu Effect: Deny em lugar nenhum. A ação simplesmente não está dentro do que a boundary permite.
Os dois JSON e a tabela
Os placeholders seguem o estilo da documentação AWS: a role arn:aws:iam::111122223333:role/app-reader e o bucket amzn-s3-demo-bucket. Os dois documentos usam "Version": "2012-10-17", a versão atual da linguagem de políticas. Nenhum deles tem Principal: a identity policy não pode ter esse elemento, e a boundary é uma managed policy com o mesmo formato, usada como teto.
Identity policy da role:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IdentityAllowS3StarOnDemoBucketObjects",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
}
]
}
Permissions boundary da mesma role:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BoundaryAllowGetObjectAppPrefixOnly",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/app/*"
}
]
}
Para tornar a regra visível, um script Python local avalia três requisições contra os dois documentos. Ele implementa só o subconjunto documentado: Allow efetivo se a identity e a boundary permitem, e Deny explícito em qualquer uma das duas vence. Isto não é o motor do IAM. O script não modela SCP, RCP, resource policy, session policy, NotAction nem policy variables. A tabela ilustra a interseção que o User Guide descreve e não substitui uma avaliação na conta.
| Requisição | Identity | Boundary | Efetivo | Por quê |
|---|---|---|---|---|
s3:GetObject em .../app/file.txt | Allow | Allow | Allow | os dois documentos casam |
s3:PutObject em .../app/file.txt | Allow | implicit Deny | Deny | a identity permite, a boundary não |
s3:GetObject em .../other/file.txt | Allow | implicit Deny | Deny | fora do prefixo da boundary |
A coluna da identity mostra Allow nas três linhas, porque s3:* em bucket/* casa com os três ARNs de objeto. Se a revisão para nessa coluna, a resposta sai errada duas vezes em três.
A segunda linha é a do PR do começo: PutObject "liberado" no JSON da role e implicit deny no teto. A terceira pega quem acha que a boundary só restringe ações. O Resource dela também é um teto, e other/ fica fora dele.
Não é o Deny da bucket policy (nem SCP)
O texto sobre Deny na resource policy tratava de outro mecanismo. Lá, identity policy e bucket policy na mesma conta se combinam por união, e um Effect: Deny explícito na bucket policy derruba o Allow da identity. Aqui não há Deny em nenhum dos dois documentos. O acesso falha porque a combinação é por interseção, e a boundary nunca disse Allow para PutObject. Se você abrir a bucket policy procurando o culpado, não vai achar nada lá. Também é diferente do IfExists na política IAM, que era uma chave de contexto ausente dentro de uma única identity policy.
Os outros dois tetos que aparecem nessa conversa:
| Mecanismo | Onde fica | Concede? | Como combina com a identity |
|---|---|---|---|
| Permissions boundary | managed policy no user ou na role | não | interseção |
| SCP | organização, OU ou conta | não | interseção; com boundary, as três precisam permitir |
| Session policy | passada ao assumir a role ou ao pedir credenciais federadas | não | interseção de identity, session policy e boundary |
A página de boundaries resume o caso combinado: quando SCP, boundary e identity policy afetam o mesmo request, "the request is allowed only if all three policy types allow it". A SCP é o teto da organização, anexado em outro nível e mantido por outro time. A session policy só vale para aquela sessão temporária. A boundary acompanha a role.
Template mínimo: role de app Java que só lê app/
Para uma aplicação que deve apenas ler objetos sob app/ e listar esse prefixo, a boundary mínima fica assim:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowGetObjectOnAppPrefix",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket/app/*"
},
{
"Sid": "AllowListBucketAppPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::amzn-s3-demo-bucket",
"Condition": {
"StringLike": {
"s3:prefix": "app/*"
}
}
}
]
}
Os dois Resource são diferentes por um motivo. Nos exemplos de identity policy do S3, as ações de objeto (GetObject, PutObject, DeleteObject) usam o ARN do objeto, arn:aws:s3:::bucket/prefixo/*. Já s3:ListBucket usa o ARN do bucket, sem /*, e restringe a listagem com "Condition": { "StringLike": { "s3:prefix": ["${aws:username}/*"] } }. O template troca a variável por um prefixo fixo, app/*. Trocar os ARNs entre as duas ações é um erro clássico. Repare também que a boundary do fixture anterior, que só tem GetObject, não permite ListBucket.
Três cuidados para não estragar o template:
- A boundary não concede. A role ainda precisa de uma identity policy com Allow para essas mesmas ações. O caminho mais simples é repetir os dois statements na identity policy. O
s3:*embucket/*do fixture nem cobririaListBucket, porque o ARN do bucket não casa combucket/*. - Configure como permissions boundary da role, não como mais uma identity policy anexada, e nunca em grupo, que não aceita boundary.
- Sem
Principal. O documento tem o formato de uma identity policy.
Limites do que foi mostrado
Os resultados da tabela vêm da documentação aplicada a dois JSON por um script local. Não houve conta AWS, CloudTrail nem Policy Simulator.
A interseção também não explica todos os caminhos de acesso. Pela página de policies, a boundary não limita o que uma resource-based policy concede, e o User Guide traz exceções: uma resource policy na mesma conta que concede acesso ao ARN de um IAM user não é limitada pelo implicit deny da boundary. Já uma concessão ao ARN de uma role é limitada. Por isso não vale a frase "a boundary sempre vence a bucket policy". O que sempre vale é o Deny explícito. Se a boundary tiver um statement com Effect: Deny que case com o request, ele derruba o acesso como em qualquer outra política avaliada, e os exemplos oficiais de boundary usam esse tipo de statement como guarda-corpo.
O que fazer na próxima revisão
Antes de tratar o s3:* da identity como acesso de produção, abra a permissions boundary configurada no user ou na role. Abra o documento de verdade, não só a policy que o PR altera. Só existe Allow efetivo onde os dois documentos dão Allow e nenhum deles nega. Se a boundary mora em outro stack, ela continua fazendo parte do diff, mesmo sem aparecer nele.
Para uma aplicação que só deve ler app/, a DevDojo coloca esse teto na role com o template acima e mantém uma identity policy com o mesmo Allow. Quando um PutObject falhar com a identity "liberando tudo", a primeira pergunta é "a boundary permite essa ação nesse ARN?", não "quem colocou Deny no bucket?". A DevDojo só abandonaria essa leitura se a boundary tiver Deny explícito, se uma SCP ou session policy entrar no request, ou se o acesso vier de uma resource policy. Nesses casos, a tabela de duas colunas não basta e a avaliação precisa acontecer na conta.