Most SOC 2 access review programs built around AWS IAM Identity Center only see half the picture. Identity Center governs permission set assignments cleanly, but any IAM role with a trust policy allowing direct cross-account sts:AssumeRole calls — set up outside Identity Center, often years ago for a specific integration — doesn't show up in an Identity Center-centric review at all.
Where Cross-Account Roles Hide
These roles typically get created for legitimate reasons that predate a more mature access governance program: a data pipeline that needs to read from a logging account, a CI/CD role that deploys across a dev-to-prod account chain, a third-party monitoring tool granted a cross-account role years ago. Each one has a trust policy naming which principals can assume it, and that trust policy is the actual access control — not anything visible in Identity Center.
Building the Full Inventory
The starting point is enumerating every IAM role across every account in the Organization and extracting its trust policy's Principal block:
import boto3
import json
def find_cross_account_trust_relationships(account_ids, org_role_arn_template):
"""Assumes an audit role in each account and inspects every IAM role's
trust policy for principals outside that same account."""
findings = []
sts = boto3.client("sts")
for account_id in account_ids:
creds = sts.assume_role(
RoleArn=org_role_arn_template.format(account_id=account_id),
RoleSessionName="cross-account-audit",
)["Credentials"]
iam = boto3.client(
"iam",
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
for role in iam.list_roles()["Roles"]:
trust_doc = role["AssumeRolePolicyDocument"]
for stmt in trust_doc.get("Statement", []):
principal = stmt.get("Principal", {})
aws_principal = principal.get("AWS", [])
external_principals = [
p for p in (aws_principal if isinstance(aws_principal, list) else [aws_principal])
if account_id not in p
]
if external_principals:
findings.append({
"account_id": account_id,
"role_name": role["RoleName"],
"external_principals": external_principals,
})
return findings
Reconciling Findings Against Known-Good Access
Not every cross-account trust relationship is a problem — most are legitimate, intentional architecture. The value of this inventory is turning an invisible list into a reviewable one: each finding gets matched against a known-good register (CI/CD pipelines, monitoring integrations, data pipeline roles) with an owner and a business justification. Anything that doesn't match a known-good entry becomes a genuine finding requiring investigation — often an old integration that was decommissioned on one side but never had its trust policy cleaned up on the other.
What Auditors Want to See Here
CC6.1 and CC6.6 evidence around network and system access typically expects a complete picture of who can access what, not just what's visible through a single pane of glass. When we deliver this inventory as part of an engagement, it becomes a recurring quarterly job — not a one-time cleanup — so trust policy drift gets caught before it becomes an unreviewed backdoor sitting for years.
Frequently Asked Questions
How is this different from a routine access review through our GRC platform?
Most GRC platform integrations pull from Identity Center or a specific IdP, which doesn't see standalone IAM role trust policies configured outside that system. This inventory specifically targets the gap those integrations don't cover.
Do we need organization-wide admin access to run this audit?
You need a role in every account capable of reading IAM role and trust policy data — typically a read-only cross-account audit role deployed via a StackSet from the management account, not full administrative access.
How often should this cross-account inventory be refreshed?
Quarterly alongside your other access reviews is a reasonable cadence for most mid-market environments, though environments with frequent account creation or high integration churn may benefit from monthly checks.
What's the most common finding when we run this for the first time?
Orphaned cross-account roles tied to decommissioned integrations — the target system or vendor relationship ended, but nobody removed the corresponding trust policy entry, leaving a valid but unused access path.
See what an Access & Credential Governance Diagnostic finds in your cross-account trust policies → rutagon.com/contact or call 907-841-8407.