IAM Permission Boundaries

Lock down AWS access with least-privilege policies and permission boundaries.

Introduction

30 Second Summary

Every company has that one employee who accidentally deleted a production database because they had permissions they never should have had. In cloud security, the principle of least privilege stops this from happening.

In this project, you will build a complete IAM access control setup on AWS. You'll create a developer role with hand-crafted policies, constrain it with a permission boundary, and then try to break out of those constraints to prove they work.

What You'll Build

You'll assume a locked-down developer role from the command line, attempt actions that should be blocked, and watch AWS deny them in real time.

By the end of this project, you'll have:

  • A developer role you can assume from the CLI using temporary credentials from STS that expire automatically.
  • A least-privilege policy written from scratch with specific actions on specific resources and zero wildcards anywhere.
  • A permission boundary that visibly blocks actions the identity policy would otherwise allow, proving the intersection model in real time.
  • Validation results from IAM Access Analyzer that catch an overly permissive policy before it reaches production.
  • Secret Mission: implement the delegated admin pattern where the developer role can create downstream roles only if it attaches your boundary.

Are there any prerequisites?

You need an AWS account with admin access and the AWS CLI installed and configured. If you can run aws sts get-caller-identity and see your account ID, you are ready to start.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters. This step is your moment to commit to the goal before any hands-on work begins.

Set Up Your S3 Test Resource

IAM policies define what actions are allowed on which resources. Without a real resource to test against, those policies are just theory.

In this step, you'll create an S3 bucket that acts as a "company resource" your developer role will access later. This gives you something concrete to test permissions against.

In this step, get ready to:
  • Verify your AWS CLI identity and note your account ID.
  • Create an S3 bucket as your test resource.
  • Upload a test file to the bucket.
Verify your AWS CLI identity

Every ARN you write in this project includes your AWS account ID. You need to confirm your CLI is authenticated and capture that ID now.

  • Confirm your identity and account ID by running this command:
aws sts get-caller-identity

What does this command do?

This calls AWS STS (Security Token Service) to return the identity currently configured in your CLI. The output shows three fields: UserId, Account, and Arn.

You should see JSON output that includes your 12-digit account ID in the Account field.

  • Copy the 12-digit number from the Account field and save it somewhere you can easily access (a sticky note, a text file, or your clipboard). You will use this in every ARN you write throughout this project.

Not seeing JSON output?

If you see a credential error, your AWS CLI is not configured. Run aws configure and enter your access key ID, secret access key, and default region.

If you see a permissions error, your credentials may be invalid. The sts:GetCallerIdentity action is allowed by default for all authenticated principals, so a permissions error usually means the credentials themselves are wrong.

Help me troubleshoot my AWS CLI credentials.

Create the S3 bucket and upload a test file

S3 bucket names must be globally unique across all of AWS. You'll include your account ID in the name to guarantee uniqueness.

  • Create your test bucket by running this command:
aws s3 mb s3://iam-boundary-test-[[ACCOUNT_ID="your 12-digit account ID"]]

What does this command do?

The aws s3 mb command creates ("makes") a new S3 bucket. The bucket is created in the default region configured in your CLI.

The naming convention iam-boundary-test-<account-id> makes the bucket easy to identify and guarantees global uniqueness.

You should see output like make_bucket: iam-boundary-test-123456789012 confirming the bucket was created.

  • Create a local test file and upload it to the bucket by running these commands:
echo "This is a test file for IAM boundary testing" > test-object.txt
aws s3 cp test-object.txt s3://iam-boundary-test-[[ACCOUNT_ID="your 12-digit account ID"]]/test-object.txt

What does this command do?

The first line creates a simple text file locally. The second line uses aws s3 cp to upload that file into your bucket as an object named test-object.txt.

This object is the "company data" your developer role will try to read, write, and eventually delete in later steps.

You should see output like upload: ./test-object.txt to s3://iam-boundary-test-123456789012/test-object.txt confirming the upload succeeded.

Seeing a BucketAlreadyExists error?

S3 bucket names are globally unique. If someone else already has a bucket with your chosen name, add a random suffix (e.g. iam-boundary-test-123456789012-lab). Use this modified name for all commands in the rest of the project.

Help me fix my S3 bucket creation error.

Verify your bucket is ready

Before you run this, what do you expect to see when you list all your S3 buckets?

  • List your S3 buckets by running:
aws s3 ls

Your new iam-boundary-test- bucket appears in the list with today's date. This is the resource you'll write IAM policies against in the next steps.

Don't see your bucket in the list?

Double-check that the aws s3 mb command earlier returned a make_bucket confirmation. If it did not, re-run it. If you see a different region's buckets, ensure your default region matches where you created the bucket.

Help me find my S3 bucket.

Your test resource is ready. Next up, you'll create an IAM role with a trust policy and assume it using temporary credentials.

Create a Role with a Trust Policy and Assume It

In the previous step you created the S3 bucket that represents your "company resource." Now you need an identity that can access it with restricted permissions.

Instead of using your admin credentials directly, you will create an IAM role and assume it using temporary credentials from STS. The trust policy on the role controls WHO can assume it. The identity policy controls WHAT the role can do once assumed.

In this step, get ready to:
  • Write a trust policy and create an IAM role that your user can assume.
  • Write and attach a least-privilege identity policy with zero wildcard actions or resources.
  • Assume the role with STS and operate using temporary credentials.
Write the trust policy and create the role

A trust policy answers one question: "Who is allowed to assume this role?" You will write a JSON document that grants your IAM user permission to call sts:AssumeRole on this role.

  • Create a file called trust-policy.json with the following content:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::[[ACCOUNT_ID="your 12-digit AWS account ID"]]:user/[[IAM_USER="your IAM username"]]"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

What does this policy do?

The Principal field names the specific IAM user ARN allowed to assume this role. The Action field restricts the allowed action to sts:AssumeRole only.

You can find your IAM username in the output from aws sts get-caller-identity that you ran in the previous step. The ARN at the end shows your user path.

  • Create the IAM role named dev-role by running this command:
aws iam create-role --role-name dev-role --assume-role-policy-document file://trust-policy.json

You should see JSON output containing the new role's ARN, which looks like arn:aws:iam::<account-id>:role/dev-role.

Seeing MalformedPolicyDocument?

Double-check that the Principal ARN matches your actual IAM user ARN exactly. A typo in the account ID or username causes this error.

Run aws sts get-caller-identity to confirm your ARN, then update trust-policy.json to match.

help me fix my trust policy JSON for the dev-role

Write a least-privilege identity policy and attach it

The trust policy controls who can wear the hat. Now you need to control what the hat-wearer can do. You will write an identity policy granting only s3:GetObject and s3:PutObject on your specific bucket.

  • Create a file called dev-policy.json with the following content:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::iam-boundary-test-[[ACCOUNT_ID="your 12-digit AWS account ID"]]",
        "arn:aws:s3:::iam-boundary-test-[[ACCOUNT_ID="your 12-digit AWS account ID"]]/*"
      ]
    }
  ]
}

What makes this least-privilege?

There are zero wildcards in this policy. The Action field lists only the two specific S3 actions needed. The Resource field names the exact bucket ARN and its objects path.

Two resource entries are required: one for the bucket itself and one for its objects (the /* suffix). Without both, some operations would be denied.

  • Attach this policy to the role as an inline policy named dev-policy by running this command:
aws iam put-role-policy --role-name dev-role --policy-name dev-policy --policy-document file://dev-policy.json
  • This command produces no output on success. Confirm the policy is attached by running:
aws iam list-role-policies --role-name dev-role

You should see dev-policy listed in the output under PolicyNames.

Seeing NoSuchEntity?

This means the role dev-role does not exist yet. Make sure the aws iam create-role command from the previous substep succeeded before running put-role-policy.

help me attach my identity policy to the dev-role

Assume the role and verify temporary credentials

Now you will become the developer. By calling aws sts assume-role, you get back temporary credentials that expire automatically. No hardcoded keys needed.

  • Assume the role by running this command:
aws sts assume-role --role-arn arn:aws:iam::[[ACCOUNT_ID="your 12-digit AWS account ID"]]:role/dev-role --role-session-name dev-session

What does this command return?

The output is a JSON object with a Credentials block containing three values: AccessKeyId, SecretAccessKey, and SessionToken. These are your temporary credentials. They expire after one hour by default.

  • Export the three credential values as environment variables. Copy the values from the JSON output and paste them into these commands:
export AWS_ACCESS_KEY_ID=your-access-key-id-here
export AWS_SECRET_ACCESS_KEY=your-secret-access-key-here
export AWS_SESSION_TOKEN=your-session-token-here

Why environment variables?

Exporting AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN overrides your default CLI credentials for this terminal session. Every command you run now uses the role's temporary credentials instead of your admin keys.

Before you verify, what do you think aws sts get-caller-identity will show now? Will it still display your original user ARN, or something different?

  • Verify you are now operating as the role by running:
aws sts get-caller-identity

The output shows an ARN containing assumed-role/dev-role/dev-session instead of your original user ARN. You are now the developer.

  • Test that you can read from the bucket by running:
aws s3 cp s3://iam-boundary-test-[[ACCOUNT_ID="your 12-digit AWS account ID"]]/test-object.txt ./test-download.txt
  • Test that you can write to the bucket by running:
echo "written by dev-role" > dev-upload.txt
aws s3 cp dev-upload.txt s3://iam-boundary-test-[[ACCOUNT_ID="your 12-digit AWS account ID"]]/dev-upload.txt

Both commands should complete successfully with no errors. Your role can read and write objects as intended.

Seeing Access Denied on S3 operations?

IAM changes can take a few seconds to propagate. Wait 10 seconds and try again.

If it still fails, confirm your dev-policy.json uses the correct bucket name including your account ID. The resource ARN must match the bucket name exactly.

help me debug why my dev-role cannot access my S3 bucket

Your role is working and scoped to just two S3 actions. Next up, you will see what happens when someone expands those permissions without a guardrail in place.

Demonstrate the Missing Guardrail

Your identity policy grants only s3:GetObject and s3:PutObject. It looks tight. But what happens when someone edits the policy later and adds s3:DeleteObject or even s3:*?

Right now, nothing stops that escalation. In this step, you'll simulate a developer who expands their own permissions by adding delete access. You'll see the expanded policy take effect immediately with no guardrail blocking it.

In this step, get ready to:
  • Expand the dev-role's identity policy to include s3:DeleteObject.
  • Assume the role and successfully delete an object from the bucket.
  • Re-upload the test file to restore the bucket for the next step.
Expand the identity policy

Imagine a scenario where a developer decides they need delete access to clean up old files. They update their own role's policy. Without any cap on what the role can do, the expanded permissions take effect immediately.

  • Open dev-policy.json in your editor.
  • Find the Action array that currently contains s3:GetObject and s3:PutObject:
"Action": [
  "s3:GetObject",
  "s3:PutObject"
]
  • Add s3:DeleteObject to the array, so it now looks like this:
"Action": [
  "s3:GetObject",
  "s3:PutObject",
  "s3:DeleteObject"
]
  • Save dev-policy.json.
  • Apply the updated policy to the role by running this command:
aws iam put-role-policy --role-name dev-role --policy-name dev-policy --policy-document file://dev-policy.json

What does this command do?

This replaces the existing inline policy on the role with your updated version. The --policy-document file:// prefix tells the CLI to read the JSON from a local file rather than inline.

Seeing an access denied error?

Make sure you are running this command with your admin credentials, not the assumed role's temporary credentials. If you still have temporary credential environment variables set from the previous step, unset them first:

unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN

Then re-run the put-role-policy command.

help me fix an access denied error when updating my IAM role policy

Assume the role and attempt the delete

The policy is updated. Now you need fresh temporary credentials that reflect the expanded permissions.

  • Assume the dev-role by running this command:
aws sts assume-role --role-arn arn:aws:iam::[[ACCOUNT_ID="your-account-id"]]:role/dev-role --role-session-name dev-session
  • Copy the AccessKeyId, SecretAccessKey, and SessionToken values from the output.
  • Export them as environment variables by running these commands (replacing each placeholder with the actual value from the output):
export AWS_ACCESS_KEY_ID=your-access-key-id-from-output
export AWS_SECRET_ACCESS_KEY=your-secret-access-key-from-output
export AWS_SESSION_TOKEN=your-session-token-from-output
  • Verify you are now operating as the dev-role by running:
aws sts get-caller-identity

The output should show an ARN containing assumed-role/dev-role/dev-session, confirming you are operating as the role.

Now for the real test. You just added s3:DeleteObject to this role's identity policy. Before you run the next command, ask yourself: do you think the delete will succeed or be blocked?

  • Attempt to delete the test object from your bucket by running:
aws s3 rm s3://iam-boundary-test-[[ACCOUNT_ID="your-account-id"]]/test-object.txt

The command succeeds. The object is gone. That is the problem.

Why is this a problem?

Anyone who can edit the role's identity policy can grant themselves any action they want. There is no ceiling on what this role can do. If a developer (or an attacker) expands the policy, it takes effect immediately.

This is privilege escalation with no guardrail in place. In the next step, you'll attach a permission boundary that caps the maximum permissions the role can ever have, regardless of what the identity policy says.

Restore the test object

You still need that test object in the bucket for the next step. The dev-role still has s3:PutObject permission, so you can re-upload it now.

  • Re-upload the test file by running:
aws s3 cp test-object.txt s3://iam-boundary-test-[[ACCOUNT_ID="your-account-id"]]/test-object.txt
  • Confirm the object is back by running:
aws s3 ls s3://iam-boundary-test-[[ACCOUNT_ID="your-account-id"]]/

You should see test-object.txt listed in the output.

  • Unset the temporary credentials to return to your admin session by running:
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN

Upload failing with access denied?

If the re-upload fails, your temporary credentials may have expired. Unset the environment variables, then re-assume the role to get fresh credentials. Session tokens from aws sts assume-role expire after one hour by default.

help me troubleshoot an access denied error when uploading to S3 with assumed role credentials

✔️ Awesome, I've got everything!

Great. Double check you've saved dev-policy.json and that your terminal is back in the admin session (no temporary credential environment variables set).

ⓧ I'd like to double check the full policy

Here is what dev-policy.json should look like after this step:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::iam-boundary-test-<account-id>",
        "arn:aws:s3:::iam-boundary-test-<account-id>/*"
      ]
    }
  ]
}

You just proved that identity policies alone have no ceiling. Anyone who can edit the policy can grant themselves any action. In the next step, you'll attach a permission boundary that caps what this role can ever do, and watch it block the delete even though the identity policy still allows it.

Attach a Permission Boundary and Prove the Intersection

In the previous step, you expanded the identity policy and watched a delete succeed with no guardrail in place. That proved the problem: anyone who can edit the identity policy can escalate their own access.

A permission boundary caps the maximum permissions the role can ever have, regardless of what identity policies say. In this step, you will attach a boundary that deliberately excludes s3:DeleteObject and watch the delete get denied even though the identity policy still allows it.

In this step, get ready to:
  • Write a permission boundary policy that only allows read and write operations.
  • Attach the boundary to your role and re-assume it with fresh credentials.
  • Prove the intersection model by attempting a delete and seeing it denied.
Write the permission boundary policy

The boundary policy defines the ceiling of what the role is allowed to do. You will deliberately leave out s3:DeleteObject so that even if the identity policy grants it, the intersection will block it.

  • Create a file called boundary-policy.json by running this command:
cat <<EOF > boundary-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::iam-boundary-test-[[AWS_ACCOUNT_ID="your-account-id"]]",
        "arn:aws:s3:::iam-boundary-test-[[AWS_ACCOUNT_ID="your-account-id"]]/*"
      ]
    }
  ]
}
EOF

What does this policy allow?

  • s3:GetObject lets the role read objects from the bucket.
  • s3:PutObject lets the role write objects to the bucket.
  • s3:DeleteObject is intentionally missing. Even though the identity policy grants it, the boundary will block it.
  • Confirm the file was created by running:
cat boundary-policy.json

You should see the JSON policy document with only s3:GetObject and s3:PutObject in the Action array.

  • Create the managed policy in IAM by running this command:
aws iam create-policy \
    --policy-name dev-boundary-policy \
    --policy-document file://boundary-policy.json

The output includes a Policy.Arn field. Copy this ARN. You will use it in the next command.

Why a managed policy instead of inline?

Permission boundaries must be managed policies. You cannot use an inline policy as a boundary. Managed policies live independently and can be reused across multiple roles.

Seeing a MalformedPolicyDocument error?

  • Check that your JSON is valid. A trailing comma after the last item in an array is the most common cause.
  • Make sure you replaced the account ID placeholder with your actual 12-digit account ID in both resource ARNs.
  • help me fix a MalformedPolicyDocument error when creating my boundary policy
Attach the boundary and re-assume the role

Now you will attach the boundary to dev-role and assume it again to get fresh temporary credentials that reflect the new boundary.

  • Attach the permission boundary to the role by running this command:
aws iam put-role-permissions-boundary \
    --role-name dev-role \
    --permissions-boundary arn:aws:iam::[[AWS_ACCOUNT_ID="your-account-id"]]:policy/dev-boundary-policy

What does this command do?

This sets the maximum permissions envelope for dev-role. From now on, the effective permissions are the intersection of the identity policy and this boundary. The command produces no output on success.

  • Clear any previous temporary credentials and re-assume the role by running these commands:
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
  • Assume the role again to get fresh credentials that include the boundary by running:
aws sts assume-role \
    --role-arn arn:aws:iam::[[AWS_ACCOUNT_ID="your-account-id"]]:role/dev-role \
    --role-session-name boundary-test
  • Export the temporary credentials from the output by running these commands (replace the values with your actual output):
export AWS_ACCESS_KEY_ID=your-access-key-id-here
export AWS_SECRET_ACCESS_KEY=your-secret-access-key-here
export AWS_SESSION_TOKEN=your-session-token-here
  • Verify you are operating as the role by running:
aws sts get-caller-identity

The output should show the assumed role ARN containing dev-role/boundary-test, confirming you are now operating with the boundary in effect.

Still seeing your original user identity?

  • Make sure you ran all three unset commands before re-assuming the role. Stale credentials override the new ones.
  • Confirm you exported all three values: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN.
  • help me troubleshoot why my assumed role credentials are not working
Prove the boundary blocks the delete

The identity policy on dev-role still grants s3:DeleteObject. But the permission boundary only allows s3:GetObject and s3:PutObject. The effective permissions are the intersection of both.

Before you run the next command, predict what will happen. Will the delete succeed like it did in the previous step, or will something block it?

  • Attempt to delete the test object by running:
aws s3 rm s3://iam-boundary-test-[[AWS_ACCOUNT_ID="your-account-id"]]/test-object.txt

You will see an Access Denied error. The boundary blocked the delete even though the identity policy explicitly allows it.

Why did it fail this time?

Effective permissions equal the identity policy INTERSECT the permission boundary. The identity policy allows s3:DeleteObject, but the boundary does not. Since s3:DeleteObject is not in both, it gets an implicit deny.

This is the core mechanism: no matter what someone adds to the identity policy later, the boundary caps what the role can actually do.

  • Confirm that reads still work by running:
aws s3 cp s3://iam-boundary-test-[[AWS_ACCOUNT_ID="your-account-id"]]/test-object.txt -

You should see the contents of test-object.txt printed to your terminal.

  • Confirm that writes still work by running:
echo "boundary write test" | aws s3 cp - s3://iam-boundary-test-[[AWS_ACCOUNT_ID="your-account-id"]]/boundary-test.txt

This should succeed because s3:PutObject exists in both the identity policy and the boundary.

Getting Access Denied on reads or writes too?

  • Double-check that your boundary-policy.json includes both s3:GetObject and s3:PutObject in the Action array.
  • Verify the Resource ARNs in the boundary match your bucket name exactly, including the /* suffix for object-level access.
  • IAM changes can take a few seconds to propagate. Wait 10 seconds and try again.
  • help me debug why my boundary policy is blocking reads and writes

You have now proven the intersection model in action. The permission boundary caps what the role can ever do, regardless of what identity policies grant. Next up, you will use IAM Policy Simulator and Access Analyzer to prove these access decisions mathematically, without making real API calls.

Validate with Policy Simulator and Access Analyzer

You proved the permission boundary works by running real API calls and watching them get denied. That is good for a demo, but in production you need to prove access decisions mathematically before deploying a policy.

The IAM Policy Simulator and IAM Access Analyzer catch issues without making real API calls. In this step, you will simulate actions against your role, validate your policy document for flaws, and create an analyzer to surface external access risks.

In this step, get ready to:
  • Simulate actions against the role and confirm the boundary blocks s3:DeleteObject mathematically.
  • Validate the overly permissive policy with Access Analyzer and review its findings.
  • Create an Access Analyzer analyzer and review findings for your account.
Simulate actions with the IAM Policy Simulator

The simulate-principal-policy command evaluates a principal's policies and reports whether each action would be allowed or denied. It respects the permission boundary that is attached to the role.

  • Simulate s3:GetObject, s3:PutObject, and s3:DeleteObject against your role by running this command:
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::[[AWS_ACCOUNT_ID="your 12-digit account ID"]]:role/dev-role \
  --action-names s3:GetObject s3:PutObject s3:DeleteObject \
  --resource-arns arn:aws:s3:::iam-boundary-test-[[AWS_ACCOUNT_ID="your 12-digit account ID"]]/*

What does this command do?

The --policy-source-arn tells the simulator which principal to evaluate. It pulls all identity policies and the attached permission boundary for that role.

The --action-names lists the S3 actions to test. The --resource-arns scopes the simulation to your specific bucket objects.

Before you read the output, predict: which of the three actions do you expect to see denied?

In the JSON output, look at the EvalDecision field for each action. You should see s3:GetObject and s3:PutObject with allowed, and s3:DeleteObject with implicitDeny.

Why implicitDeny and not explicitDeny?

A permission boundary does not contain an explicit Deny statement. It simply omits the action from its allow list. Because the boundary does not grant s3:DeleteObject, the intersection of identity policy and boundary produces an implicit deny.

This is different from an explicit deny in an SCP or resource policy, which would override everything.

Seeing explicitDeny or all denied?

If all three actions show denied, confirm you attached the inline policy dev-policy to the role. Run aws iam list-role-policies --role-name dev-role and confirm it appears.

If s3:DeleteObject shows allowed, the boundary might not be attached. Run aws iam get-role --role-name dev-role and check the PermissionsBoundary field in the output.

Help me debug my simulate-principal-policy output

Validate your policy with Access Analyzer

The Policy Simulator tells you what would happen at runtime. Access Analyzer's validate-policy feature reviews the policy document itself and flags security issues, errors, and suggestions before you ever attach it.

  • Run validate-policy against your dev-policy.json file (the overly permissive version that includes s3:DeleteObject) by running:
aws accessanalyzer validate-policy \
  --policy-document file://dev-policy.json \
  --policy-type IDENTITY_POLICY

What does this command do?

The --policy-document flag points to your local JSON file. The --policy-type IDENTITY_POLICY tells Access Analyzer this is a policy attached to an IAM entity (not a resource or SCP).

Access Analyzer returns findings with a findingType of SECURITY, ERROR, WARNING, or SUGGESTION. Each finding describes what is wrong and how to fix it.

Review the output. You should see findings that flag your policy. The exact findings depend on your policy structure, but common results include suggestions about overly broad resource patterns or missing condition keys.

No charge for policy validation

Basic policy validation checks with Access Analyzer are free. You can run them as many times as you want during development without incurring costs.

Seeing an empty findings list?

Confirm dev-policy.json exists in your current directory. Run cat dev-policy.json to verify it contains the policy with s3:DeleteObject.

If the file is missing, recreate it with the identity policy JSON that includes s3:GetObject, s3:PutObject, and s3:DeleteObject.

Help me fix my validate-policy command

Create an Access Analyzer and review findings

Policy validation checks a single document. An Access Analyzer continuously monitors your account for resources that grant external access. You will create one now and review what it finds.

  • Create an Access Analyzer for your account by running:
aws accessanalyzer create-analyzer \
  --analyzer-name dev-access-analyzer \
  --type ACCOUNT

What does this command do?

The --type ACCOUNT scopes the analyzer to your account in the current region. It scans resource-based policies (like S3 bucket policies) and reports any that grant access to external principals.

You can only have one ACCOUNT-type analyzer per region. If you already have one, the command will return an error. In that case, use your existing analyzer's ARN in the next step.

The command returns the analyzer's ARN. Note it down for the next command.

  • List findings from your new analyzer by running:
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:[[AWS_REGION="your region, e.g. us-east-1"]]:[[AWS_ACCOUNT_ID="your 12-digit account ID"]]:analyzer/dev-access-analyzer

What will the findings show?

External access findings flag resources in your account that can be accessed from outside the account. Your S3 bucket uses only identity-based policies, so it likely has no external access findings.

If you see an empty list, that is a good sign. It means no resources in this region grant access to external principals. In a real environment with many resources, this is where you would catch accidentally public S3 buckets or overly broad role trust policies.

Seeing ConflictException when creating the analyzer?

This means an ACCOUNT-type analyzer already exists in this region. You can only have one per region. Run aws accessanalyzer list-analyzers to find the existing analyzer's ARN, then use that ARN in the list-findings command instead.

Help me fix the ConflictException error

Before you move on, predict: what did the simulate-principal-policy output prove that the validate-policy output did not?

The simulator proved that the boundary blocks s3:DeleteObject at evaluation time, even though the identity policy allows it. The validator flagged the policy document in isolation. Together, they give you both a runtime proof and a pre-deployment check.

You now have mathematical proof that your permission boundary works. You can show this output to an auditor and prove, without running a single real API call against S3, that the role cannot delete objects. Next up, you will take this further with the Secret Mission and implement delegated admin controls.

Secret mission

Implement the Delegated Admin Pattern

You proved that permission boundaries cap a role's power. But what if the developer needs to create roles for their team? In this mission, you'll let the developer role create new IAM roles, but only if it attaches your boundary. Try to skip the boundary and watch IAM slam the door.

Clean Up Your Resources

Clean Up Your Resources

This project uses only IAM and S3 free-tier resources, so there are no ongoing costs. Decide whether to keep your resources for continued experimentation or delete them entirely.

Resources you used:

  • S3 bucket (iam-boundary-test-<account-id>).
  • IAM role (dev-role) with inline policy and permissions boundary.
  • IAM managed policy (dev-boundary-policy).
  • IAM Access Analyzer (dev-access-analyzer).
  • Local JSON policy files (trust-policy.json, dev-policy.json, boundary-policy.json).

Keep everything running

No action needed. IAM resources are free and do not incur charges. Choose this if you want to continue experimenting with permission boundaries, trust policies, or role assumption.

Your local JSON files remain on your machine for reference.

Pause - I'll come back to this later

Since IAM resources are free, there is nothing to pause. Your roles, policies, and analyzer will sit idle at no cost until you return.

If you want to free up environment variables from your current terminal session, clear the temporary credentials by running:

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN

This restores your terminal to your base admin credentials.

Delete - I don't want to use this again

Remove all project resources and start fresh. If you completed the Secret Mission, these resources are already deleted. If you skipped it, follow these steps.

  • First, make sure you are using your admin credentials (not the assumed role). Clear any temporary credentials by running:
  • Delete the S3 bucket and its contents by running:
  • Remove the permissions boundary from the role by running:
  • Delete the inline policy from the role by running:
  • Delete the role by running:
  • Delete the managed boundary policy by running:
  • Delete the Access Analyzer by running:
  • Delete the local JSON files by running:
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
aws s3 rb s3://[[BUCKET_NAME="iam-boundary-test-YOUR-ACCOUNT-ID"]] --force
aws iam delete-role-permissions-boundary --role-name dev-role
aws iam delete-role-policy --role-name dev-role --policy-name dev-s3-access
aws iam delete-role --role-name dev-role
aws iam delete-policy --policy-arn arn:aws:iam::[[AWS_ACCOUNT_ID="your-account-id"]]:policy/dev-boundary-policy
aws accessanalyzer delete-analyzer --analyzer-name dev-access-analyzer
rm trust-policy.json dev-policy.json boundary-policy.json

How do I know it worked?

Each of these commands produces no output on success. If you see no error, the resource is gone.

To double check, run:

aws iam list-roles --query "Roles[?RoleName=='dev-role']"

You should see an empty list: [].

Nice Work!

Nice Work!

You just built a complete IAM access control setup from scratch. You can now explain and demo how temporary credentials, least-privilege policies, and permission boundaries work together to prevent privilege escalation.

You've learned how to:

  • Write a least-privilege IAM policy from scratch with zero wildcards, and assume a role using STS temporary credentials instead of hardcoded keys.
  • Prove the intersection model by watching a permission boundary block an action that the identity policy explicitly allowed.
  • Validate access decisions before deployment using IAM Policy Simulator and IAM Access Analyzer to catch overly permissive policies without making real API calls.
  • Secret Mission: Implemented the delegated admin pattern where a developer role can create downstream roles only if it attaches your permission boundary.

Ready to quiz yourself?