devuplabs.cloud
PreviewLab5 hours

Audit & Least Privilege

Use IAM Access Analyzer, policy simulation, and CloudTrail to audit permissions and tighten least-privilege policies.

Prerequisites

0 of 7 checked

Mental Model: The Audit Loop

Every production IAM hygiene practice fits into one of four phases. This lab covers all four.


IAM Access Analyzer

Goal

Create an Access Analyzer, generate findings by creating an overly permissive resource policy, interpret the finding JSON, archive a finding, and understand the difference between external access analysis and unused access analysis.

Estimated time: 1–2 hours


What Access Analyzer does

What's happening here

IAM Access Analyzer continuously examines resource-based policies (S3 bucket policies, SQS queue policies, KMS key policies, IAM role trust policies, Lambda resource policies, etc.) and identifies resources that are accessible from outside your zone of trust: by default, your AWS account. Any resource that grants access to a principal in another account, to the public (Principal: *), or to an AWS service externally, generates a finding. Findings are not errors, they're observations. Some are intentional (a public S3 website bucket). Others are accidents. Access Analyzer has two modes: external access (what's reachable from outside) and unused access (what permissions exist but haven't been used). External access is free. Unused access costs per IAM role/user analyzed per day.

bash
# Create an Access Analyzer for your account
# Zone of trust = ACCOUNT (findings = anything accessible from outside this account)
export ANALYZER_NAME="${LAB_PREFIX}-analyzer"

ANALYZER_ARN=$(aws accessanalyzer create-analyzer \
  --analyzer-name $ANALYZER_NAME \
  --type ACCOUNT \
  --region $AWS_REGION \
  --query 'arn' --output text)

echo "Analyzer ARN: $ANALYZER_ARN"

# List all analyzers in the region
aws accessanalyzer list-analyzers \
  --region $AWS_REGION | jq '.analyzers[] | {name, arn, type, status}'

type: ACCOUNT means the zone of trust is your account, any cross-account or public access generates a finding. If you were in an AWS Organization, you could set type to ORGANIZATION and the zone of trust would be the whole org.

Checkpoint: analyzer created with status ACTIVE.


Generate a finding: overly permissive S3 bucket policy

What's happening here

Access Analyzer scans resource-based policies continuously. When you attach a policy that grants access to a principal outside your zone of trust (another account, *, or an AWS service without aws:SourceAccount scoping), it generates a finding within minutes. We'll deliberately create an S3 bucket policy that allows public GetObject (a common accidental misconfiguration) and watch the finding appear.

bash
# Create an S3 bucket
export AUDIT_BUCKET="${LAB_PREFIX}-audit-demo"
aws s3api create-bucket \
  --bucket $AUDIT_BUCKET \
  --region $AWS_REGION

echo "Bucket: $AUDIT_BUCKET"

# Disable block public access on this bucket (so we can attach the permissive policy)
aws s3api put-public-access-block \
  --bucket $AUDIT_BUCKET \
  --public-access-block-configuration \
    BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false

# Attach a public read policy (classic accidental misconfiguration)
cat > /tmp/public-read-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AccidentalPublicRead",
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::${AUDIT_BUCKET}/*"
  }]
}
EOF

aws s3api put-bucket-policy \
  --bucket $AUDIT_BUCKET \
  --policy file:///tmp/public-read-policy.json

echo "Permissive policy applied. Access Analyzer will generate a finding within 1-5 minutes."
echo "Waiting 90 seconds before checking for findings..."
sleep 90
bash
# List findings from the analyzer
aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region $AWS_REGION | jq '.findings[] | {
    id: .id,
    resourceType: .resourceType,
    resource: .resource,
    status: .status,
    isPublic: .isPublic,
    principal: .principal
  }'

# Get the finding ID
FINDING_ID=$(aws accessanalyzer list-findings \
  --analyzer-arn $ANALYZER_ARN \
  --region $AWS_REGION \
  --query 'findings[?resourceType==`AWS::S3::Bucket`].id' \
  --output text | head -1)

echo "Finding ID: $FINDING_ID"

# Get full finding details
aws accessanalyzer get-finding \
  --analyzer-arn $ANALYZER_ARN \
  --id $FINDING_ID \
  --region $AWS_REGION | jq '.finding | {
    resource: .resource,
    resourceType: .resourceType,
    condition: .condition,
    action: .action,
    isPublic: .isPublic,
    status: .status,
    analyzedAt: .analyzedAt
  }'

finding shows isPublic: true, principal: {"*": "*"}, and action: ["s3:GetObject"]. The finding tells you exactly which principal has which access, not just "something is wrong" but the precise vector.


Fix the policy, watch the finding auto-resolve

What's happening here

Access Analyzer findings have a lifecycle: ACTIVERESOLVED (resource policy changed to remove the access) or ARCHIVED (you've acknowledged it's intentional). When you fix the policy, Access Analyzer detects the change and auto-resolves the finding within minutes. If you archive a finding without fixing it, it remains accessible but won't appear in the active queue, archive is for intentional findings like "this S3 bucket is a public static website".

Unlock all 24 AWS services & 291+ lab sessions (~180 hours)

Pricing