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
- The minimal auditor policy
- What each statement is for
- Service layer actions in detail
- An auditor role for cross-account use
- How 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
vpcatlasunless you change it with--role-session-name(orVPCATLAS_ROLE_SESSION_NAME), so CloudTrail in the target account shows who called. Names are 2 to 64 characters from letters, digits and+ = , . @ _ -. --session-duration(orVPCATLAS_SESSION_DURATION) is 900 to 43200 seconds, default 3600. It cannot exceed theMaxSessionDurationof 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-serialand--mfa-token, which go together.vpcatlas servedoes 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:AssumeRoleon 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.