Skip to content

IAM permissions

vpcatlas only reads. It calls list, describe, get and search operations and never a create, update or delete API. This page covers the permissions the tool needs, how to grant them in one account or across several, and how the credentials reach the tool.

Which policy to use

Option When Trade-off
The minimal auditor policy (recommended) Any account you do not fully control, and every role you create just for this tool Only the actions the tool can call. Smaller to review, and it will not grant access to data you did not mean to share.
The AWS managed policy ReadOnlyAccess A quick first run in a sandbox, or an account where a role with it already exists It covers every collector, but it is far broader than what the tool calls.

Both work. A call that the role is not allowed to make is recorded as a gap and the scan carries on (see Limitations), so a policy that is too narrow gives an incomplete diagram, not a failed run.

The minimal auditor policy

Print it with the CLI. This needs no credentials and makes no AWS call:

vpcatlas iam-policy > auditor-policy.json
vpcatlas iam-policy --output auditor-policy.json     # same, with the path printed
vpcatlas iam-policy --core-only > core-policy.json   # only the statements the network diagrams need

--core-only keeps the statements whose name starts with Core. Use it when you want the network pages only: the service pages (DNS and edge, compute, data stores, account services) then show gaps for every call they make.

The same file is published in the repository as deploy/iam/auditor-policy.json and ships inside the package, so the command prints exactly this document:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CoreIdentity",
      "Effect": "Allow",
      "Action": [
        "sts:GetCallerIdentity"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CoreEc2Network",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeAvailabilityZones",
        "ec2:DescribeCustomerGateways",
        "ec2:DescribeEgressOnlyInternetGateways",
        "ec2:DescribeInternetGateways",
        "ec2:DescribeManagedPrefixLists",
        "ec2:DescribeNatGateways",
        "ec2:DescribeNetworkAcls",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DescribeRegions",
        "ec2:DescribeRouteTables",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeTransitGatewayAttachments",
        "ec2:DescribeTransitGatewayPeeringAttachments",
        "ec2:DescribeTransitGatewayRouteTables",
        "ec2:DescribeTransitGateways",
        "ec2:DescribeTransitGatewayVpcAttachments",
        "ec2:DescribeVpcEndpointConnections",
        "ec2:DescribeVpcEndpoints",
        "ec2:DescribeVpcEndpointServiceConfigurations",
        "ec2:DescribeVpcPeeringConnections",
        "ec2:DescribeVpcs",
        "ec2:DescribeVpnConnections",
        "ec2:DescribeVpnGateways",
        "ec2:GetManagedPrefixListEntries",
        "ec2:GetTransitGatewayRouteTableAssociations",
        "ec2:GetTransitGatewayRouteTablePropagations",
        "ec2:SearchTransitGatewayRoutes"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CoreElasticLoadBalancing",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeTags"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CoreNetworkFirewall",
      "Effect": "Allow",
      "Action": [
        "network-firewall:DescribeFirewall",
        "network-firewall:DescribeFirewallPolicy",
        "network-firewall:ListFirewallPolicies",
        "network-firewall:ListFirewalls"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CoreRoute53Resolver",
      "Effect": "Allow",
      "Action": [
        "route53resolver:ListResolverEndpointIpAddresses",
        "route53resolver:ListResolverEndpoints",
        "route53resolver:ListResolverRuleAssociations",
        "route53resolver:ListResolverRules"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CoreResourceAccessManager",
      "Effect": "Allow",
      "Action": [
        "ram:GetResourceShares",
        "ram:ListPrincipals",
        "ram:ListResources"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CoreDirectConnect",
      "Effect": "Allow",
      "Action": [
        "directconnect:DescribeConnections",
        "directconnect:DescribeDirectConnectGatewayAssociations",
        "directconnect:DescribeDirectConnectGatewayAttachments",
        "directconnect:DescribeDirectConnectGateways",
        "directconnect:DescribeVirtualInterfaces"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalEc2Network",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeAddresses",
        "ec2:DescribeClientVpnAuthorizationRules",
        "ec2:DescribeClientVpnEndpoints",
        "ec2:DescribeClientVpnRoutes",
        "ec2:DescribeClientVpnTargetNetworks",
        "ec2:DescribeDhcpOptions",
        "ec2:DescribeFlowLogs",
        "ec2:DescribeInstances",
        "ec2:DescribeIpamPools",
        "ec2:DescribeIpams",
        "ec2:DescribeIpamScopes",
        "ec2:DescribeSecurityGroupRules",
        "ec2:DescribeTransitGatewayConnectPeers",
        "ec2:DescribeTransitGatewayConnects",
        "ec2:DescribeVpcAttribute",
        "ec2:DescribeVpcEndpointServicePermissions",
        "ec2:DescribeVpcEndpointServices",
        "ec2:GetIpamPoolAllocations",
        "ec2:GetIpamPoolCidrs",
        "ec2:GetIpamResourceCidrs",
        "ec2:GetManagedPrefixListAssociations"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalElasticLoadBalancing",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeRules",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalNetworkFirewall",
      "Effect": "Allow",
      "Action": [
        "network-firewall:DescribeLoggingConfiguration",
        "network-firewall:ListRuleGroups"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalRoute53Resolver",
      "Effect": "Allow",
      "Action": [
        "route53resolver:ListFirewallRuleGroupAssociations",
        "route53resolver:ListFirewallRuleGroups",
        "route53resolver:ListResolverQueryLogConfigAssociations",
        "route53resolver:ListResolverQueryLogConfigs"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalRoute53PrivateZones",
      "Effect": "Allow",
      "Action": [
        "route53:GetHostedZone",
        "route53:ListHostedZones",
        "route53:ListHostedZonesByVPC"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalResourceAccessManager",
      "Effect": "Allow",
      "Action": [
        "ram:GetResourceShareAssociations",
        "ram:GetResourceShareInvitations"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalDirectConnect",
      "Effect": "Allow",
      "Action": [
        "directconnect:DescribeHostedConnections",
        "directconnect:DescribeLags",
        "directconnect:DescribeLocations"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalGlobalAccelerator",
      "Effect": "Allow",
      "Action": [
        "globalaccelerator:ListAccelerators",
        "globalaccelerator:ListCrossAccountAttachments",
        "globalaccelerator:ListCrossAccountResources",
        "globalaccelerator:ListEndpointGroups",
        "globalaccelerator:ListListeners"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalVpcLattice",
      "Effect": "Allow",
      "Action": [
        "vpc-lattice:ListServiceNetworks",
        "vpc-lattice:ListServiceNetworkServiceAssociations",
        "vpc-lattice:ListServiceNetworkVpcAssociations",
        "vpc-lattice:ListServices"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalNetworkManager",
      "Effect": "Allow",
      "Action": [
        "networkmanager:DescribeGlobalNetworks",
        "networkmanager:GetConnections",
        "networkmanager:GetCoreNetwork",
        "networkmanager:GetCoreNetworkPolicy",
        "networkmanager:GetDevices",
        "networkmanager:GetLinkAssociations",
        "networkmanager:GetLinks",
        "networkmanager:GetSites",
        "networkmanager:GetTransitGatewayRegistrations",
        "networkmanager:ListAttachments",
        "networkmanager:ListCoreNetworks",
        "networkmanager:ListPeerings"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalAccountNames",
      "Effect": "Allow",
      "Action": [
        "iam:ListAccountAliases",
        "organizations:DescribeOrganization",
        "organizations:ListAccounts"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalAccountServices",
      "Effect": "Allow",
      "Action": [
        "cloudtrail:DescribeTrails",
        "cloudtrail:GetTrailStatus",
        "cloudwatch:DescribeAlarms",
        "config:DescribeConfigurationRecorderStatus",
        "config:DescribeConfigurationRecorders",
        "config:DescribeDeliveryChannels",
        "ecr:DescribeRepositories",
        "guardduty:GetDetector",
        "guardduty:ListDetectors",
        "kms:DescribeKey",
        "kms:ListAliases",
        "kms:ListKeys",
        "logs:DescribeLogGroups"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalApiGateway",
      "Effect": "Allow",
      "Action": [
        "apigateway:GET"
      ],
      "Resource": [
        "arn:aws:apigateway:*::/restapis",
        "arn:aws:apigateway:*::/restapis/*",
        "arn:aws:apigateway:*::/apis",
        "arn:aws:apigateway:*::/apis/*",
        "arn:aws:apigateway:*::/domainnames",
        "arn:aws:apigateway:*::/domainnames/*",
        "arn:aws:apigateway:*::/vpclinks",
        "arn:aws:apigateway:*::/vpclinks/*"
      ]
    },
    {
      "Sid": "DenyApiGatewaySecrets",
      "Effect": "Deny",
      "Action": [
        "apigateway:GET"
      ],
      "Resource": [
        "arn:aws:apigateway:*::/apikeys",
        "arn:aws:apigateway:*::/apikeys/*",
        "arn:aws:apigateway:*::/clientcertificates",
        "arn:aws:apigateway:*::/clientcertificates/*"
      ]
    },
    {
      "Sid": "OptionalCompute",
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups",
        "ecs:DescribeClusters",
        "ecs:DescribeServices",
        "ecs:ListClusters",
        "ecs:ListServices",
        "eks:DescribeCluster",
        "eks:DescribeNodegroup",
        "eks:ListClusters",
        "eks:ListFargateProfiles",
        "eks:ListNodegroups",
        "lambda:GetPolicy",
        "lambda:ListFunctions"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalDataStores",
      "Effect": "Allow",
      "Action": [
        "dynamodb:ListTables",
        "elasticache:DescribeCacheClusters",
        "elasticache:DescribeCacheSubnetGroups",
        "elasticache:DescribeReplicationGroups",
        "elasticfilesystem:DescribeFileSystems",
        "elasticfilesystem:DescribeMountTargetSecurityGroups",
        "elasticfilesystem:DescribeMountTargets",
        "rds:DescribeDBClusters",
        "rds:DescribeDBInstances",
        "rds:DescribeDBSubnetGroups",
        "s3:GetBucketLocation",
        "s3:GetBucketPublicAccessBlock",
        "s3:GetBucketWebsite",
        "s3:ListAllMyBuckets"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalEdge",
      "Effect": "Allow",
      "Action": [
        "acm:DescribeCertificate",
        "acm:ListCertificates",
        "acm:ListTagsForCertificate",
        "cloudfront:ListDistributions",
        "cloudfront:ListTagsForResource",
        "shield:GetSubscriptionState",
        "shield:ListProtections",
        "wafv2:GetLoggingConfiguration",
        "wafv2:GetWebACL",
        "wafv2:ListResourcesForWebACL",
        "wafv2:ListWebACLs"
      ],
      "Resource": "*"
    },
    {
      "Sid": "OptionalRoute53Records",
      "Effect": "Allow",
      "Action": [
        "route53:ListResourceRecordSets",
        "route53:ListTagsForResources"
      ],
      "Resource": "*"
    }
  ]
}

Size matters when you create the role. Without whitespace the document is 7,678 characters. That fits the 10,240 character quota for the inline policies of a role, but it is over the 6,144 character quota of a customer managed policy. Attach it as an inline policy (the Terraform example below does), or use ReadOnlyAccess. Quotas can change; check Service Quotas for IAM in your account.

What each statement is for

The policy has two groups of statements. The names tell which: Core statements are what the network diagrams need, Optional statements feed the service pages and the labels. A denied Optional call costs detail, never the run.

Statement Grants What it is for
CoreIdentity sts:GetCallerIdentity Reads which account, ARN and partition the credentials belong to. AWS does not require a permission for this call; the statement documents it.
CoreEc2Network ec2:Describe* and Get* calls for VPCs, subnets, route tables, gateways, NAT, ENIs, security groups, network ACLs, prefix lists, Transit Gateways with their route tables, peering, VPN, endpoints and endpoint services The network itself: everything drawn on the Global, Region and VPC pages.
CoreElasticLoadBalancing DescribeLoadBalancers, DescribeTags Load balancers and their names.
CoreNetworkFirewall List and describe firewalls and firewall policies Firewalls in the hub strip. Rule bodies are never read.
CoreRoute53Resolver Resolver endpoints, their IP addresses, rules and rule associations Resolver endpoints and rules, including rules shared with you.
CoreResourceAccessManager ram:GetResourceShares, ListPrincipals, ListResources AWS RAM shares, which is how a Transit Gateway, subnet or Resolver rule from another account is found.
CoreDirectConnect Connections, virtual interfaces, gateways, associations and attachments Direct Connect links on the Global page.
OptionalEc2Network ec2:DescribeInstances and a group of other ec2 reads DescribeInstances groups instances by Auto Scaling group or Name tag. See the note below for the other actions.
OptionalElasticLoadBalancing Listeners, rules, target groups, target health What each load balancer forwards to.
OptionalNetworkFirewall Logging configuration, rule groups Not called by any collector in 0.1.0.
OptionalRoute53Resolver DNS Firewall rule groups, query log configs Not called by any collector in 0.1.0.
OptionalRoute53PrivateZones route53:ListHostedZones, GetHostedZone, ListHostedZonesByVPC Hosted zones, drawn in the edge and DNS layer.
OptionalResourceAccessManager GetResourceShareInvitations, GetResourceShareAssociations GetResourceShareInvitations reports pending RAM invitations, so a share that was not accepted does not look missing. GetResourceShareAssociations is not called in 0.1.0.
OptionalDirectConnect Hosted connections, LAGs, locations Not called by any collector in 0.1.0.
OptionalGlobalAccelerator List accelerators, listeners, endpoint groups Not called by any collector in 0.1.0. A Route 53 alias to an accelerator is recognised without it.
OptionalVpcLattice List service networks, services and associations Not called by any collector in 0.1.0.
OptionalNetworkManager Global networks, core networks, sites, links, attachments Not called by any collector in 0.1.0.
OptionalAccountNames iam:ListAccountAliases, organizations:DescribeOrganization, organizations:ListAccounts Readable names for accounts. ListAccounts works only from the management account; elsewhere the id is used.
OptionalAccountServices CloudTrail, GuardDuty, Config, CloudWatch, KMS, ECR reads The account services page: counts and status, never contents.
OptionalApiGateway apigateway:GET on /restapis, /apis, /domainnames and /vpclinks REST, HTTP and WebSocket APIs, stages, custom domains and VPC links.
DenyApiGatewaySecrets Explicit Deny of apigateway:GET on /apikeys and /clientcertificates A guard: the role can never read API key values or client certificates, even if another policy allows it.
OptionalCompute Auto Scaling, ECS, EKS, Lambda reads Clusters, services, node groups, Fargate profiles and functions with a VPC configuration. lambda:GetPolicy reveals cross-account callers.
OptionalDataStores RDS, ElastiCache, EFS, S3 bucket settings, DynamoDB ListTables Databases, caches and file systems with their subnets. S3 and DynamoDB are summarised in one node each.
OptionalEdge ACM, CloudFront, Shield, WAFv2 reads Certificates, distributions, web ACLs and Shield Advanced protections.
OptionalRoute53Records route53:ListResourceRecordSets, ListTagsForResources Record sets, so DNS names can be linked to the load balancers, distributions and APIs they point to.

Some statements list actions that no collector calls in 0.1.0: the ones marked above, and in OptionalEc2Network the Client VPN, IPAM, flow log, Elastic IP, DHCP options, security group rule, Transit Gateway Connect and endpoint service permission reads. They stay in the policy as published. If you want the tightest role, remove them; nothing in the scan depends on them. Coverage lists every call that is actually made.

Service layer actions in detail

Every action below is read-only and is called by the collector named in the second column. A collector that is denied one of them records a gap for that call and keeps the rest of its scan. The policy grants them in the Optional* statements.

Action Collector (collectors/) Why it is needed
route53:ListResourceRecordSets route53.py Route53Collector Records of each hosted zone, so DNS names can be linked to their targets.
route53:ListTagsForResources route53.py Tags of the hosted zones, read in batches, for their names.
cloudfront:ListDistributions cloudfront.py CloudFrontCollector Distributions, read once in the home region because CloudFront is global.
cloudfront:ListTagsForResource cloudfront.py Name tag of each distribution.
wafv2:ListWebACLs wafv2.py WafV2Collector Web ACLs in CLOUDFRONT scope (home region) and in REGIONAL scope.
wafv2:GetWebACL wafv2.py Default action and rule count of each web ACL.
wafv2:ListResourcesForWebACL wafv2.py Load balancers, API stages and other resources each web ACL protects.
wafv2:GetLoggingConfiguration wafv2.py Whether a web ACL logs, and where.
acm:ListCertificates acm.py AcmCollector Certificates of each region.
acm:DescribeCertificate acm.py Domain names, status and the resources that use each certificate.
acm:ListTagsForCertificate acm.py Certificate tags, for the name.
shield:GetSubscriptionState shield.py ShieldCollector Whether Shield Advanced is subscribed. No node is written when the call is denied.
shield:ListProtections shield.py Resources under Shield Advanced protection.
elasticloadbalancing:DescribeRules targets.py TargetGroupCollector Listener rules that forward to a target group.
ec2:DescribeInstances ec2_instances.py InstanceCollector Instances, grouped by Auto Scaling group or Name tag, with their subnets.
autoscaling:DescribeAutoScalingGroups ec2_instances.py Auto Scaling groups that own the instances.
ecs:ListClusters, ecs:DescribeClusters ecs.py EcsCollector ECS clusters.
ecs:ListServices, ecs:DescribeServices ecs.py ECS services, with their subnets and load balancers.
eks:ListClusters, eks:DescribeCluster eks.py EksCollector EKS clusters, with their endpoint and VPC configuration.
eks:ListNodegroups, eks:DescribeNodegroup eks.py Managed node groups and their subnets.
eks:ListFargateProfiles eks.py Fargate profiles and their subnets.
lambda:ListFunctions lambda_fn.py LambdaCollector Functions, with their VPC configuration.
lambda:GetPolicy lambda_fn.py Resource policy of each function, which reveals cross-account callers.
apigateway:GET apigw.py ApiGatewayCollector Every REST (v1) and HTTP/WebSocket (v2) read: APIs, stages, resources, domain names, mappings and VPC links. API Gateway reads are all GET requests, so one action covers both versions. Scoped to /restapis, /apis, /domainnames and /vpclinks (and their children); /apikeys and /clientcertificates are explicitly denied, so the auditor can never read key values or certificates.
rds:DescribeDBInstances, rds:DescribeDBClusters rds.py RdsCollector DB instances and Aurora clusters, with their members.
rds:DescribeDBSubnetGroups rds.py VPC and subnets of each DB instance or cluster.
elasticache:DescribeReplicationGroups, elasticache:DescribeCacheClusters elasticache.py ElastiCacheCollector Replication groups and the clusters outside them.
elasticache:DescribeCacheSubnetGroups elasticache.py VPC and subnets of each cache.
elasticfilesystem:DescribeFileSystems efs.py EfsCollector EFS file systems.
elasticfilesystem:DescribeMountTargets efs.py Mount targets, with their subnets.
elasticfilesystem:DescribeMountTargetSecurityGroups efs.py Security groups of each mount target.
s3:ListAllMyBuckets s3.py S3Collector Bucket names, summarised in one node.
s3:GetBucketLocation s3.py Region of each bucket.
s3:GetBucketWebsite s3.py Whether a bucket serves a static website.
s3:GetBucketPublicAccessBlock s3.py Whether public access is blocked on a bucket.
dynamodb:ListTables dynamodb.py DynamoDbCollector Table count and names, summarised in one node.
cloudtrail:DescribeTrails account_services.py CloudTrailCollector Trails, read in the home region only. Shadow trails are deduplicated.
cloudtrail:GetTrailStatus account_services.py Whether each trail is logging.
guardduty:ListDetectors account_services.py GuardDutyCollector Detector of each region.
guardduty:GetDetector account_services.py Detector status.
config:DescribeConfigurationRecorders account_services.py ConfigCollector Configuration recorders and what they record.
config:DescribeConfigurationRecorderStatus account_services.py Whether each recorder is recording.
config:DescribeDeliveryChannels account_services.py Delivery channels of the recorders.
logs:DescribeLogGroups account_services.py CloudWatchCollector Log group count.
cloudwatch:DescribeAlarms account_services.py Metric and composite alarm count.
kms:ListKeys, kms:ListAliases, kms:DescribeKey account_services.py KmsCollector Customer managed keys (AWS managed keys are excluded). Pending deletion keys are not counted.
ecr:DescribeRepositories account_services.py EcrCollector Repository count.

An auditor role for cross-account use

The pattern is the one that Prowler, Steampipe, CloudQuery and cartography use: a read-only role in every target account, and a trusted identity that assumes it. You run the tool from the identity (a laptop profile, an SSO role, a CI job, an ECS task or an EKS pod) and give it the role ARN with --role-arn.

Trust policy of the role in the target account (111122223333), trusting the account that runs the tool (999988887777) and asking for an ExternalId:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::999988887777:root" },
      "Action": "sts:AssumeRole",
      "Condition": { "StringEquals": { "sts:ExternalId": "<external-id>" } }
    }
  ]
}

Choose the principal as narrowly as you can. :root trusts any identity of that account that its own IAM policies allow to assume the role; a role ARN such as arn:aws:iam::999988887777:role/vpcatlas-runner trusts that role only. To require MFA, add "Bool": { "aws:MultiFactorAuthPresent": "true" } to the condition and run with --mfa-serial and --mfa-token.

The ExternalId is a protection against the confused deputy problem when a third party assumes roles in your accounts. Inside one organization it is optional, but the tool supports it everywhere: --external-id, or the environment variable VPCATLAS_EXTERNAL_ID. One ExternalId is used for every role of a run.

CloudFormation

AWSTemplateFormatVersion: "2010-09-09"
Description: Read-only auditor role for vpcatlas

Parameters:
  AuditorAccountId:
    Type: String
    AllowedPattern: '^\d{12}$'
    Description: Account that runs vpcatlas
  ExternalId:
    Type: String
    NoEcho: true
    MinLength: 8

Resources:
  AtlasAuditor:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AtlasAuditor
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub arn:${AWS::Partition}:iam::${AuditorAccountId}:root
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref ExternalId
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess

Outputs:
  RoleArn:
    Value: !GetAtt AtlasAuditor.Arn

This version uses ReadOnlyAccess. To use the minimal policy, delete ManagedPolicyArns and add a Policies entry (PolicyName: vpcatlas-auditor) whose PolicyDocument is the output of vpcatlas iam-policy, written as YAML. Keep it inline: as a stand-alone managed policy it is over the size quota (see above).

Terraform

variable "auditor_account_id" {
  type = string
}

variable "external_id" {
  type      = string
  sensitive = true
}

data "aws_partition" "current" {}

resource "aws_iam_role" "atlas_auditor" {
  name                 = "AtlasAuditor"
  max_session_duration = 3600

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "arn:${data.aws_partition.current.partition}:iam::${var.auditor_account_id}:root" }
      Action    = "sts:AssumeRole"
      Condition = { StringEquals = { "sts:ExternalId" = var.external_id } }
    }]
  })
}

# The minimal policy, saved with: vpcatlas iam-policy > auditor-policy.json
resource "aws_iam_role_policy" "atlas_auditor" {
  name   = "vpcatlas-auditor"
  role   = aws_iam_role.atlas_auditor.id
  policy = file("${path.module}/auditor-policy.json")
}

# Or, instead of the inline policy, the AWS managed one:
# resource "aws_iam_role_policy_attachment" "read_only" {
#   role       = aws_iam_role.atlas_auditor.name
#   policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/ReadOnlyAccess"
# }

output "role_arn" {
  value = aws_iam_role.atlas_auditor.arn
}

Apply it once per target account, with a provider for each.

The identity that assumes the role

The identity that runs the tool needs permission to assume the roles. Nothing else:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": [
        "arn:aws:iam::111122223333:role/AtlasAuditor",
        "arn:aws:iam::444455556666:role/AtlasAuditor"
      ]
    }
  ]
}

Then run:

vpcatlas discover --role-arn arn:aws:iam::111122223333:role/AtlasAuditor --external-id <external-id> \
  --regions eu-west-1 --output-dir ./out

Rules:

  • The session name is vpcatlas unless you change it with --role-session-name (or VPCATLAS_ROLE_SESSION_NAME), so CloudTrail in the target account shows who called. Names are 2 to 64 characters from letters, digits and + = , . @ _ -.
  • --session-duration (or VPCATLAS_SESSION_DURATION) is 900 to 43200 seconds, default 3600. It cannot exceed the MaxSessionDuration of the role, and AWS caps a session at one hour when the role is assumed by another assumed role (role chaining). The credentials refresh by themselves before they expire, except with an MFA code, which cannot be reused.
  • MFA uses --mfa-serial and --mfa-token, which go together. vpcatlas serve does not take them, because a one-time code cannot keep a long-running dashboard alive.
  • Keep the role read-only. The tool does not need write permissions in any account.
  • For the dashboard, the task, pod or function role needs sts:AssumeRole on the target roles, and the auditor policy only if it also reads its own account.

How credentials reach the tool

vpcatlas reads credentials the way the AWS CLI does, through the standard boto3 chain. Secret keys are never command line flags, so they stay out of your shell history and the process list. Use whichever method you already have:

Method How
Access keys in the environment AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and, for temporary keys, AWS_SESSION_TOKEN.
Shared files ~/.aws/credentials and ~/.aws/config; pick a profile with --profile or AWS_PROFILE.
IAM Identity Center (SSO) profile aws sso login --profile audit, then --profile audit.
A profile that assumes a role A profile with role_arn and source_profile (or credential_source) works as it is, without --role-arn.
Assume a role from the command line --role-arn, with --external-id, --mfa-serial, --mfa-token, --session-duration, --role-session-name.
credential_process Any helper configured in the profile.
EC2 instance role Found through the instance metadata service. No option needed.
ECS and Fargate task role Found through the container credentials endpoint. No option needed.
EKS pod role IRSA (web identity token) or EKS Pod Identity. No option needed.
Lambda execution role Found in the environment of the function. No option needed.

Repeat --profile or --role-arn to scan several accounts in one run; see Cross-account. The dashboard reads the same options from VPCATLAS_PROFILE, VPCATLAS_ROLE_ARN, VPCATLAS_EXTERNAL_ID, VPCATLAS_SESSION_DURATION and VPCATLAS_ROLE_SESSION_NAME (comma separated lists for the first two).

Inside Docker, a credential_process profile does not work, because its helper is not in the image, and an SSO profile works only while its cached token is valid. Export short-lived keys into the container instead; see Troubleshooting.