DEV Community

Alex
Alex

Posted on

Give Your Auditor AWS Access — Without Sharing a Single Credential

If your auditor asks for that, that should be the first finding. 🚩

An external auditor needs access to your AWS accounts, and the first instinct in most companies is one of two things: create an IAM user and share the access keys, or provision the auditor a user in your identity provider so they can log into the console.

Neither is necessary. An auditor doesn’t need a login in your organization at all — API access is enough. And AWS has had a mechanism for exactly this situation for over a decade. It’s the same one AWS partners and every third-party monitoring tool use:

A cross-account IAM role with a trust relationship. No credentials ever change hands.

In this article I’ll walk through how the pattern works and share two CloudFormation templates I built — one for a single AWS account, one that covers an entire AWS Organization. You can deploy either in about five minutes. Both templates are also on GitHub: https://github.com/up-cdk/aws-audit-access

Why not just create a user?

Because every credential you create is a liability you now own:

  • Access keys are long-lived. They work until someone remembers to delete them. Every audit I’ve read about that went wrong involved keys that outlived the engagement.

  • Passwords get shared insecurely. Email, Slack, a text message — the moment credentials travel, you’ve lost control of where they end up.

  • IdP users pollute your directory. An external party in your identity provider inherits whatever group memberships and app assignments someone clicks together, and deprovisioning is a manual step someone has to remember.

The pattern: cross-account role + External ID

The idea is simple. Instead of giving the auditor your credentials, you create a role in your account that the auditor’s own AWS account is allowed to assume.

Four pieces make it safe:

  1. The trust policy names exactly one AWS account — the auditor’s. Not a user, not a password: an account-to-account trust. The auditor authenticates in their account with their credentials, then calls STS AssumeRole to get temporary credentials for the role in yours.

  2. An External ID guards the trust. This is a shared secret agreed between you and the auditor, set as a condition on the trust policy. Without presenting it, the role cannot be assumed — even by the trusted account. This protects against the confused deputy problem: if the auditor works with many clients, a malicious client can’t trick the auditor’s tooling into accessing your account, because they don’t know your External ID.

  3. All access is temporary. STS sessions expire (1 hour by default in these templates, configurable). There is no long-lived credential anywhere in the setup.

  4. Everything is logged. Every AssumeRole call and every API call the auditor makes lands in your CloudTrail, attributable to the audit role’s session. You can see exactly what was looked at, and when.

And the permissions? Read-only, and scoped for the job: AWS’s managed ReadOnlyAccess and SecurityAudit policies, optionally AWSBillingReadOnlyAccess for cost review, plus a small supplementary policy for audit-relevant services the managed policies miss (GuardDuty findings, Security Hub, Cost Explorer, Trusted Advisor, and a few others).

Option 1: single account

Deploy this template in the account being audited. Two parameters matter: the auditor’s 12-digit AWS account ID, and the External ID they give you.

Template: aws-audit-readonly-role.yaml in the GitHub repo — https://github.com/up-cdk/aws-audit-access

Deploy it from the console (CloudFormation → Create stack → Upload template), or from the CLI:

aws cloudformation deploy --template-file aws-audit-readonly-role.yaml --stack-name external-audit-access --capabilities CAPABILITY_NAMED_IAM  --parameter-overrides AuditorAWSAccountId=111122223333 ExternalId=the-external-id-from-your-auditor
Enter fullscreen mode Exit fullscreen mode

Then send the auditor the role ARN from the stack outputs. That’s it — that’s the entire exchange. An ARN is not a secret; combined with the External ID (which the auditor already has, since they generated it) and the trust policy, it’s everything they need and nothing more.

Option 2: the entire AWS Organization

Auditing one account is rare — most audits cover an Organization with anywhere from a handful to hundreds of member accounts. Creating the role by hand in each one doesn’t scale.

The second template solves this with a service-managed CloudFormation StackSet. You deploy one stack in the management account, and it:

  • creates the audit role in the management account itself,

  • creates a StackSet that rolls the same role out to every member account under the OUs you target (pass your root ID, e.g. r-abcd, to cover the whole organization),

  • optionally auto-deploys to any new account added to those OUs later — no gaps if accounts are created mid-audit,

  • and tears everything down centrally when you delete the stack.

One prerequisite: service-managed StackSets require trusted access between AWS Organizations and CloudFormation to be activated (CloudFormation console → StackSets → “Activate trusted access”, a one-time click in the management account).

Template: aws-audit-organization-stackset.yaml in the same repo. The deploy command:

aws cloudformation deploy --template-file aws-audit-organization-stackset.yaml --stack-name external-audit-access-org --capabilities CAPABILITY_NAMED_IAM --parameter-overrides AuditorAWSAccountId=111122223333 ExternalId=the-external-id-from-your-auditor TargetOrganizationalUnitIds=r-abcd DeploymentRegions=us-east-1

IAM is a global service, so one deployment region is enough for the stack instances.

After deployment, every in-scope account contains a role with the same name, so the auditor can iterate over account IDs with a single ARN pattern:

arn:aws:iam:::role/ExternalReadOnlyAuditRole

The auditor’s side

From their own account, the auditor assumes the role:

aws sts assume-role --role-arn arn:aws:iam::444455556666:role/ExternalReadOnlyAuditRole --role-session-name aws-audit --external-id THE_EXTERNAL_ID

For an organization audit, the auditor loops the same profile pattern over the account list — which they can fetch with organizations:ListAccounts, included in the role’s permissions.

Ending the audit

This is my favorite part, because it’s one line:

aws cloudformation delete-stack --stack-name external-audit-access

Delete the stack (or the org stack, which removes the StackSet instances from every member account). Access is gone. Nothing to rotate, nothing to deprovision, no forgotten IAM user with active keys waiting for the next auditor to find.

The way you grant audit access says a lot about how you manage access everywhere else. If your next audit starts with “please create a user for us” — send them this article instead.

Top comments (0)