The situation
The fintech estate:
- 40 AWS accounts in a single AWS Organization, with a delegated security account (
444444444444) owned by the platform security team. - Workload accounts run a mix: microservices on EKS, serverless data pipelines, third-party integrations (a payment processor, a KYC vendor, two data partners) that legitimately need cross-account access to specific resources.
- Resources in scope for “external access”: S3 buckets (some public content, most not), KMS keys (one shared with the KYC vendor for envelope-encrypted PII handoff), IAM roles (cross-account assume-role for the partners and for the vendor’s workload), Lambda functions (one fronted by a partner’s API Gateway), SQS queues (one consumed by the payment processor), Secrets Manager secrets (shared with the payment processor for a runtime API key).
- IAM footprint: ~800 IAM roles across the estate, about 120 IAM users (mostly legacy, a migration to Identity Center in progress), several thousand attached policies.
The brief from the Head of Security: show every externally-shared resource across the Organization in one place; catch an accidental Principal: * in pull-request review; tell me which IAM roles nobody has touched in ninety days, and within the roles that are used, which permissions have never been exercised.
What actually matters
Before mapping services to the ask, worth naming the deeper properties of an answer.
The first observation is that the three asks look different but share an engine: proving properties about IAM and resource policies. “Is this resource shared outside the Org?” is a question about what a policy allows. “Does this policy grant Principal: * without a guard?” is a question about what a policy grants. “Did anyone ever use this permission?” is a question about whether the allowed action was ever exercised. All three want the same kind of answer, high confidence, resource-type coverage, and a definitive “yes this allows X” or “no this doesn’t.” Heuristics and pattern-matching aren’t the correct shape. Automated reasoning, proving policy properties mathematically, is.
Coverage across resource types is the breadth test. If the tool covers S3 and IAM but not Lambda permissions and SQS queue policies, there are blind spots where cross-account access can legitimately hide. The KYC vendor’s access is via an IAM role trust policy and a KMS key grant; the payment processor’s is via an SQS queue policy and a Secrets Manager resource policy; a partner integration lives in a Lambda permission. “Every externally-shared resource” means, in practice, every resource type that has a resource-based policy or a trust policy. Partial coverage produces the illusion of visibility.
Pre-deployment versus post-deployment is the timing property. Catching Principal: * after the apply is useful, the safety net, but the cheap fix is catching it at pull-request review or in the IDE as the developer types. The ideal tool has three synchronous entry points: a console view, a CLI command, and an IDE plugin, all hitting the same validation API with the same reasoning engine. The analyst reviewing the PR, the pipeline running the CI gate, and the developer writing the policy get the same answer from the same source of truth.
Unused-access identification is the property that closes the loop on least privilege. Every service ships with “principle of least privilege” in its security-best-practices document. Nearly every production estate has IAM roles with permissions somebody requested eighteen months ago and never exercised. The gap between policy and ambition is closed by a report that names untouched roles, untouched access keys, untouched console passwords, and, crucially, untouched permissions within otherwise-active roles. That last one is the service-granularity signal that turns “least privilege” from a slogan into a quarterly workflow.
Finally, there’s organisational deployment. Forty accounts, growing. The tool has to be configured once in the security account and read across every member account without the security team building a cross-account-IAM plumbing project of its own. Per-account deployment is the common pattern that silently drifts; delegated-administrator deployment with auto-enrol for new accounts is the pattern that stays aligned.
What we’ll filter on
- Resource-type coverage. S3, KMS, IAM roles (trust policies), Lambda, SQS, Secrets Manager, SNS, ECR, EFS, DynamoDB, RDS snapshots, EBS snapshots.
- Pre-deployment policy validation. Check a policy before it’s applied. IDE, CLI, IAM console.
- Unused-access identification. Untouched IAM users and roles; untouched access keys and passwords; untouched permissions and services within an active role.
- Org-wide roll-up. One analyzer configured in the security account that reads every member account.
- Operational overhead. Minimal pipeline to maintain, no Lambdas crawling
GetBucketPolicy, no Athena parsing CloudTrail, no Config rules the team has to keep current.
The visibility landscape
Five candidates.
1. IAM Access Analyzer. AWS’s automated-reasoning-based service for resource-policy and identity-policy issues. Three analyzer types: external access findings identify resources shared outside a zone of trust (the Organization, in the scenario); unused access findings identify IAM users, roles, access keys, passwords, and in-role permissions untouched within a configurable tracking period; policy validation runs the same reasoning engine over a policy document before it’s applied, via ValidatePolicy, the IAM console, the CLI, and AWS Toolkit IDE plugins. All three are organisation-aware when the security account is delegated administrator; findings flow to Security Hub as ASFF and EventBridge as events; external-access is free; unused-access is priced per IAM role per region per month.
2. AWS Config with custom rules. Config records resource configuration changes and evaluates them against managed or custom rules. Managed rules cover common cases – s3-bucket-public-read-prohibited, iam-root-access-key-check, mfa-enabled-for-iam-console-access. For cross-account grants the coverage thins: catching “S3 bucket policy grants to a principal outside the Org” needs a custom rule (Lambda or CloudFormation Guard) that you write and maintain. Config has no pre-deployment mode either, it evaluates after the change lands.
3. CloudTrail plus Athena. Query CloudTrail Lake or events in S3 to find PutBucketPolicy, PutKeyPolicy, CreateRole, AddPermission calls whose resulting policies name non-Org principals. Technically possible; operationally ugly. Parsing JSON payloads with json_extract_scalar, missing anything set before your query window. Unused-access means aggregating months of events and joining against the current IAM snapshot.
4. AWS Organizations SCPs and RCPs. Preventative, not detective. An SCP denying s3:PutBucketPolicy with a non-Org principal stops the accidental public bucket being created, and an RCP restricting cross-account access is the org-wide resource-side guardrail. Essential complements; neither answers the three asks directly. SCPs and RCPs are the fence; Access Analyzer is the patrol.
5. Third-party CSPM (Wiz, Orca, Prisma Cloud, Datadog CSP). Vendor platforms aggregating resource inventory and flagging exposure patterns including external shares. Strong on unified dashboards; weaker on AWS-native pre-deployment validation. Separate commercial relationship, separate trust boundary, separate place to look.
Side by side
| Option | Resource coverage | Pre-deployment validation | Unused-access identification | Org-wide roll-up | Ops overhead |
|---|---|---|---|---|---|
| IAM Access Analyzer | ✓ | ✓ | ✓ | ✓ | ✓ |
| AWS Config + custom rules | , | ✗ | ✗ | ✓ | ✗ |
| CloudTrail + Athena | , | ✗ | , | , | ✗ |
| SCPs / RCPs | , | , | ✗ | ✓ | ✓ |
| Third-party CSPM | ✓ | , | ✓ | ✓ | , |
Access Analyzer is the only row with all ticks. Config partially covers detection but has no pre-deployment mode and no unused-access story. CloudTrail plus Athena can answer most questions given enough SQL; the ops overhead row kills it. SCPs and RCPs belong in the picture as the preventative layer but are not themselves a visibility tool. Third-party CSPM is viable when the organisation already runs one; it’s not the AWS-native answer.
The three analyzer types
1. The external-access analyzer is what most people mean when they say “Access Analyzer”. Pick a zone of trust (the account, or the Organization) and the analyzer continuously evaluates resource-based policies against that trust boundary. Anything granting access outside the zone of trust becomes a finding. Covered resource types at the current version: S3 general-purpose and directory buckets, S3 access points, IAM role trust policies, KMS keys, Lambda functions and layers, SQS queues, Secrets Manager secrets, SNS topics, DynamoDB tables and streams, EBS volume snapshots, RDS DB snapshots, RDS DB cluster snapshots, ECR repositories, EFS file systems, and IAM Identity Center permission sets. Every finding names the resource, the external principal, the action granted, and the severity. Free.
The evaluation engine is Zelkova, AWS’s automated-reasoning layer. Zelkova translates a policy into logical statements and proves (or disproves) questions like “does this policy allow s3:GetObject from any principal outside account 444444444444?”. The answer is mathematically complete, it isn’t heuristics, it isn’t pattern-matching, and it won’t miss a weird Condition combination. The same engine underpins policy validation and the custom policy checks below.
2. The unused-access analyzer is the newer type (GA November 2023). Configure a tracking period, 1 to 365 days, default 90, and the analyzer reports IAM resources that haven’t been active in that window. Five finding classes: unused IAM role, unused IAM user, unused access key, unused password (console login credentials never used), and unused permissions within an otherwise-active role, the last being the most useful one, because it names the specific services and actions attached to a role that the role has never actually exercised.
Pricing is per IAM role per region per month. At $0.20/role in the scenario’s 800 roles across (say) three active regions, the bill is around $480/month for the unused-access analyzer, priced visibly because the scanning is substantive. The external-access analyzer is free.
3. Policy validation is the synchronous one. There’s no long-running analyzer to create; you call accessanalyzer:ValidatePolicy with a policy document and get findings back in four classes: ERROR (the policy is invalid, missing required fields, bad JSON, unknown actions), SECURITY_WARNING (the policy is valid but grants dangerous patterns – Principal: * without a Condition, actions that bypass controls), SUGGESTION (style and best practice, add a Sid, use wildcards less), WARNING (AWS deprecated patterns, soon-to-be-restricted features).
Policy validation shows up in three places: the IAM console policy editor (paste a policy, see findings inline); the AWS CLI (aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://policy.json in a CI step, failing the build on ERROR or SECURITY_WARNING); and AWS Toolkit IDE plugins (findings inline in VS Code, IntelliJ, PyCharm, against .json and .hcl files referring to IAM policies).
The custom policy checks – CheckNoNewAccess, CheckAccessNotGranted, CheckNoPublicAccess, extend this into proposition-checking: “does this new policy version grant anything the old one didn’t?”, “does this policy grant s3:DeleteBucket to anyone?”, “does this policy grant public access?”. These are the pull-request-gate operations; a merge that would grant any new access gets blocked at CI before the Terraform applies.
Organisation deployment
From the management account, the security account is designated delegated administrator for Access Analyzer. From then on, every analyzer configuration lives in the security account and applies org-wide. Three analyzers get created:
- One external-access analyzer with organisation zone of trust. Scans resource policies across all 40 member accounts; auto-enrols new accounts on Organization join.
- One unused-access analyzer with organisation zone of trust and a 90-day tracking period. Scans IAM users and roles across all member accounts.
- No third analyzer, policy validation doesn’t require one. It’s a synchronous API call invoked by the IAM console, CLI, IDE plugins, and CI jobs on demand. Permissions for the CI role to call
accessanalyzer:ValidatePolicyand the threeCheck*APIs are the only configuration.
Findings from both standing analyzers flow into AWS Security Hub as ASFF alongside GuardDuty, Inspector, Macie, Config, and third-party findings. They also emit EventBridge events on creation and status change, which is where severity-based routing gets wired: detail.severity: High pages on-call; detail.severity: Medium opens a Jira ticket; detail.severity: Low is visible in the dashboard but doesn’t fire anything.
Archive rules are how the signal-to-noise ratio stays workable. The KYC vendor legitimately has cross-account access to one KMS key. The payment processor legitimately has a Secrets Manager share. These are expected findings. An archive rule matching resourceType: AWS::KMS::Key AND principal: arn:aws:iam::<kyc-account>:role/KycWorkload AND action: kms:Decrypt auto-archives the finding on creation, so the review queue doesn’t drown in known-intentional shares.
Worked example, a bucket policy goes through the three paths
A developer on the payments team opens a pull request adjusting an S3 bucket policy for payments-reports-2027. They intend to grant a specific partner role read access. The new policy:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PartnerReadAccess",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::payments-reports-2027/*"
}]
}
The developer meant "Principal": {"AWS": "arn:aws:iam::999999999999:role/PartnerReporting"} but copy-pasted a snippet and left the wildcard. Three things catch this.
Path 1: in the IDE. The AWS Toolkit plugin calls ValidatePolicy as the developer types. A SECURITY_WARNING lights up: “Grants access to all principals. Specify a principal that identifies a single account, user, or role.” Caught at the moment of writing.
Path 2: in CI. The developer misses the warning and commits. The pull-request pipeline invokes CheckAccessNotGranted against the rendered bucket policy with an access list containing s3:GetObject for Principal: *. The check returns FAIL; the pipeline fails; the PR can’t merge.
Path 3: in prod (both earlier gates somehow bypassed). Within minutes, the external-access analyzer flags arn:aws:s3:::payments-reports-2027 as allowing an external principal; severity High. EventBridge routes to PagerDuty; no archive rule matches (wildcard principals are never auto-archived). A remediation Lambda applies a temporary deny-all policy scoped to non-privileged principals; the owner account is ticketed.
Three gates. The pre-deployment ones are cheaper to fix; the post-deployment one is the safety net.
What’s worth remembering
- IAM Access Analyzer has three analyzer types: external-access findings for resource sharing outside the zone of trust; unused-access findings for IAM users, roles, access keys, passwords, and in-role permissions untouched within the tracking period; policy validation as a synchronous API for pre-deployment checks.
- Zone of trust is either the account or the Organization. Organisation zone of trust requires delegated-administrator setup.
- External-access covered resource types include S3 buckets and access points, IAM role trust policies, KMS keys, Lambda functions and layers, SQS queues, Secrets Manager secrets, SNS topics, DynamoDB tables and streams, EBS and RDS snapshots, ECR repositories, EFS file systems, and IAM Identity Center permission sets.
- Unused-access analyzer (GA November 2023). Configurable tracking period (1-365 days, default 90); five finding classes including unused permissions within an otherwise-active role, the service-granularity least-privilege signal. $0.20 per IAM role per region per month. External-access is free.
- Policy validation runs synchronously via
ValidatePolicy. Four finding classes: ERROR, SECURITY_WARNING, SUGGESTION, WARNING. Used in the IAM console policy editor, AWS CLI, AWS Toolkit IDE plugins, and CI jobs. - Custom policy checks extend policy validation for pull-request gates:
CheckNoNewAccess(is this change more permissive?),CheckAccessNotGranted(does it grant any action in a blocked list?),CheckNoPublicAccess(does it grant public access?). All three return PASS or FAIL. - Automated reasoning is the engine. Zelkova proves policy properties rather than heuristically matching patterns; a weird
Conditioncombination won’t sneak past. - Findings flow to Security Hub as ASFF and EventBridge as events; severity-based routing lives in EventBridge; cross-service posture lives in Security Hub.
- Archive rules keep the queue workable, auto-archive known-intentional external shares so the unarchived queue contains only unexpected findings.
- AWS Config, CloudTrail+Athena, and SCPs/RCPs are complements, not substitutes. Config runs its own Lambda-rule fleet for bespoke checks; CloudTrail+Athena is the brute-force option for history; SCPs and RCPs are the preventative fence.