← Back to Blog

AWS IAM Access Analyzer: Finding Unintended External Access

6 min read• by Security Team

AWS IAM Access Analyzer: Finding Unintended External Access

Why IAM Access Analyzer Matters

Misconfigured resource policies are one of the most common causes of data breaches in AWS environments. A single S3 bucket policy with an overly broad principal, an IAM role with a trust policy that allows any AWS account to assume it, or a KMS key policy that grants decrypt access to an unintended external entity -- any of these can expose sensitive data or provide an attacker with a foothold.

IAM Access Analyzer uses automated reasoning (formal methods based on Zelkova, AWS's policy analysis engine) to evaluate resource policies and identify access paths that extend outside your zone of trust. Unlike manual policy review, it mathematically proves whether a policy allows external access, eliminating false negatives.

Setting Up Access Analyzer

Create an analyzer at the organization level for maximum visibility:

# Create an organization-level analyzer
aws accessanalyzer create-analyzer \
  --analyzer-name "org-external-access" \
  --type ORGANIZATION

# Verify it is active
aws accessanalyzer list-analyzers \
  --query 'analyzers[*].{Name:name,Status:status,Type:type}' \
  --output table

An organization-level analyzer evaluates resources across all member accounts and reports findings to the management or delegated administrator account. This is far more effective than per-account analyzers because cross-account access patterns are visible in one place.

Understanding External Access Findings

When Access Analyzer detects a resource policy that grants access to a principal outside your zone of trust, it generates a finding with these key fields:

  • Resource: The ARN of the resource with external access
  • Principal: The external entity that has access
  • Action: The specific API actions that are accessible
  • Condition: Any conditions that limit the access
  • Status: Active, Archived, or Resolved

Common Finding Categories

S3 Buckets: Public access via bucket policies or ACLs. This is the most frequent finding type.

IAM Roles: Trust policies that allow cross-account role assumption without proper conditions.

KMS Keys: Key policies granting encrypt/decrypt to external accounts.

Lambda Functions: Resource-based policies allowing external invocation.

SQS Queues and SNS Topics: Policies allowing external accounts to publish or subscribe.

import boto3

def get_active_findings_by_resource_type(analyzer_arn):
    """Retrieve and categorize all active Access Analyzer findings."""
    client = boto3.client('accessanalyzer')
    findings_by_type = {}

    paginator = client.get_paginator('list_findings_v2')
    pages = paginator.paginate(
        analyzerArn=analyzer_arn,
        filter={
            'status': {
                'eq': ['ACTIVE']
            }
        }
    )

    for page in pages:
        for finding in page['findingsSummaries']:
            resource_type = finding['resourceType']
            if resource_type not in findings_by_type:
                findings_by_type[resource_type] = []
            findings_by_type[resource_type].append({
                'resource': finding['resource'],
                'resourceOwnerAccount': finding['resourceOwnerAccount'],
                'status': finding['status'],
                'createdAt': str(finding['analyzedAt'])
            })

    for resource_type, findings in findings_by_type.items():
        print(f"\n{resource_type}: {len(findings)} findings")
        for f in findings:
            print(f"  - {f['resource']} (Account: {f['resourceOwnerAccount']})")

    return findings_by_type

Unused Access Analysis

Launched in late 2023, unused access analysis identifies permissions that exist but have not been used. This is critical for enforcing least privilege because it answers the question: "What permissions does this role have that it does not actually need?"

Enable unused access analysis when creating an analyzer:

# Create an unused access analyzer with a 90-day tracking period
aws accessanalyzer create-analyzer \
  --analyzer-name "unused-access-analyzer" \
  --type ORGANIZATION_UNUSED_ACCESS \
  --configuration '{
    "unusedAccess": {
      "unusedAccessAge": 90
    }
  }'

The analyzer generates findings for:

  • Unused roles: Roles that have not been assumed in the tracking period
  • Unused permissions: Specific actions in a role's policies that have not been invoked
  • Unused access keys: Access keys with no API activity

Generating Policies from CloudTrail

Instead of writing IAM policies from scratch and guessing which permissions a workload needs, you can generate a policy based on actual CloudTrail activity:

import boto3
import time
import json

def generate_policy_from_cloudtrail(role_arn, trail_arn, days=90):
    """Generate a least-privilege policy based on actual role usage."""
    client = boto3.client('accessanalyzer')
    from datetime import datetime, timedelta

    end_time = datetime.utcnow()
    start_time = end_time - timedelta(days=days)

    response = client.start_policy_generation(
        policyGenerationDetails={
            'principalArn': role_arn
        },
        cloudTrailDetails={
            'trails': [
                {
                    'cloudTrailArn': trail_arn,
                    'allRegions': True
                }
            ],
            'accessRole': 'arn:aws:iam::123456789012:role/AccessAnalyzerRole',
            'startTime': start_time,
            'endTime': end_time
        }
    )

    job_id = response['jobId']
    print(f"Policy generation started: {job_id}")

    # Poll until complete
    while True:
        result = client.get_generated_policy(jobId=job_id)
        status = result['jobDetails']['status']
        if status in ('SUCCEEDED', 'FAILED', 'CANCELED'):
            break
        print(f"Status: {status}, waiting...")
        time.sleep(10)

    if status == 'SUCCEEDED':
        policies = result['generatedPolicyResult']['generatedPolicies']
        for i, policy in enumerate(policies):
            print(f"\nGenerated Policy {i + 1}:")
            print(json.dumps(json.loads(policy['policy']), indent=2))
        return policies

    print(f"Policy generation failed: {status}")
    return None

This approach produces policies that reflect real-world usage, dramatically reducing the risk of over-permissioning.

Custom Policy Checks in CI/CD

Access Analyzer's custom policy checks let you validate IAM policies against your security standards before deployment. This is where Access Analyzer shifts from detective to preventive control.

Example: Block Policies That Grant s3:* on All Resources

# check-policy.sh - Run as a CI/CD pipeline step
#!/bin/bash
set -euo pipefail

POLICY_FILE="$1"

RESULT=$(aws accessanalyzer check-access-not-granted \
  --analyzer-arn "arn:aws:accessanalyzer:us-east-1:123456789012:analyzer/ci-policy-checker" \
  --access '[{"actions": ["s3:*"], "resources": ["*"]}]' \
  --policy-document "file://${POLICY_FILE}" \
  --policy-type IDENTITY_POLICY \
  --output json)

CHECK_RESULT=$(echo "$RESULT" | jq -r '.result')

if [ "$CHECK_RESULT" = "FAIL" ]; then
  echo "BLOCKED: Policy grants s3:* on all resources"
  echo "$RESULT" | jq '.reasons'
  exit 1
fi

echo "PASSED: Policy does not grant overly broad S3 access"

Integrate this into your Terraform, CDK, or CloudFormation pipeline so that IAM policy changes are validated before they are applied.

Policy Validation Checks

In addition to access-not-granted checks, Access Analyzer provides:

  • ValidatePolicy: Checks for grammar errors, best practice warnings, and security suggestions
  • CheckNoNewAccess: Compares a new policy against a reference policy and ensures it does not grant additional access
  • CheckNoPublicAccess: Verifies that a resource policy does not allow public access

Securing External Access with AccessLens

IAM Access Analyzer is a powerful tool, but it focuses on individual resource policies. In complex multi-account environments, the full picture of external access requires understanding how IAM roles, trust relationships, and resource policies interact across accounts.

AccessLens complements Access Analyzer by providing:

  • Cross-Account Trust Graph Visualization: See every trust relationship across your AWS accounts in an interactive graph, making it immediately obvious which external entities have access and through which roles.
  • Risk-Scored Findings: AccessLens assigns severity scores to every IAM risk it detects, including overprivileged roles, unused permissions, and dangerous trust configurations. This helps you prioritize remediation.
  • Continuous Scanning: While Access Analyzer generates point-in-time findings, AccessLens provides scheduled scans that track how your IAM posture changes over time.
  • Compliance Reporting: Generate reports that combine Access Analyzer findings with AccessLens's own analysis for a comprehensive view of your IAM security posture.

Use Access Analyzer for automated policy reasoning and AccessLens for the broader context of how access flows through your organization.

Gain full IAM visibility across your AWS accounts with AccessLens

Ready to secure your AWS environment?

Get comprehensive IAM visibility across all your AWS accounts in minutes.