Audit cross-account IAM access
Learn to audit cross-account access with IAM in this Cloud security essentials tutorial. Understand the core concepts, follow a step-by-step walkthrough, and prepare for the next lesson.
Focus: audit cross-account access with iam
You've built a multi-account architecture, but how do you actually know who can access what across those accounts? Without a clear audit trail, a single over-permissive role can become a silent backdoor. In this lesson, you'll learn how to audit cross-account access with IAM, turning AWS IAM's complexity into a manageable, auditable system.
The problem this lesson solves
In a multi-account AWS environment, cross-account access is both a necessity and a risk. You grant a role in Account B to a user in Account A, but over time, roles get modified, policies get attached, and trust relationships broaden. Soon, you have a web of access that no one fully understands. The problem isn't just security—it's also compliance. Auditors require evidence of who has access to what, and without a systematic audit process, you're flying blind.
Common pain points include:
- Scope creep: Roles accumulate permissions beyond their original intent.
- Lack of visibility: You can't easily answer "Who can access this S3 bucket?" across accounts.
- Trust relationship complexity: A role in Account B may trust multiple accounts, and tracking them manually is error-prone.
If you don't audit cross-account access regularly, you risk data breaches, failed compliance audits, and a loss of trust from stakeholders.
Core concept / mental model
Think of IAM as a security guard with a ledger. Each account has a guard, and cross-account access is a handshake between two guards: the trusting account (the one with the resource) and the trusted account (the one whose users need access). This handshake involves two key pieces:
- Trust policy: On the role in the trusting account, defines which accounts can assume the role.
- Permissions policy: On the role, defines what actions the role can perform once assumed.
Auditing cross-account access means reviewing all trust relationships and attached policies to ensure they align with your security requirements. You're essentially checking every handshake to make sure the right people have the right keys.
Consider this diagram-in-words:
Account A (Trusted) ---> Assumes Role ---> Account B (Trusting)
User Bob Role: AuditRole
Trust policy: Allow Account A
Permissions policy: ReadOnly access to S3
Core definitions
- Cross-account access: A principal from one AWS account assumes a role in another.
- Trust policy: A JSON policy on the role defining trusted entities.
- Permissions policy: A JSON policy on the role defining allowed actions.
- IAM Access Analyzer: An AWS service that identifies external access to your resources.
With this mental model, you can move on to the specifics of how to audit effectively.
How it works step by step
Auditing cross-account access isn't a single command—it's a process that combines visibility, analysis, and validation.
Step 1: Inventory your roles
First, identify all IAM roles that can be assumed cross-account. You can use the AWS Console or CLI. The goal is to create a list of roles with trust policies that include "AWS" principals from other accounts.
Use the AWS CLI to list roles and their trust policies:
aws iam list-roles --query "Roles[?AssumeRolePolicyDocument.Statement[?Principal.AWS]]" --output table
This gives you a starting list, though you'll need to parse trust policies more deeply.
Step 2: Analyze trust policies
For each role, inspect the trust policy to see which accounts can assume it. Look for patterns like "Principal": {"AWS": "arn:aws:iam::123456789012:root"} or "Principal": {"AWS": "arn:aws:iam::123456789012:role/SomeRole"}. A trust policy is the first place to spot dangerous access.
Step 3: Review attached permissions policies
Next, check what actions the role can perform. Attach policies may be inline or managed. Review them for overly broad permissions like "Action": "*" and "Resource": "*". This is where the blast radius is determined.
Step 4: Use IAM Access Analyzer for continuous audit
AWS IAM Access Analyzer can automatically detect external access. It scans your resources and generates findings for cross-account access, including roles, S3 buckets, and KMS keys. Its findings help you prioritize which cross-account relationships to review.
Enable Access Analyzer in each account and monitor its findings through AWS Security Hub or EventBridge.
Step 5: Document and remediate
Create a report of all cross-account access and compare it against your intended access model (e.g., a central identity account). For any role that shouldn't have cross-account trust, remove it. For permissions that are too broad, refine them.
The step-by-step process ensures you're not just looking at one aspect, but at the full picture.
Hands-on walkthrough
Now let's put this into practice. You'll write a Python script that uses boto3 to audit cross-account roles across a list of AWS profiles. This script will list roles, check trust policies, and flag roles with broad permissions.
Prerequisites
- AWS CLI configured with profiles for each account.
- Python 3.10+ and boto3 installed (
pip install boto3).
Example 1: List roles with cross-account trust
import boto3
import json
def get_roles_with_cross_account_access(session):
iam = session.client('iam')
roles = []
paginator = iam.get_paginator('list_roles')
for page in paginator.paginate():
for role in page['Roles']:
trust_policy = role.get('AssumeRolePolicyDocument', {})
statements = trust_policy.get('Statement', [])
for stmt in statements:
if stmt.get('Effect') == 'Allow':
principal = stmt.get('Principal', {})
aws_principal = principal.get('AWS')
if isinstance(aws_principal, list):
# Check if any principal is from a different account
for arn in aws_principal:
if 'iam::' in arn and not arn.startswith(f"arn:aws:iam::{session.client('sts').get_caller_identity()['Account']}"):
roles.append({
'RoleName': role['RoleName'],
'Principal': arn
})
elif isinstance(aws_principal, str):
if 'iam::' in aws_principal and not aws_principal.startswith(f"arn:aws:iam::{session.client('sts').get_caller_identity()['Account']}"):
roles.append({
'RoleName': role['RoleName'],
'Principal': aws_principal
})
return roles
# Example usage
session = boto3.Session(profile_name='prod-account')
roles = get_roles_with_cross_account_access(session)
print(f"Found {len(roles)} roles with cross-account access:")
for role in roles:
print(f" Role: {role['RoleName']}, Principal: {role['Principal']}")
Expected output (truncated):
Found 3 roles with cross-account access:
Role: DataReadRole, Principal: arn:aws:iam::123456789012:root
Role: AnalyticsRole, Principal: arn:aws:iam::123456789012:role/DataPipelineRole
Role: BackupRole, Principal: arn:aws:iam::210987654321:root
Example 2: Check for broad permissions on those roles
Once you have roles, check their attached policies for "Action": "*" or "Resource": "*":
import boto3
import json
def get_permissions_actions(session, role_name):
iam = session.client('iam')
actions = set()
# List attached managed policies
paginator = iam.get_paginator('list_attached_role_policies')
for page in paginator.paginate(RoleName=role_name):
for policy in page['AttachedPolicies']:
policy_arn = policy['PolicyArn']
policy_version = iam.get_policy_version(PolicyArn=policy_arn, VersionId='v1')
document = policy_version['PolicyVersion']['Document']
statements = document.get('Statement', [])
if isinstance(statements, dict):
statements = [statements]
for stmt in statements:
if stmt.get('Effect') == 'Allow':
action = stmt.get('Action')
if isinstance(action, list):
actions.update(action)
elif action:
actions.add(action)
return actions
session = boto3.Session(profile_name='prod-account')
role_name = 'DataReadRole'
permissions = get_permissions_actions(session, role_name)
print(f"Permissions for {role_name}:")
for a in sorted(permissions):
print(f" {a}")
if '*' in permissions:
print("Warning: This role has wildcard actions!")
Expected output:
Permissions for DataReadRole:
s3:GetObject
s3:ListBucket
Warning: none
In this case, the role is well-scoped. If you saw *, you'd flag it for review.
Example 3: Use IAM Access Analyzer to find external access
You can programmatically list active findings from Access Analyzer:
import boto3
def get_access_analyzer_findings(session):
access_analyzer = session.client('accessanalyzer')
analyzers = access_analyzer.list_analyzers()
for analyzer in analyzers['analyzers']:
if analyzer['status'] == 'ACTIVE':
response = access_analyzer.list_findings(analyzerArn=analyzer['arn'])
for finding in response['findings']:
print(f"Finding: {finding['resource']} - {finding['principal']} - {finding['status']}")
session = boto3.Session(profile_name='security-audit')
get_access_analyzer_findings(session)
This gives you automated detection of external access. These findings are your single source of truth for what needs review.
Compare options / when to choose what
There are multiple ways to audit cross-account access. Each has its trade-offs. Here's a comparison:
| Method | Pros | Cons | Best for |
|---|---|---|---|
| IAM Access Analyzer | Automated, continuous, detects external access | Requires setup, may miss some cases | Ongoing monitoring |
| Manual CLI script (like above) | Customizable, full control | Time-consuming, human error | One-time deep audits |
| AWS Organizations + SCPs | Centrally managed, preventive | Doesn't give detailed per-role visibility | Guardrails and broad policy control |
| Third-party tools (e.g., Cloudsplaining) | Pre-built analysis, reports | Extra cost, may not cover all services | Large organizations with complex needs |
When to choose what
- Choose IAM Access Analyzer for continuous, automated detection. It's the first line of defense.
- Choose a custom script when you need specific logic or a one-off investigation.
- Choose AWS Organizations SCPs to enforce a boundary (e.g., deny cross-account access except through a dedicated trust account) as a preventive control.
- Choose third-party tools for ready-made reports and when you need to scale across hundreds of accounts with limited team time.
Pro tip: Combine Access Analyzer for detection with SCPs for boundary enforcement. This gives you both detection and prevention.
Troubleshooting & edge cases
Even with a good process, you'll hit issues. Here are common pitfalls:
1. IAM Access Analyzer not discovering resources
Symptom: No findings even when you know cross-account access exists.
Fix: Ensure the analyzer is created in the correct region. Access Analyzer is region-specific, so you need one per region. Also, verify the service is enabled and has the necessary permissions to scan resources.
2. Trust policy uses Principal: "*"
Symptom: Your script flags a role, but it's actually public (everyone can assume it).
Fix: Treat this as a critical finding. A wildcard principal is even more dangerous than cross-account. Immediately restrict the trust policy to only known accounts.
3. Role chaining
Symptom: You audit role A, which trusts role B, but role B trusts an external account. Your audit misses this indirect access.
Fix: Map the full trust chain. IAM Access Analyzer reports direct access, not indirect. You may need to manually trace role chaining or use a script that recursively checks trust policies.
4. Partial match on account ID in trust policy
Symptom: Your script only looks for arn:aws:iam:: but the principal is an organization ID like arn:aws:iam::123456789012:root. Your regex fails.
Fix: Use a more robust check—parse the account ID from the ARN and compare it to your own account ID.
5. Deny statements override
Edge case: A trust policy has both Allow and Deny, but the Deny should override. Your script only checks for Allow and falsely flags the role.
Fix: When analyzing, honor the evaluation logic: if there is a matching Deny, the access is denied. Your audit script should account for Deny statements.
What you learned & what's next
You've learned the core ideas behind auditing cross-account access with IAM. You can now explain the concept, understand the trust and permission policy interplay, and perform a hands-on exercise to identify roles with cross-account trust and broad permissions. You also know how to use IAM Access Analyzer for continuous monitoring and how to choose between different audit methods.
Next lesson
Now that you can audit access, the next step is to implement least privilege across accounts. You'll learn how to design permission boundaries and use SCPs to enforce them, building a stronger defense-in-depth. Ready to dive in?
Practice recap
Run the provided Python script against your AWS account to list all roles with cross-account trust. Then, for each flagged role, check its permissions for wildcard actions and document your findings. As a challenge, set up IAM Access Analyzer and compare its findings with your script's output.
Common mistakes
- Frantic audits without a scripted process: Manually checking roles one by one is easy to miss and leads to incomplete coverage. Always automate with a script or Access Analyzer.
- Ignoring deny statements: Trust policies can include Deny, which overrides Allow. Auditors who only look for Allow risk false positives. Always account for evaluation logic.
- Assuming Access Analyzer covers all services: It may not detect all cross-account access, especially indirect access via role chaining. Use it as one tool, not the only tool.
- Forgetting to review inline policies: Many roles have inline policies beyond managed ones. Your audit must include those too, or you'll miss permissive actions.
- Not checking for wildcard principals (Principal: "*"): This is even more dangerous than cross-account access—it's public. Treat any wildcard principal as critical.
Variations
- Instead of a custom script, use Cloudsplaining (open-source tool) to generate a detailed HTML report of IAM roles, including cross-account trust analysis.
- Use AWS Organizations resource-based policies to control access at the organizational level, complementing per-role audits.
- Leverage AWS Config rules that automatically flag IAM roles with cross-account trust policies that violate your compliance standards.
Real-world use cases
- A finance company audits cross-account access quarterly to comply with PCI-DSS, proving that only authorized analysts can read transaction data in the production account.
- A SaaS startup uses a custom Python script during every deployment to ensure new roles don't accidentally grant cross-account access and create a security breach.
- A DevOps team uses IAM Access Analyzer findings to prioritize remediation of over-permissive roles discovered during a merger, reducing risk across newly joined accounts.
Key takeaways
- Cross-account access in IAM is defined by trust policies (who can assume) and permissions policies (what they can do).
- Auditing requires a systematic process: inventory, analyze trust, review permissions, use automation like Access Analyzer, and document findings.
- IAM Access Analyzer is a crucial automated tool, but it has limitations—use it alongside manual or custom checks.
- Broad permissions like wildcard actions and wildcard principals are critical findings that demand immediate remediation.
- Choose the right audit method based on your needs: continuous monitoring vs. one-time deep audit, and consider combining detection with SCPs for prevention.
- Regular audits are a security and compliance necessity to avoid silent backdoors in a multi-account environment.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.