Configure AWS IAM roles and least-privilege policies
Design exact trust, identity, boundary, S3-prefix, and KMS-key permissions; validate with Access Analyzer, simulate candidates/live role, test temporary credentials, audit CloudTrail, reduce unused access, rotate trust, revoke sessions, and roll back safely.
Create an AWS role whose principal, session, actions, resources, context, maximum permissions, runtime behavior, audit, and retirement are all explicit and regression-tested.
- AWS IAM current regional/global APIs
- AWS CLI v2
- IAM policy language 2012-10-17
- CloudFormation 2010-09-09 template format
- Governed AWS organization Federation/MFA, SCPs, organization CloudTrail with required data events, IAM Access Analyzer, Config/security monitoring, and protected IAM change role.
- Owned principal and resources Exact trusted caller role, S3 bucket/prefix owner, KMS key owner/policy, region, endpoint/bucket policies, and workload lifecycle are documented.
- Executable access matrix Required and denied action/resource/context tuples, synthetic test data, policy assertions, and rollback owner exist before policy creation.
- Protected infrastructure source Reviewed CloudFormation/policy documents, no literal secrets/account IDs, exact change set approval, dependency inventory, and policy-version retention.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- A workload-oriented AWS IAM role whose trust policy names one exact principal and External ID condition, whose identity policy permits one documented S3 release prefix and one KMS key, and whose permissions boundary caps the role at the same approved service envelope.
- A policy engineering workflow that begins with an action/resource/condition matrix, runs IAM Access Analyzer validation and custom-policy simulation before creation, reviews a CloudFormation change set, tests allowed and denied calls with temporary credentials, and evaluates organization guardrails.
- An operating lifecycle with CloudTrail, external-access findings, service-last-accessed data, role session limits, controlled policy versioning, emergency session revocation, periodic reduction, and dependency-aware rollback.
- Only the exact trusted principal can call sts:AssumeRole with the required context; another principal, missing/wrong External ID, and excessive session duration are denied.
- The role can list only the approved S3 prefix, read/write only objects below that prefix, and use the exact KMS key for the required encryption operations; neighboring buckets, prefixes, keys, IAM actions, and administrative APIs are denied.
- Access Analyzer returns no unresolved ERROR or SECURITY_WARNING, simulations cover positive and negative action/resource/context tuples, and CloudTrail records assumption and data/control calls according to trail configuration.
- Role removal or policy rollback follows a dependency inventory and active-session plan instead of deleting policy versions blindly or broadening permissions to restore service.
Architecture
How the parts fit together
A protected CloudFormation stack owns a customer-managed permissions-boundary policy, a customer-managed workload policy, one IAM role, and their attachments. The role trust policy controls who may request temporary credentials; the role's identity policy controls what those credentials may call; the boundary caps the maximum identity-policy permissions but grants nothing by itself. Organization SCPs, resource policies, KMS key policy, VPC endpoint policy, and session policy can further restrict the effective decision. The example release publisher uses an exact trusted role ARN, a required External ID, a maximum one-hour session, an exact S3 bucket and releases/production prefix, and an exact KMS key. No Action or Principal wildcard exists. A trailing wildcard appears only in the object resource ARN to represent objects under the one approved prefix; it is not an account/service wildcard. Access Analyzer validates policy structure and security guidance. IAM simulators evaluate documented tuples but do not model every service, resource policy, SCP, key grant, or runtime context. Real tests use short-lived role credentials and synthetic objects.
- Identify the calling workload, data owner, exact API operations, resources, conditions, session duration, failure mode, and reviewer before writing JSON.
- Write trust, identity, boundary, S3 bucket, and KMS key policy together because effective access is their intersection with organization controls.
- Validate every policy with Access Analyzer and simulate explicit allow and deny tuples using realistic context.
- Create a CloudFormation change set, inspect every IAM resource and attachment, then execute only after named-IAM approval.
- Assume the role with the approved principal and test synthetic allowed calls plus neighboring-resource, IAM, and wrong-context denials.
- Correlate CloudTrail and Access Analyzer, record policy versions and dependencies, review last-accessed data over an adequate period, and remove unused permissions.
- Rotate the trusted principal/External ID and policy versions with overlap only when necessary; revoke sessions under incident control and roll back through the stack.
Assumptions
- The organization has AWS Organizations/SCP governance, a multi-region organization CloudTrail, IAM Access Analyzer, federated administrator identities with MFA, change review, and protected CloudFormation execution.
- The example role publishes encrypted release objects to an existing S3 bucket/prefix with an existing customer-managed KMS key. Bucket and key owners approve their resource policies separately.
- The trusted caller is one workload role, not an IAM user or broad account principal. The caller itself has sts:AssumeRole permission for only this target role.
- The External ID is a confused-deputy control rather than a password. It is unique and protected as operational metadata but not relied upon as the sole authentication factor.
- No literal AWS account IDs, access keys, secret keys, session tokens, object data, or internal role identifiers are committed to the tutorial. CloudFormation parameters/pseudo parameters and redacted evidence are used.
- A synthetic non-sensitive object and isolated production-like prefix are available for positive/negative tests; testing never overwrites a real release.
Key concepts
- Trust policy
- A role resource policy that names principals allowed to call role-assumption APIs under conditions. It is evaluated separately from the role's identity permissions.
- Identity policy
- A policy attached to an identity that grants actions on resources when conditions match, subject to boundaries, SCPs, resource policies, and explicit denies.
- Permissions boundary
- A maximum-permissions filter for identity policies. It grants no permission and does not constrain resource-based grants in every scenario the same way.
- Least privilege
- Only actions, resources, conditions, principals, and duration needed for the current workload, refined with tests and observed use rather than a broad service role.
- Policy validation versus simulation
- Validation finds syntax and security findings; simulation predicts decisions for supplied tuples. Neither proves all runtime policy layers or service behavior.
- Explicit deny
- A matching deny overrides an allow. SCP, boundary, session, identity, resource, endpoint, and key policies can all influence a request.
- Role session
- Temporary credentials issued by STS with a principal identity, duration, optional source identity/tags, and policies. Updating a role does not necessarily invalidate already issued sessions immediately.
Before you copy
Values used in this guide
{{awsRegion}}Approved region for S3/KMS and testing.
Example: eu-central-1{{iamStackName}}CloudFormation stack owning the role and policies.
Example: production-release-publisher-role{{roleName}}Stable role name without embedded account ID.
Example: ProductionReleasePublisher{{trustedPrincipalArn}}Exact approved caller role ARN injected through protected deployment parameters.
Example: arn:aws:iam::REDACTED:role/ApprovedCiPublisher{{externalId}}secretUnique confused-deputy context value delivered to both trust owner and caller; not treated as a standalone secret.
Example: organization-issued-value-redacted{{releaseBucketName}}Existing S3 bucket approved for release objects.
Example: organization-production-releases{{releasePrefix}}Exact object prefix owned by this workload.
Example: releases/production/{{kmsKeyArn}}Exact customer-managed KMS key ARN approved by the key owner.
Example: arn:aws:kms:eu-central-1:REDACTED:key/REDACTED{{policyVersionToRestore}}Reviewed non-default managed-policy version for rollback.
Example: v3{{releaseVersion}}Non-secret IAM policy change identifier used for change-set correlation.
Example: 2026.07.28-1Security and production boundaries
- Never use Principal:'*', Action:'*', service:* action families, Resource:'*' for resource-capable APIs, AdministratorAccess, PowerUserAccess, or an account-root principal as a shortcut.
- An object-prefix ARN necessarily ends with '*' to represent objects below that exact prefix. Pair it with an exact bucket ARN and s3:prefix conditions; do not broaden it to the bucket's entire object namespace.
- A role name or External ID is not authentication. The exact trusted principal, its own identity policy, organization membership, source identity/session tags, duration, and monitoring form the assumption boundary.
- KMS authorization is the intersection of IAM policy, key policy, grants, encryption context, and explicit denies. Do not add kms:* or all-key resources when a decrypt/encrypt test fails.
- Policy simulators do not evaluate every SCP, resource policy, permissions boundary, key grant, service control, or runtime condition in the same way as a real request. Keep negative live tests.
- CloudTrail management-event history does not automatically include all S3 object data events. Configure a retained organization trail and data-event selectors for the protected prefix as required.
Stop before continuing if
- Stop if the calling principal, resource owner, exact actions, ARNs, prefix, key, region, session duration, or rollback owner is unknown.
- Stop on any Action/Principal wildcard, account-wide trust, broad object wildcard, unbounded pass-role, IAM mutation, or policy-attachment authority.
- Stop if Access Analyzer returns an unresolved ERROR or SECURITY_WARNING, or simulations lack neighboring-resource and administrative-action denials.
- Stop if the change set replaces/deletes an existing shared role/policy, removes the boundary, or exposes literal identifiers/secrets.
- Stop if real negative tests succeed, CloudTrail is missing, active sessions cannot be inventoried, or resource policies/SCP/key policy are outside review.
verification
Inventory caller, resources, organization controls, and existing grants
Confirm the federated administrator role and alias, identify the exact trusted workload role, bucket, prefix, KMS key, region, SCPs, resource policies, endpoint policies, existing IAM policies, active sessions, and CloudTrail trail before creating another grant.
Why this step matters
IAM decisions are the intersection of more than the role policy. Existing bucket/key policies, SCPs, boundaries, endpoint policies, grants, and sessions can permit or deny independently. Inventory prevents duplicate roles and hidden grants. Numeric account IDs and policy contents can be sensitive topology, so evidence remains protected/redacted.
What to understand
Name resource and principal owners separately.
Record current role/policy attachments and policy versions.
Confirm organization trail and data-event scope.
System changes
- Queries identity and authorization inventory only.
Syntax explained
path-prefix- Narrows IAM inventory to the owned workload namespace.
effective SCP- Shows organization maximum/deny layer for the target.
aws sts get-caller-identity --query Arn --output text && aws iam list-account-aliases --query 'AccountAliases[0]' --output text && aws organizations describe-effective-policy --policy-type SERVICE_CONTROL_POLICY --target-id REDACTED_TARGET 2>/dev/null || true && aws iam list-roles --path-prefix /workloads/release/ --query 'Roles[].RoleName' --output textarn:aws:sts::REDACTED:assumed-role/IamPolicyOperator/session-redacted production-platform Effective organization policy reviewed in protected evidence No existing role named ProductionReleasePublisher
Checkpoint: All effective authorization owners are known
aws iam get-role --role-name {{roleName}} 2>&1 || trueContinue whenNo conflicting role exists, and principal/resource/organization owners plus policies are recorded.
Stop whenExisting role/grant, account/region, resource owner, active session, or policy layer is unexplained.
If this step fails
Organization policy cannot be read.
Likely causeOperator lacks Organizations read or is in member account.
Request protected SCP evidence from organization owner.
ResolutionDo not finalize policy until the effective organization constraints are reviewed.
Security notes
- Do not paste full sensitive policies or account IDs into public tickets.
Alternatives
- Use a centrally generated authorization graph from organization tooling.
Stop conditions
- Stop on incomplete policy-layer inventory.
instruction
Write an action, resource, condition, and denial test matrix
For every workload operation, name the API action, exact resource ARN pattern, required request/resource context, expected allow, neighboring-resource deny, administrative-action deny, owner, and evidence. Exclude permissions not exercised by the workflow.
Why this step matters
Least privilege starts from operations, not JSON. The matrix forces positive and negative cases before policy syntax. Neighboring resources catch prefix/ARN mistakes; administrative actions catch escalation. Conditions such as TLS, encryption key, prefix, region, source endpoint, and tags must be supplied in simulations or real tests.
What to understand
Separate bucket-level and object-level S3 actions/resources.
Distinguish encrypt, decrypt, data-key, and key administration.
Include session assumption failures and maximum duration.
System changes
- Creates local review/test data only.
Syntax explained
expected deny- First-class acceptance criteria, not merely absence from allow list.
owner- Person/team authorized to approve that resource operation.
column -t -s, policy-tests.csv | sed -n '1,20p'case action resource expected list-release-prefix s3:ListBucket arn:aws:s3:::organization-production-releases allow put-release-object s3:PutObject arn:aws:s3:::.../releases/production/test.bin allow put-other-prefix s3:PutObject arn:aws:s3:::.../releases/staging/test.bin deny change-bucket-policy s3:PutBucketPolicy arn:aws:s3:::organization-production-releases deny use-other-key kms:Encrypt arn:aws:kms:...:key/OTHER deny
Checkpoint: Every permission has a business operation and a negative boundary
awk -F, 'NR>1 {count[$4]++} END {print "allow=" count["allow"], "deny=" count["deny"]}' policy-tests.csvContinue whenAll required operations have allow tests and every resource/action boundary has denial tests; deny cases outnumber broad assumptions.
Stop whenAny requested action lacks exact resource/context/owner or an administrative capability is justified only as convenience.
If this step fails
Team cannot name exact API actions.
Likely causeWorkflow/tool behavior is undocumented or permissions were copied from a managed policy.
Capture calls in a sandbox through CloudTrail and service docs.
ResolutionPrototype in an isolated account and enumerate operations before production policy.
Security notes
- The matrix can reveal resource names and controls; protect it.
Alternatives
- Start with Access Analyzer generated policy from sandbox activity, then reduce manually.
Stop conditions
- Stop before JSON until the matrix is complete.
config
Trust one exact workload principal with bounded sessions
Name the exact caller role ARN, require the organization-issued External ID, and cap session duration at one hour. Do not trust an account root, wildcard, IAM user collection, or mutable principal tag without separate controls.
Why this step matters
Trust determines who receives credentials; identity policy determines what credentials can do. Exact role ARN avoids granting an entire account. External ID helps prevent confused-deputy use when a third party operates the caller but is not a secret or substitute for exact trust. MaxSessionDuration belongs on the role resource. For human access, require federation/MFA in the caller chain rather than adding users to this trust.
What to understand
Verify caller role ownership and its own sts:AssumeRole permission.
Plan role recreation because principal IDs can affect stored trust relationships.
Use source identity/session tags only with exact allowed keys/values.
System changes
- Validates local trust policy only.
Syntax explained
Principal exact role ARN- Only the named workload role can satisfy principal matching.
sts:ExternalId- Required assumption context mitigating confused-deputy scenarios.
iam/trust-policy.jsonValues stay on this page and are never sent or saved.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "TrustedPublisherOnly",
"Effect": "Allow",
"Principal": {"AWS": "{{trustedPrincipalArn}}"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "{{externalId}}"}
}
}]
}aws accessanalyzer validate-policy --policy-type RESOURCE_POLICY --validate-policy-resource-type AWS::IAM::AssumeRolePolicyDocument --policy-document file://trust-policy.json --query 'findings[].[findingType,issueCode,learnMoreLink]' --output table-------------------- | ValidatePolicy | +------------------+ | No findings | +------------------+
Checkpoint: Trust has one principal and one assumption path
jq -e '.Statement | length==1 and .[0].Principal.AWS!="*" and .[0].Action=="sts:AssumeRole"' iam/trust-policy.jsonContinue whenOne exact approved principal, no wildcard/account-root, required context, one-hour role maximum, and analyzer no security finding.
Stop whenPrincipal broadens, External ID is sole control, federation context is unclear, or analyzer finds external access.
If this step fails
Exact role ARN breaks after caller role recreation.
Likely causeIAM transformed the principal to an internal ID tied to deleted role.
Inspect trust policy principal representation and caller lifecycle.
ResolutionReapprove/reapply the exact new role ARN; do not switch to account-wide trust.
Security notes
- Treat External ID as protected metadata, not a password.
Alternatives
- Use exact OIDC provider and subject/audience claims for CI federation.
Stop conditions
- Stop if exact principal lifecycle cannot be governed.
config
Scope S3 objects, prefix listing, TLS, and exact KMS use
Separate bucket ListBucket from object access, constrain prefix and TLS, require the approved KMS key on writes, and allow only exact KMS data-plane actions on that key. Do not grant bucket policy, ACL, delete, IAM, key administration, or all-service operations.
Why this step matters
S3 uses different ARNs for bucket and objects. Prefix listing requires bucket ARN plus s3:prefix condition; object operations require the prefix object ARN. The trailing object wildcard is deliberately bounded by bucket and prefix. Read and write are separate because GetObject does not carry PutObject encryption headers. TLS and KMS headers prevent unencrypted writes, while the exact KMS ARN, region, and S3 encryption context prevent use with another key or namespace.
What to understand
Consider omitting Decrypt for write-only publishing.
Use bucket policy to deny insecure transport and wrong encryption independently.
Avoid s3:DeleteObject unless lifecycle/rollback explicitly requires it.
System changes
- Validates candidate identity policy only.
Syntax explained
s3:prefix- Constrains bucket listing to one logical object namespace.
object ARN prefix*- Represents objects beneath one approved prefix, not all bucket objects.
exact KMS ARN- Limits cryptographic data-plane calls to one customer-managed key.
iam/publisher-policy.jsonValues stay on this page and are never sent or saved.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListOnlyReleasePrefix",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": ["arn:aws:s3:::{{releaseBucketName}}"],
"Condition": {"StringLike": {"s3:prefix": ["{{releasePrefix}}*"]}}
},
{
"Sid": "ReadReleaseObjectsOverTls",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::{{releaseBucketName}}/{{releasePrefix}}*"],
"Condition": {"Bool": {"aws:SecureTransport": "true"}}
},
{
"Sid": "WriteReleaseObjectsWithApprovedKms",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": ["arn:aws:s3:::{{releaseBucketName}}/{{releasePrefix}}*"],
"Condition": {
"Bool": {"aws:SecureTransport": "true"},
"StringEquals": {
"s3:x-amz-server-side-encryption": "aws:kms",
"s3:x-amz-server-side-encryption-aws-kms-key-id": "{{kmsKeyArn}}"
}
}
},
{
"Sid": "UseExactReleaseKey",
"Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
"Resource": ["{{kmsKeyArn}}"],
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "{{awsRegion}}",
"kms:EncryptionContext:aws:s3:arn": "arn:aws:s3:::{{releaseBucketName}}/{{releasePrefix}}*"
}
}
}
]
}aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://publisher-policy.json --query 'findings[].[findingType,issueCode]' --output table-------------------- | ValidatePolicy | +------------------+ | No findings | +------------------+
Checkpoint: Policy grants only matrix operations and resources
jq -e '[.Statement[].Action[]] | all(. != "*" and (endswith(":*")|not))' iam/publisher-policy.jsonContinue whenNo wildcard action, exact bucket/prefix/key resources, required transport/encryption/region context, and analyzer no errors/security warnings.
Stop whenAny action family, broader bucket/key, delete/admin action, absent encryption, or unresolved analyzer finding appears.
If this step fails
GetObject denied because PutObject-only headers are required.
Likely causeOne statement applies request-header conditions to both read and write.
Simulate and inspect actual context keys per action.
ResolutionSplit read and write into separate exact statements with applicable conditions.
Security notes
- Do not include real object names or KMS identifiers in public examples.
Alternatives
- Create a write-only publisher and separate read/verification role.
Stop conditions
- Stop until every action-condition combination is valid.
config
Attach a permissions boundary at role creation
Create a customer-managed boundary that permits no more than the approved S3 prefix and KMS key envelope, then require its ARN on the role resource. Protect boundary modification/removal through the deployment operator policy and organization controls.
Why this step matters
A boundary is a maximum, not a grant. Effective identity permission is the intersection of identity policy and boundary, subject to other denies. The boundary example names exact resources but combines actions/resources; service-inapplicable pairs do not create access. A more verbose service-separated boundary improves clarity. Its main value is preventing a later attached identity policy from escaping the approved service/resource envelope.
What to understand
Attach the boundary in the same CloudFormation role creation.
Deny deployment operators iam:DeleteRolePermissionsBoundary except controlled stack execution.
Simulate with and without candidate identity-policy expansion.
System changes
- Validates candidate boundary only.
Syntax explained
PermissionsBoundary- Role property pointing to the maximum-permissions policy ARN.
boundary allow- Defines possible maximum; still requires an identity-policy allow.
iam/publisher-boundary.jsonValues stay on this page and are never sent or saved.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "MaximumReleasePublishingEnvelope",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject",
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": [
"arn:aws:s3:::{{releaseBucketName}}",
"arn:aws:s3:::{{releaseBucketName}}/{{releasePrefix}}*",
"{{kmsKeyArn}}"
]
}]
}aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://publisher-boundary.json --query 'findings[].[findingType,issueCode]' --output table-------------------- | ValidatePolicy | +------------------+ | No findings | +------------------+
Checkpoint: Boundary blocks every out-of-envelope candidate
aws iam simulate-custom-policy --policy-input-list file://publisher-boundary.json --action-names iam:CreateUser s3:PutObject --resource-arns arn:aws:s3:::{{releaseBucketName}}/outside/test --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output tableContinue whenIAM creation and outside-prefix object write are implicitDeny.
Stop whenBoundary uses broad resources/actions or can be removed by workload/deployment role.
If this step fails
Boundary unexpectedly permits another S3 prefix.
Likely causeResource list/pattern is broader or test ARN falls under wildcard.
Expand and compare ARN path characters and simulator result.
ResolutionNarrow resource pattern and add explicit negative regression tests.
Security notes
- Boundary administration is a privilege-escalation control and needs independent review.
Alternatives
- Use a centrally mandated organization boundary template plus workload-specific narrower policy.
Stop conditions
- Stop if boundary can be bypassed by ordinary operators.
verification
Validate JSON and simulate every allow and deny tuple
Run Access Analyzer validation on trust, identity, and boundary documents. Use simulate-custom-policy with exact action/resource/context cases from the matrix. Store structured results and fail the pipeline if an expected deny becomes allowed or expected allow lacks required context.
Why this step matters
Validation detects malformed elements, unsupported actions/conditions, wildcard risk, and best-practice findings. Simulation turns the access matrix into executable policy tests. Context values must mirror the request; omitting encryption or transport should produce deny. The simulator does not replace live tests or every policy layer, but a candidate that fails its own matrix cannot proceed.
What to understand
Test wrong bucket, adjacent prefix, other KMS key, HTTP transport, absent encryption, administrative actions, and excessive session duration.
Use machine-readable assertions rather than visual tables in CI.
Review WARNING/SUGGESTION, not only ERROR.
System changes
- Queries validation/simulation APIs without creating IAM resources.
Syntax explained
simulate-custom-policy- Evaluates supplied policy documents independent of an existing principal.
context-entries- Supplies request/environment keys used by policy conditions.
Values stay on this page and are never sent or saved.
aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://publisher-policy.json > /secure-staging/validate.json && aws iam simulate-custom-policy --policy-input-list file://publisher-policy.json --action-names s3:PutObject s3:DeleteObject iam:CreateAccessKey --resource-arns arn:aws:s3:::{{releaseBucketName}}/{{releasePrefix}}synthetic.bin arn:aws:s3:::{{releaseBucketName}}/neighbor/synthetic.bin --context-entries ContextKeyName=aws:SecureTransport,ContextKeyValues=true,ContextKeyType=boolean ContextKeyName=s3:x-amz-server-side-encryption,ContextKeyValues=aws:kms,ContextKeyType=string ContextKeyName=s3:x-amz-server-side-encryption-aws-kms-key-id,ContextKeyValues={{kmsKeyArn}},ContextKeyType=string --output table------------------------------------------------- | SimulateCustomPolicy | +---------------------+--------------------------+ | s3:PutObject | allowed | | s3:PutObject neighbor| implicitDeny | | s3:DeleteObject | implicitDeny | | iam:CreateAccessKey | implicitDeny | +---------------------+--------------------------+
Checkpoint: Policy matrix is executable and passes both directions
jq -e '.findings | map(select(.findingType=="ERROR" or .findingType=="SECURITY_WARNING")) | length==0' /secure-staging/validate.jsonContinue whenNo unresolved high-severity finding; all intended allows allowed only with context; every negative tuple denied.
Stop whenAny negative case allowed, positive case works without context, or finding lacks written disposition.
If this step fails
Simulator says missing context values.
Likely causePolicy condition keys were not included or wrong types supplied.
Inspect MissingContextValues and service condition-key docs.
ResolutionAdd realistic typed context to tests; do not remove the production condition.
Security notes
- Simulation inputs can reveal exact resources and controls; protect artifacts.
Alternatives
- Use local policy-as-code tests in addition to AWS simulation.
Stop conditions
- Stop until all matrix assertions pass.
config
Preview role, managed policies, boundary, and attachments as one stack
Use CloudFormation parameters and pseudo parameters to avoid literal account IDs. Create a change set with named-IAM capability, then inspect trust, policy bodies, role path, session duration, boundary attachment, tags, and every create/update/delete/replace.
Why this step matters
Role, policies, and boundary should change atomically under one owner. NoEcho masks parameter display but is not a secret store; protect parameters and avoid placing credentials in the template. CAPABILITY_NAMED_IAM highlights stable IAM names, which can be referenced externally and conflict. The inline documents deliberately match the separately validated trust, identity, and boundary artifacts; compare digests in CI so the preview cannot silently drift from the reviewed policies.
What to understand
Protect the stack execution role from changing unrelated IAM paths.
Review replacement impact: replacing a role can break trust principal IDs and active workloads.
Apply stack policy/tag governance where appropriate.
System changes
- Creates change-set preview only.
Syntax explained
NoEcho- Masks parameter display in common views; does not make template metadata a secret vault.
MaxSessionDuration: 3600- Caps role sessions at one hour.
Path- Places role in an owned IAM namespace for inventory/governance.
iam/release-publisher-stack.ymlAWSTemplateFormatVersion: "2010-09-09"
Parameters:
TrustedPrincipalArn: {Type: String}
ExternalId: {Type: String, NoEcho: true}
ReleaseBucketName: {Type: String}
ReleasePrefix: {Type: String}
KmsKeyArn: {Type: String}
Resources:
PublisherBoundary:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Maximum release publishing envelope
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ListExactReleasePrefix
Effect: Allow
Action: s3:ListBucket
Resource: !Sub arn:${AWS::Partition}:s3:::${ReleaseBucketName}
Condition:
StringLike:
s3:prefix: [!Sub "${ReleasePrefix}*"]
- Sid: ReadWriteExactReleaseObjects
Effect: Allow
Action: [s3:GetObject, s3:PutObject]
Resource: !Sub arn:${AWS::Partition}:s3:::${ReleaseBucketName}/${ReleasePrefix}*
- Sid: UseExactReleaseKey
Effect: Allow
Action: [kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey]
Resource: !Ref KmsKeyArn
PublisherPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Exact release publishing operations
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ListExactReleasePrefix
Effect: Allow
Action: s3:ListBucket
Resource: !Sub arn:${AWS::Partition}:s3:::${ReleaseBucketName}
Condition:
StringLike:
s3:prefix: [!Sub "${ReleasePrefix}*"]
- Sid: ReadReleaseObjectsOverTls
Effect: Allow
Action: s3:GetObject
Resource: !Sub arn:${AWS::Partition}:s3:::${ReleaseBucketName}/${ReleasePrefix}*
Condition:
Bool: {aws:SecureTransport: "true"}
- Sid: WriteReleaseObjectsWithApprovedKms
Effect: Allow
Action: s3:PutObject
Resource: !Sub arn:${AWS::Partition}:s3:::${ReleaseBucketName}/${ReleasePrefix}*
Condition:
Bool: {aws:SecureTransport: "true"}
StringEquals:
s3:x-amz-server-side-encryption: aws:kms
s3:x-amz-server-side-encryption-aws-kms-key-id: !Ref KmsKeyArn
- Sid: UseExactReleaseKey
Effect: Allow
Action: [kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey]
Resource: !Ref KmsKeyArn
Condition:
StringEquals:
aws:RequestedRegion: !Ref AWS::Region
kms:EncryptionContext:aws:s3:arn: !Sub arn:${AWS::Partition}:s3:::${ReleaseBucketName}/${ReleasePrefix}*
PublisherRole:
Type: AWS::IAM::Role
Properties:
RoleName: ProductionReleasePublisher
Path: /workloads/release/
MaxSessionDuration: 3600
PermissionsBoundary: !Ref PublisherBoundary
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: TrustedPublisherOnly
Effect: Allow
Principal: {AWS: !Ref TrustedPrincipalArn}
Action: sts:AssumeRole
Condition:
StringEquals: {sts:ExternalId: !Ref ExternalId}
ManagedPolicyArns: [!Ref PublisherPolicy]
Tags:
- {Key: Owner, Value: ReleaseEngineering}
- {Key: ManagedBy, Value: CloudFormation}Values stay on this page and are never sent or saved.
aws cloudformation create-change-set --stack-name {{iamStackName}} --change-set-name reviewed-{{releaseVersion}} --change-set-type CREATE --template-body file://iam/release-publisher-stack.yml --parameters file://iam/parameters.json --capabilities CAPABILITY_NAMED_IAM --query Id --output text && aws cloudformation wait change-set-create-complete --stack-name {{iamStackName}} --change-set-name reviewed-{{releaseVersion}} && aws cloudformation describe-change-set --stack-name {{iamStackName}} --change-set-name reviewed-{{releaseVersion}} --query 'Changes[].ResourceChange.[Action,LogicalResourceId,ResourceType,Replacement]' --output tablearn:aws:cloudformation:eu-central-1:REDACTED:changeSet/reviewed/REDACTED | Add | PublisherBoundary | AWS::IAM::ManagedPolicy | False | | Add | PublisherPolicy | AWS::IAM::ManagedPolicy | False | | Add | PublisherRole | AWS::IAM::Role | False |
Checkpoint: Change set creates exactly three governed IAM resources
aws cloudformation describe-change-set --stack-name {{iamStackName}} --change-set-name reviewed-{{releaseVersion}} --output json > /secure-staging/iam-change-set.json && sha256sum /secure-staging/iam-change-set.jsonContinue whenOnly boundary, identity policy, and role/attachments appear; no replacement/delete, literal account/secret, broad trust, or boundary omission.
Stop whenUnexpected IAM resource, attachment, role replacement, broad path/name collision, or policy differs from validated artifact.
If this step fails
Change set proposes replacing the role.
Likely causeImmutable name/path/property or stack ownership changed.
Inspect replacement details, active sessions, trust dependents, and role ID references.
ResolutionPlan a parallel role migration; do not replace an active shared role in place.
Security notes
- Change set and parameters expose authorization architecture; restrict access.
Alternatives
- Use a centrally controlled IAM deployment pipeline with equivalent named-IAM preview.
Stop conditions
- Stop if template cannot faithfully embed validated policies.
command
Execute the reviewed IAM change set and inspect effective attachments
Execute only the approved change set, wait for stack completion, then retrieve role metadata, trust, boundary, attached policies, default policy versions, and tags. Do not edit the role in the console after stack creation.
Why this step matters
Stack completion means IAM accepted resources, not that effective permissions match the matrix. Reading the live role catches parameter substitution, URL-decoded trust, wrong path, missing boundary, session duration, and attachments. Managed policies have versions; record default version IDs and document digests. Console edits create drift and can bypass validated source.
What to understand
Run stack drift and compare live policy documents with validated artifacts.
Record role unique ID for dependency/recreation analysis.
Do not output External ID or deployment parameters.
System changes
- Creates the role, boundary, policy, and attachments.
Syntax explained
get-role- Returns live trust, path, duration, boundary, and role identifiers.
list-attached-role-policies- Enumerates managed policy attachments; inspect inline policies separately.
Values stay on this page and are never sent or saved.
aws cloudformation execute-change-set --stack-name {{iamStackName}} --change-set-name reviewed-{{releaseVersion}} && aws cloudformation wait stack-create-complete --stack-name {{iamStackName}} && aws iam get-role --role-name {{roleName}} --query 'Role.[Arn,Path,MaxSessionDuration,PermissionsBoundary.PermissionsBoundaryArn,AssumeRolePolicyDocument]' --output json && aws iam list-attached-role-policies --role-name {{roleName}} --output table[
"arn:aws:iam::REDACTED:role/workloads/release/ProductionReleasePublisher",
"/workloads/release/",
3600,
"arn:aws:iam::REDACTED:policy/ProductionReleasePublisherBoundary",
{"Version":"2012-10-17","Statement":[{"Principal":{"AWS":"arn:aws:iam::REDACTED:role/ApprovedCiPublisher"}}]}
]
Attached: ProductionReleasePublisherPolicyCheckpoint: Live IAM matches validated source exactly
aws iam list-role-policies --role-name {{roleName}} && aws iam list-attached-role-policies --role-name {{roleName}} && aws cloudformation detect-stack-drift --stack-name {{iamStackName}} --query StackDriftDetectionId --output textContinue whenNo unexpected inline policy, one identity attachment, required boundary, exact trust/path/duration/tags, and stack later IN_SYNC.
Stop whenBoundary missing, extra attachment/inline policy, trust changed, duration broad, or stack drift appears.
If this step fails
Role exists but boundary is missing.
Likely causeTemplate property omitted, console-created conflicting role, or stack failed partially.
Inspect stack events, role metadata, and physical resource ownership.
ResolutionDo not test/use role; correct stack and attach boundary through reviewed change.
Security notes
- Restrict iam:Get* output because it reveals trust and control design.
Alternatives
- Use an organization IAM provisioning service returning equivalent evidence.
Stop conditions
- Stop before assumption if live state diverges.
verification
Simulate the created principal with boundary and context
Run simulate-principal-policy against the live role ARN for every matrix tuple, including exact and neighboring resources, missing encryption context, administrative APIs, and other KMS keys. Record permissions-boundary decision details.
Why this step matters
Live-principal simulation includes attached identity policies and boundary details, making it stronger than candidate-only simulation. It still may not include every resource policy/SCP/key/endpoint/session layer. Missing context can turn intended allows into denies; unexpected allows are higher severity. Store structured results and assert every matrix row.
What to understand
Test secure transport false, wrong KMS key, wrong prefix, delete, policy mutation, pass-role, and another region.
Inspect OrganizationsDecisionDetail where available.
Do not use simulator output as permission to skip live negative tests.
System changes
- Calls IAM simulation only.
Syntax explained
policy-source-arn- Selects live principal policies for simulation.
PermissionsBoundaryDecisionDetail- Shows whether the boundary permits the evaluated action/resource.
Values stay on this page and are never sent or saved.
ROLE_ARN=$(aws iam get-role --role-name {{roleName}} --query Role.Arn --output text) && aws iam simulate-principal-policy --policy-source-arn "$ROLE_ARN" --action-names s3:PutObject s3:DeleteObject iam:AttachRolePolicy kms:Encrypt --resource-arns arn:aws:s3:::{{releaseBucketName}}/{{releasePrefix}}synthetic.bin arn:aws:s3:::{{releaseBucketName}}/neighbor/synthetic.bin {{kmsKeyArn}} --context-entries ContextKeyName=aws:SecureTransport,ContextKeyValues=true,ContextKeyType=boolean ContextKeyName=aws:RequestedRegion,ContextKeyValues={{awsRegion}},ContextKeyType=string --output json > /secure-staging/effective-simulation.json && jq -r '.EvaluationResults[] | [.EvalActionName,.EvalResourceName,.EvalDecision,.PermissionsBoundaryDecisionDetail.AllowedByPermissionsBoundary] | @tsv' /secure-staging/effective-simulation.jsons3:PutObject arn:aws:s3:::.../releases/production/synthetic.bin allowed true s3:PutObject arn:aws:s3:::.../neighbor/synthetic.bin implicitDeny false s3:DeleteObject arn:aws:s3:::.../releases/production/synthetic.bin implicitDeny false iam:AttachRolePolicy * implicitDeny false kms:Encrypt arn:aws:kms:eu-central-1:...:key/... allowed true
Checkpoint: Effective role passes the complete policy matrix
jq -e '[.EvaluationResults[] | select((.EvalActionName|test("Delete|Attach|Create")) and .EvalDecision=="allowed")] | length==0' /secure-staging/effective-simulation.jsonContinue whenOnly explicit required tuples allowed; neighboring/admin/missing-context cases denied; boundary blocks all out-of-envelope cases.
Stop whenAny unexpected allow, boundary bypass, missing context, or unexplained difference from custom simulation appears.
If this step fails
Live simulation differs from candidate simulation.
Likely causeBoundary, attachment, inline policy, SCP, or live policy version differs.
Compare attachments/default versions and decision details.
ResolutionResolve live policy source/drift before real credentials are issued.
Security notes
- Simulation artifact contains sensitive authorization map.
Alternatives
- Use automated policy regression tests on every stack change.
Stop conditions
- Stop until both candidate/live simulations agree with matrix.
verification
Assume the role and run synthetic positive and negative calls
From the exact trusted caller, assume the role for a short named session with the required External ID. Inject temporary credentials only into an isolated subprocess, upload one non-sensitive synthetic object with the approved KMS key, verify it, then test neighboring prefix, delete, IAM, and wrong-key denials. Remove the test object through a separate approved cleanup identity if the role intentionally lacks delete.
Why this step matters
Real calls evaluate STS trust, caller permission, role identity policy, boundary, SCP, bucket policy, key policy, endpoint policy, and service behavior together. The session is short and stored only in a mode-0600 ephemeral file. Credentials must not appear in shell tracing, process arguments, output, or test reports. Synthetic content prevents production overwrite. A cleanup identity is separate because adding DeleteObject solely for tests would violate the role's business need.
What to understand
Disable shell xtrace/history and ensure the test harness never prints credentials.
Use unique synthetic key and encryption headers.
Test missing/wrong External ID and excessive duration from controlled callers.
System changes
- Issues a 15-minute role session and writes/reads one synthetic encrypted object.
- Negative API calls generate denied CloudTrail events where logged.
Syntax explained
--duration-seconds 900- Limits verification credential lifetime to 15 minutes.
isolated subprocess- Prevents credentials from contaminating the operator shell/environment.
aws sts assume-role --role-arn "$ROLE_ARN" --role-session-name policy-verification-redacted --external-id "$EXTERNAL_ID_FROM_SECRET_SOURCE" --duration-seconds 900 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken,Expiration]' --output json > /run/user/1000/role-session.json && chmod 600 /run/user/1000/role-session.json && ./tests/run-role-policy-matrix.sh /run/user/1000/role-session.json && rm -f /run/user/1000/role-session.jsonPASS allow s3:ListBucket releases/production/ PASS allow s3:PutObject releases/production/policy-test.bin (SSE-KMS exact key) PASS allow s3:GetObject releases/production/policy-test.bin PASS deny s3:PutObject releases/staging/policy-test.bin PASS deny s3:DeleteObject releases/production/policy-test.bin PASS deny iam:ListUsers PASS deny kms:Encrypt other-key Temporary session file removed
Checkpoint: Runtime permissions equal matrix and credentials are gone
test ! -e /run/user/1000/role-session.json && aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole --max-results 5 --output tableContinue whenEvery allow/deny passes, synthetic object cleaned by owner, session file removed, and assumption attributed in CloudTrail.
Stop whenCredential leaks, unapproved principal assumes, negative call succeeds, positive call needs broader policy, or audit event absent.
If this step fails
Negative API unexpectedly succeeds.
Likely causeResource policy, SCP exception, another session policy, or role policy grants access.
Stop testing, preserve CloudTrail request, inspect every effective layer.
ResolutionTreat as security incident and remove the unintended grant before role use.
Security notes
- STS credentials are secrets; rotate/revoke incident path if exposed.
Alternatives
- Run the matrix in a protected CI job with ephemeral OIDC federation.
Stop conditions
- Stop immediately on any unexpected allow or credential exposure.
verification
Verify CloudTrail attribution and Access Analyzer findings
Confirm the organization trail captures role assumption and required S3/KMS data events, inspect source identity/session name, and query Access Analyzer for external-access and unused-access findings associated with the role and resources.
Why this step matters
CloudTrail answers who requested credentials and what APIs were called, but lookup event history is limited and S3 object/KMS data visibility depends on trail selectors. Access Analyzer external findings identify resource-based access outside the zone of trust; unused-access analyzers can help reduce permissions after enough observation. No finding does not prove safe policy—the trust zone and analyzer coverage matter.
What to understand
Verify trail encryption, log validation, region coverage, destination, retention, and delivery alarms.
Alert on assumption by unexpected principal/source, denied bursts, policy/boundary changes, and session duration anomalies.
Archive findings only with documented reason and owner.
System changes
- Queries audit and analyzer services only.
Syntax explained
list-findings-v2- Queries current Access Analyzer findings with filters.
data events- Trail selectors required for object-level S3 operations beyond management history.
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole --max-results 20 --query 'Events[].{Time:EventTime,User:Username,Event:EventName}' --output table && aws accessanalyzer list-analyzers --type ACCOUNT --query 'analyzers[?status==`ACTIVE`].[name,arn]' --output table && aws accessanalyzer list-findings-v2 --analyzer-arn "$ANALYZER_ARN" --filter '{"resource":{"eq":["'$ROLE_ARN'"]}}' --output table------------------------------------------------- | LookupEvents | +---------------------------+-------------------+ | 2026-07-28T11:42:18+00:00 | policy-verification-redacted | AssumeRole | Active analyzer: production-account-access No ACTIVE external-access findings for role
Checkpoint: Assumption and resource use are attributable and monitored
aws accessanalyzer list-findings-v2 --analyzer-arn "$ANALYZER_ARN" --filter '{"status":{"eq":["ACTIVE"]}}' --query 'findings[?resource==`'$ROLE_ARN'`]' --output jsonContinue whenExpected assumption and test activity attributed, required data events retained, no unexplained active finding, and alerts route to an owner.
Stop whenTrail gap, unrecognized principal/session, missing data events, inactive analyzer, or external finding remains.
If this step fails
AssumeRole event has ambiguous session name.
Likely causeCaller does not set controlled role-session-name/source identity.
Inspect userIdentity sessionIssuer, principalId, and request parameters in retained event.
ResolutionRequire a structured non-secret session name/source identity in the caller workflow and alert on deviations.
Security notes
- CloudTrail logs are security records containing principal/resource metadata.
Alternatives
- Stream CloudTrail to a central SIEM with identity enrichment.
Stop conditions
- Stop production enablement without attributable audit.
verification
Review service last accessed and remove unused permissions
After a representative operational period, generate service-last-accessed details, compare CloudTrail and workload requirements, and remove permissions that are unused or no longer required through a new policy version and change set. Lack of observed access is evidence, not automatic proof of safety.
Why this step matters
Last-accessed data helps find unused services/actions but has reporting lag, tracking-period limits, service coverage gaps, and cannot know future recovery paths. Compare it with the access matrix, application code, release schedules, and CloudTrail. Removal follows the same validation, simulation, change-set, live negative tests, and rollback gates as addition.
What to understand
Observe long enough to include rare but valid release/recovery operations.
Separate read/write/admin usage and identify permissions granted but never expected.
Keep policy versions within IAM limits and retention.
System changes
- Starts and reads an IAM access report only.
Syntax explained
ACTION_LEVEL- Requests per-action usage where service supports it.
with-entities- Correlates service action access with identities.
JOB_ID=$(aws iam generate-service-last-accessed-details --arn "$ROLE_ARN" --granularity ACTION_LEVEL --query JobId --output text) && aws iam get-service-last-accessed-details-with-entities --job-id "$JOB_ID" --service-namespace s3 --max-items 100 --output tableActionName LastAccessedEntity LastAccessedTime ListBucket ProductionReleasePublisher 2026-07-28T11:43:02Z PutObject ProductionReleasePublisher 2026-07-28T11:43:05Z GetObject ProductionReleasePublisher 2026-07-28T11:43:09Z DeleteObject - -
Checkpoint: Permissions remain minimal over observed lifecycle
aws iam list-policy-versions --policy-arn "$PUBLISHER_POLICY_ARN" --output tableContinue whenEvery retained action remains tied to matrix need or documented rare recovery; obsolete permissions scheduled for tested removal.
Stop whenObservation window incomplete, data unsupported, recovery use unmodeled, or removal would break approved function.
If this step fails
Last-accessed report remains IN_PROGRESS.
Likely causeAsynchronous job has not completed or account data is large.
Poll job status with bounded interval.
ResolutionWait; do not infer no use from an incomplete report.
Security notes
- Usage reports reveal operational schedules and resource relationships.
Alternatives
- Use Access Analyzer unused-access findings with owner review.
Stop conditions
- Stop automated permission removal without human/context review.
warning
Rotate trusted caller/context and rehearse session revocation
Rotate the trusted caller role or External ID through a dual-control plan, minimize overlap, test the new path, then remove the old path. For compromise, block new assumptions and revoke older sessions with a time-based explicit deny under incident control while preserving CloudTrail evidence.
Why this step matters
Updating trust blocks future assumptions but existing STS credentials remain valid until expiry unless an explicit revocation deny is added. AWS console 'revoke sessions' creates a deny based on token issue time. That deny can interrupt legitimate workloads and must be applied with exact timestamp and incident approval. Normal rotation can overlap old/new caller briefly, but overlap expands trust and needs monitoring.
What to understand
Inventory active session duration and workload schedules before rotation.
Test new principal with complete matrix and audit before removing old.
After compromise, preserve events and rotate any resource credentials/data affected.
System changes
- Shown commands are read-only; later trust/revocation changes affect session authorization.
Syntax explained
RoleLastUsed- Coarse service/region usage hint, not complete active-session inventory.
TokenIssueTime deny- Emergency policy condition denying sessions issued before a cutoff.
Values stay on this page and are never sent or saved.
aws iam get-role --role-name {{roleName}} --query 'Role.[Arn,MaxSessionDuration,RoleLastUsed]' --output json && aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue={{roleName}} --max-results 50 --output table[
"arn:aws:iam::REDACTED:role/workloads/release/ProductionReleasePublisher",
3600,
{"LastUsedDate":"2026-07-28T11:43:09Z","Region":"eu-central-1"}
]
Recent events reviewed: AssumeRole, UpdateAssumeRolePolicy (none unexpected)Checkpoint: New path works and old sessions/principal are retired deliberately
aws iam get-role --role-name {{roleName}} --query 'Role.AssumeRolePolicyDocument' --output jsonContinue whenOnly current exact principal/context remains, old sessions expired/revoked as planned, negative old-path test denies, CloudTrail/alerts clean.
Stop whenActive workload ownership unknown, overlap unmonitored, cutoff uncertain, or revocation would create unsafe outage.
If this step fails
Old session continues after trust policy removal.
Likely causeTrust affects new AssumeRole, not credentials already issued.
Check token issue/expiration and CloudTrail session principal.
ResolutionWait for bounded expiry or apply reviewed token-issue-time revoke deny under incident process.
Security notes
- Revocation policy details and incident timestamps are sensitive.
Alternatives
- Use shorter default sessions and workload retry/failover to reduce revocation impact.
Stop conditions
- Stop normal rotation and invoke incident response on suspected compromise.
warning
Roll back policy versions or retire the role through its stack
For a bad policy update, restore the reviewed managed-policy version through a CloudFormation change set, retest trust/effective/live cases, and keep the boundary attached. To retire, block new assumptions, wait/revoke sessions, detach workload dependencies, retain audit, and delete through the stack in reverse dependency order.
Why this step matters
Managed-policy versions allow fast default-version restoration, but CloudFormation must remain the source of truth or the next deployment will undo the emergency change. A prior policy may still be unsafe with current resources/workflow, so revalidate and simulate it. Role retirement requires dependency and session handling because temporary credentials survive detachment/trust changes until expiry and resource policies may reference the role.
What to understand
Preserve bad policy, findings, CloudTrail, and affected requests before rollback.
Never remove the boundary as a rollback technique.
Delete non-default versions only after retention and recovery review.
System changes
- Reads/validates prior policy; a later approved change may switch policy and eventually delete IAM resources.
Syntax explained
policy version- Immutable document version under a managed policy; one version is default.
stack retirement- Removes attachments, role, and policies under the declared owner and event history.
Values stay on this page and are never sent or saved.
aws iam list-policy-versions --policy-arn "$PUBLISHER_POLICY_ARN" --output table && aws iam get-policy-version --policy-arn "$PUBLISHER_POLICY_ARN" --version-id {{policyVersionToRestore}} --query 'PolicyVersion.Document' --output json > /secure-staging/policy-to-restore.json && aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file:///secure-staging/policy-to-restore.jsonVERSION DEFAULT CREATED v4 True 2026-07-28T14:00:00Z v3 False 2026-07-21T09:00:00Z Policy validation: no ERROR or SECURITY_WARNING
Checkpoint: Restored policy passes current matrix and role has no orphan authority
aws iam list-entities-for-policy --policy-arn "$PUBLISHER_POLICY_ARN" --output table && aws iam list-instance-profiles-for-role --role-name {{roleName}} --output tableContinue whenRestored policy validated/simulated/live-tested, stack in sync, or retirement dependencies empty and sessions expired/revoked.
Stop whenPrior version fails current controls, boundary removed, active dependency/session remains, or resource policy still trusts deleted role.
If this step fails
CloudFormation restores the newer bad policy after manual default switch.
Likely causeTemplate/source still declares newer document.
Compare stack template, Git revision, policy default, and drift.
ResolutionCommit reviewed rollback/forward fix and deploy through a change set immediately.
Security notes
- Policy rollback can reintroduce removed permissions; run full current test matrix.
Alternatives
- Create a new parallel role and migrate caller/resources before retiring the old role.
Stop conditions
- Stop deletion while any active session or resource-policy dependency is unresolved.
Finish line
Verification checklist
aws iam get-role --role-name {{roleName}} --query 'Role.[AssumeRolePolicyDocument,MaxSessionDuration,PermissionsBoundary]' --output jsonOne exact trusted role/context, one-hour maximum, and required boundary.aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://publisher-policy.jsonNo unresolved ERROR/SECURITY_WARNING and no wildcard action/principal or broad resource.aws iam simulate-principal-policy --policy-source-arn "$ROLE_ARN" --action-names s3:PutObject s3:DeleteObject iam:AttachRolePolicy --resource-arns arn:aws:s3:::{{releaseBucketName}}/{{releasePrefix}}synthetic.binOnly required exact tuple allows; delete/admin/neighbor/wrong-context cases deny and boundary details match../tests/run-role-policy-matrix.sh /run/user/1000/role-session.jsonSynthetic allowed calls succeed, every negative call denies, credentials/logs remain protected, and test object is cleaned by its owner.aws cloudformation describe-stacks --stack-name {{iamStackName}} --query 'Stacks[0].StackStatus' --output text && aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole --max-results 5Stack in sync, assumptions attributed, required data events retained, analyzer/alerts active, and no unexplained finding.Recovery guidance
Common problems and safe checks
AssumeRole is denied for the approved caller.
Likely causeCaller identity policy, trust principal, External ID, source identity, session duration, SCP, or boundary condition differs.
Inspect caller ARN, trust policy, encoded denial, and simulate sts:AssumeRole where supported.Compare External ID through approved metadata handling without logging it.
ResolutionCorrect the exact mismatched principal/context or caller permission; do not broaden trust to account root or wildcard.
An unapproved principal can assume the role.
Likely causeTrust uses broad account/root/wildcard principal, a permissive condition, or shared caller role.
Inspect role trust and CloudTrail AssumeRole principal ARNs.
ResolutionTreat as incident, tighten trust to exact principal, revoke older sessions, and review activity.
S3 ListBucket is denied but object writes work.
Likely causeBucket-level ListBucket requires bucket ARN and matching s3:prefix condition separate from object ARN.
Simulate s3:ListBucket on exact bucket with s3:prefix context.
ResolutionAdd only the required bucket action/resource/condition; do not grant s3:*.
Object PutObject is denied under the approved prefix.
Likely causeObject ARN slash/prefix mismatch, required encryption headers absent, bucket policy, endpoint policy, or KMS authorization denies.
Compare exact object ARN and request encryption headers.Inspect bucket/key/endpoint policy and CloudTrail denial.
ResolutionCorrect the exact prefix/context intersection rather than widening bucket or KMS resources.
KMS Encrypt succeeds but Decrypt fails.
Likely causeIAM/key policy grants differ, encryption context does not match, key state/region differs, or grant is absent.
Inspect key ARN/state/policy and encryption context without decrypting production data.
ResolutionAdd only the required key operation/context for the approved workflow or redesign one-way publishing.
Access Analyzer reports wildcard or pass-role finding.
Likely causePolicy is broader than resource capabilities require or contains privilege-escalation path.
Read finding location and AWS service authorization resource types.
ResolutionEnumerate actions/resources/conditions and remove pass-role/IAM mutation unless separately designed.
Simulator allows a call that real AWS denies.
Likely causeSCP, resource policy, boundary, session policy, endpoint policy, key policy, or unsupported condition is absent from simulation.
Inspect all effective policy layers and CloudTrail authorization context.
ResolutionPreserve the deny and correct the intended restrictive layer only; do not broaden identity policy blindly.
Simulator denies a call that real AWS allows.
Likely causeA resource-based policy or session context grants access not modeled in the selected simulation.
Inspect bucket/key/resource policies and actual principal/session ARN.
ResolutionRemove unintended resource-based grant or update the test model; treat unexpected allow as security finding.
Permissions boundary blocks a newly attached policy.
Likely causeThe requested permission is outside the approved maximum or boundary condition/resource is narrower.
Simulate identity policy intersected with the boundary and review requested business need.
ResolutionKeep it blocked unless a separate boundary-version change is approved and tested; boundaries are not debugging obstacles.
CloudTrail shows AssumeRole but not S3 object calls.
Likely causeTrail data events are not enabled for the bucket/prefix or delivery is delayed.
Inspect organization trail event selectors, region, destination, and delivery health.
ResolutionEnable scoped S3 data events and alarms; do not claim complete usage evidence from management history alone.
Deleting an old policy version fails.
Likely causeVersion is default or still referenced by recovery/change process.
List policy versions/default and attachment/dependency inventory.
ResolutionSet the reviewed replacement default first, verify behavior, then delete only a non-default version outside retention.
Role deletion fails due to dependencies.
Likely causePolicies, instance profiles, or service integrations remain attached.
List attached/inline policies, instance profiles, and CloudFormation resources.
ResolutionRemove dependencies through the owning stack in reverse order after active-session and service checks.
Recovery
Rollback
Restore a previously reviewed managed-policy version only after validating it against current resources and the complete matrix, and update the CloudFormation source immediately so ownership converges. Never remove the permissions boundary to restore service. For role retirement, block new assumptions, inventory/wait/revoke active sessions, remove caller/resource-policy dependencies, preserve audit evidence, and delete attachments, role, and policies through the owning stack.
- Freeze IAM changes and capture stack/change-set IDs, role unique ID/trust/boundary, attached/inline policies, default/non-default versions, resource policies, active use, findings, and CloudTrail.
- Identify whether failure is trust, identity, boundary, SCP, bucket/key/endpoint/resource policy, session context, or service behavior.
- Retrieve the prior policy version and validate/simulate it against the current complete matrix.
- Create a CloudFormation change set restoring the exact prior document; reject boundary removal, trust broadening, role replacement, or new attachment.
- Execute and run candidate, live-principal, and temporary-session positive/negative tests.
- Confirm CloudTrail, Access Analyzer, workload health, and stack drift/in-sync state.
- If compromise is suspected, block new assumptions and apply reviewed older-session revocation while preserving evidence.
- For retirement, wait for or revoke sessions, remove caller and resource-policy references, ensure no instance profile/service dependency, then delete through the stack.
- Retain policy/audit evidence and delete obsolete non-default versions only after recovery retention expires.
Evidence