role template
The read‑only role, explained line by line.
You create this role in your own AWS account from our CloudFormation template. Here is every part of it: who it trusts, what it allows, what it explicitly denies, and how to check each part yourself.
the template
Nine parts, in plain English.
Each excerpt is copied line for line from the template. Its allow statements name exactly the actions in our scanner allowlist's default set, and a test in the scanner's codebase fails if the two ever differ. IAM refuses any call they do not name.
Download the template (YAML)-
Parameters lines 4–25 · shown in full Parameters: RoleName: Type: String Default: VishcoreReadOnlyDiscovery Description: Name for the client-owned read-only discovery role. PolicyName: Type: String Default: VishcoreReadOnlyDiscoveryPolicy Description: Name for the client-owned read-only scanner policy. VishcorePrincipalArn: Type: String Description: AWS principal ARN approved to assume this role. AllowedPattern: "^arn:aws:iam::[0-9]{12}:(role|user)/[A-Za-z0-9+=,.@_/-]+$" ExternalId: Type: String NoEcho: true MinLength: 16 Description: External ID generated for this engagement and shared through the approved secure channel. ApprovedRegions: Type: CommaDelimitedList Default: ca-central-1 Description: Comma-separated AWS Regions approved for regional scanner metadata reads; include the enabled-Region discovery scope if used.Part 1
You fill in five values.
RoleNameandPolicyNamedefault toVishcoreReadOnlyDiscoveryandVishcoreReadOnlyDiscoveryPolicy; you can rename them.VishcorePrincipalArnis the one Vishcore scanner role allowed in. We send you its value, shaped likearn:aws:iam::<VISHCORE_ACCOUNT_ID>:role/<VISHCORE_SCANNER_ROLE_NAME>, and its pattern accepts only a role or user ARN, never a whole account or a wildcard.ExternalIdmust be at least 16 characters, andNoEchomasks it wherever CloudFormation shows the stack's parameters.ApprovedRegions(defaultca-central-1) limits the regional reads to the Regions you list. -
Resources: the role and its trust policy lines 28–44 · shown in full VishcoreReadOnlyDiscoveryRole: Type: AWS::IAM::Role Properties: RoleName: !Ref RoleName Description: Read-only Vishcore Cloud Control Scan discovery role. No remediation/write permissions. AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: AWS: !Ref VishcorePrincipalArn Action: sts:AssumeRole Condition: StringEquals: sts:ExternalId: !Ref ExternalId ManagedPolicyArns: - !Ref VishcoreReadOnlyDiscoveryPolicyPart 2
It trusts one role, and only with your External ID.
The trust policy has one statement and one principal: the ARN you entered for
VishcorePrincipalArn. No other identity, inside Vishcore or outside it, can assume the role.The
sts:ExternalIdcondition refuses even that role unless it also presents the External ID for your engagement. Knowing your role ARN is not enough to reach your account through Vishcore.The role carries one policy, the managed policy below, and nothing else.
-
The policy's own comment lines 53–60 · shown in full # Residual read exposure the denies do not remove, disclosed on purpose: # - cloudfront:GetDistributionConfig and cloudfront:ListDistributions both return # origin custom header values; the scanner keeps aliases and booleans only. # - apigateway:GET on stage collections returns stage variables; the scanner keeps # logging and tracing booleans only. API key values are denied below. # - inspector2:ListFindings returns finding metadata (resource identifiers, package # and CVE names) with no environment or secret fields; the scanner keeps # severity counts and age buckets only.Part 3
The template names what its denies leave readable.
Before its first statement, the policy says in a comment that four allowed reads return more than the scanner keeps:
cloudfront:GetDistributionConfig,cloudfront:ListDistributions,apigateway:GET, andinspector2:ListFindings. We put it in the template so the disclosure travels with the role.Both CloudFront reads return origin custom header values, which are often shared secrets between CloudFront and your origin, and AWS has no CloudFront read that leaves them out. API Gateway stage lists return stage variables. Inspector findings carry finding metadata such as resource identifiers and package and CVE names, with no environment or secret fields.
The scanner keeps aliases and booleans from CloudFront, logging and tracing booleans from API Gateway stages, and severity counts and age buckets from Inspector. It drops the rest before anything is stored.
-
Policy statement 1 of 6 lines 62–97 · shown in full - Sid: DenyMutatingOperations Effect: Deny Action: - "*:Create*" - "*:Put*" - "*:Delete*" - "*:Modify*" - "*:Update*" - "*:Start*" - "*:Stop*" - "*:Terminate*" - "*:Run*" - "*:Register*" - "*:Deregister*" - "*:Associate*" - "*:Disassociate*" - "*:Attach*" - "*:Detach*" - "*:Authorize*" - "*:Revoke*" - "*:Tag*" - "*:Untag*" - "*:Enable*" - "*:Disable*" - "*:Schedule*" - "*:Cancel*" - "*:Reboot*" - "*:Restore*" - "*:BatchWrite*" - "*:Invoke*" - "*:Publish*" - "*:Send*" - "*:AddTags*" - "*:RemoveTags*" - "*:Set*" Resource: "*"Part 4
Write patterns, denied on every service.
This statement denies 32 write patterns on every AWS service, such as
*:Create*,*:Put*, and*:Delete*: each one matches every call whose name starts with that verb.In IAM an explicit deny always wins over an allow, so an allow added later by mistake could not reopen a call these patterns match. Writes whose names start with other verbs, such as
apigateway:POSTandiam:PassRole, are refused because no allow names them. The allow lists name only reads, so discovery cannot change production. -
Policy statement 2 of 6 lines 98–112 · shown in full - Sid: DenySecretValueAndDeepEnvironmentReads Effect: Deny Action: - secretsmanager:GetSecretValue - ssm:GetParameter - ssm:GetParameters - ssm:GetParametersByPath - ssm:GetParameterHistory - lambda:ListFunctions - lambda:GetFunctionConfiguration - ecs:ListTaskDefinitions - ecs:DescribeTaskDefinition - config:SelectResourceConfig - securityhub:GetFindings Resource: "*"Part 5
Secret-value calls, and reads that can return environment values, are explicitly denied.
The first five actions are the main calls that return secret values from Secrets Manager and SSM Parameter Store, parameter history included. Other reads that can return those values, such as
secretsmanager:BatchGetSecretValue, are refused because no allow names them; that one also needssecretsmanager:GetSecretValueon each secret, which this statement denies.The next four cover Lambda and ECS configuration:
lambda:ListFunctions,lambda:GetFunctionConfiguration, andecs:DescribeTaskDefinitioncan return environment values, andecs:ListTaskDefinitions, which returns only task-definition ARNs, is denied with them as the first step of that path. The last two,config:SelectResourceConfigandsecurityhub:GetFindings, run AWS Config advanced queries and read Security Hub findings, which can return that same recorded configuration. IAM cannot narrow these reads to leave that content out, so the template denies them all.Scanner tests that need these reads, such as the Lambda environment KMS key test and the Security Hub failed-finding test, report “Not tested: read-only boundary” under this role.
The role also grants no KMS decrypt or key-material access: no secret values read from Secrets Manager, SSM, or KMS.
-
Policy statement 3 of 6 lines 113–121 · shown in full - Sid: DenyApiGatewayKeyValueReads Effect: Deny Action: - apigateway:GET Resource: - arn:aws:apigateway:*::/apikeys - arn:aws:apigateway:*::/apikeys/* - arn:aws:apigateway:*::/usageplans/*/keys - arn:aws:apigateway:*::/usageplans/*/keys/*Part 6
The API key paths are explicitly denied.
API Gateway has one IAM action for every management read,
apigateway:GET, so IAM cannot grant its reads one call at a time. The template scopes it byResourcepath instead.This statement denies
apigateway:GETon/apikeys,/apikeys/*,/usageplans/*/keys, and/usageplans/*/keys/*, the API key and usage-plan key paths. As an explicit deny, it holds on those paths even if an allow is widened later. No allow grants them today either: the allow statement below names six other paths. -
Policy statements 4 and 5 of 6 lines 122–263 · shortened - Sid: AllowGlobalDiscoveryMetadata Effect: Allow Action: - sts:GetCallerIdentity - iam:GetAccountPasswordPolicy - iam:GetAccountSummary - iam:GetGroupPolicy - iam:GetLoginProfile # … 31 more of 36 actions in this statement Resource: "*" - Sid: AllowRegionalDiscoveryMetadata Effect: Allow Action: - acm:DescribeCertificate - acm:ListCertificates - cloudtrail:DescribeTrails - access-analyzer:ListAnalyzers - access-analyzer:ListFindings # … 90 more of 95 actions in this statement Resource: "*" Condition: StringEquals: aws:RequestedRegion: !Ref ApprovedRegionsSee all 131 actions in these two statements
AllowGlobalDiscoveryMetadata (36)
sts:GetCallerIdentity iam:GetAccountPasswordPolicy iam:GetAccountSummary iam:GetGroupPolicy iam:GetLoginProfile iam:GetPolicy iam:GetPolicyVersion iam:GetUserPolicy iam:ListAccessKeys iam:ListAttachedGroupPolicies iam:ListAttachedUserPolicies iam:ListGroupPolicies iam:ListGroupsForUser iam:ListMFADevices iam:ListUserPolicies iam:ListUsers iam:GetAccessKeyLastUsed iam:ListServerCertificates cloudfront:GetDistributionConfig cloudfront:ListDistributions route53:GetDNSSEC route53:ListHostedZones s3:ListAllMyBuckets s3:GetBucketAcl s3:GetBucketLogging s3:GetBucketPolicy s3:GetBucketPolicyStatus s3:GetBucketTagging s3:GetBucketVersioning s3:GetEncryptionConfiguration s3:GetLifecycleConfiguration s3:GetBucketPublicAccessBlock s3control:GetPublicAccessBlock organizations:DescribeOrganization organizations:ListPolicies s3:GetReplicationConfigurationAllowRegionalDiscoveryMetadata (95)
acm:DescribeCertificate acm:ListCertificates cloudtrail:DescribeTrails access-analyzer:ListAnalyzers access-analyzer:ListFindings backup:ListBackupPlans backup:ListBackupVaults backup:ListProtectedResources backup:ListRecoveryPointsByBackupVault cloudwatch:DescribeAlarms cloudwatch:DescribeAlarmsForMetric config:DescribeConfigurationRecorders config:DescribeConfigurationRecorderStatus config:DescribeDeliveryChannels config:ListDiscoveredResources cognito-idp:DescribeUserPool cognito-idp:ListUserPools dynamodb:DescribeContinuousBackups dynamodb:DescribeTable dynamodb:ListTagsOfResource dynamodb:ListTables ec2:DescribeFlowLogs ec2:DescribeImageAttribute ec2:DescribeImages ec2:DescribeInstances ec2:DescribeNetworkAcls ec2:DescribeRegions ec2:DescribeRouteTables ec2:DescribeSecurityGroups ec2:DescribeSnapshotAttribute ec2:DescribeSnapshots ec2:DescribeSubnets ec2:DescribeVolumes ec2:DescribeVpcs ec2:GetEbsEncryptionByDefault ecr:DescribeRepositories ecr:DescribeImages ecr:GetRegistryScanningConfiguration ecr:GetLifecyclePolicy ecr:GetRepositoryPolicy ecr:ListTagsForResource ecs:DescribeClusters ecs:DescribeServices ecs:ListClusters ecs:ListServices ecs:ListTagsForResource elasticache:DescribeCacheClusters elasticache:DescribeReplicationGroups elasticache:DescribeSnapshots elasticache:ListTagsForResource eks:DescribeCluster eks:ListClusters elasticfilesystem:DescribeBackupPolicy elasticfilesystem:DescribeFileSystems elasticfilesystem:ListTagsForResource elasticloadbalancing:DescribeListeners elasticloadbalancing:DescribeLoadBalancerAttributes elasticloadbalancing:DescribeLoadBalancers elasticloadbalancing:DescribeRules elasticloadbalancing:DescribeTags events:ListRules events:ListTagsForResource events:ListTargetsByRule guardduty:ListDetectors inspector2:BatchGetAccountStatus inspector2:ListFindings kms:DescribeKey kms:GetKeyRotationStatus kms:ListKeys logs:DescribeLogGroups lambda:GetPolicy lambda:ListFunctionUrlConfigs rds:DescribeDBClusterParameters rds:DescribeDBClusterSnapshotAttributes rds:DescribeDBClusterSnapshots rds:DescribeDBClusters rds:DescribeDBInstances rds:DescribeDBParameters rds:DescribeDBSnapshotAttributes rds:DescribeDBSnapshots rds:ListTagsForResource secretsmanager:ListSecrets securityhub:DescribeHub securityhub:GetEnabledStandards sns:GetTopicAttributes sns:ListTagsForResource sns:ListTopics sqs:GetQueueAttributes sqs:ListQueueTags sqs:ListQueues ssm:DescribeInstanceInformation wafv2:GetWebACLForResource elasticloadbalancing:DescribeSSLPolicies cloudtrail:GetTrailStatus cloudtrail:GetEventSelectorsPart 7
Only configuration metadata, and only where you said.
These two statements name 131 read calls one by one, with no wildcards: 36 account-wide and 95 regional. All but one are Describe, Get, or List calls; that one is the read‑only
inspector2:BatchGetAccountStatus.The regional reads work only in the Regions you put in
ApprovedRegions, through theaws:RequestedRegioncondition. Nothing allows data-plane reads, so S3 objects, table items, and log events fall to an implicit deny.Some allowed configuration reads, such as CloudFront distribution settings, can include sensitive fields; the scanner reduces those responses to aliases and booleans before anything is stored.
-
Policy statement 6 of 6 lines 264–277 · shown in full - Sid: AllowApiGatewayReadPaths Effect: Allow Action: - apigateway:GET Resource: - arn:aws:apigateway:*::/restapis - arn:aws:apigateway:*::/restapis/*/stages - arn:aws:apigateway:*::/restapis/*/authorizers - arn:aws:apigateway:*::/apis - arn:aws:apigateway:*::/apis/*/stages - arn:aws:apigateway:*::/apis/*/authorizers Condition: StringEquals: aws:RequestedRegion: !Ref ApprovedRegionsPart 8
API Gateway reads, only on the scanner's paths.
This statement allows
apigateway:GETon six paths only:/restapis,/restapis/*/stages,/restapis/*/authorizers,/apis,/apis/*/stages, and/apis/*/authorizers. Those are the two API lists and each API's stages and authorizers, the calls the scanner makes.The same
aws:RequestedRegioncondition keeps it to the Regions inApprovedRegions. Any other API Gateway path, such as an API's integrations or deployments, falls to an implicit deny.Stage lists include stage variables; the scanner keeps only logging and tracing booleans from them.
-
Outputs lines 279–288 · shown in full Outputs: RoleArn: Description: Share this role ARN with Vishcore through the approved secure channel. Value: !GetAtt VishcoreReadOnlyDiscoveryRole.Arn PolicyArn: Description: Client-owned read-only discovery policy ARN. Value: !Ref VishcoreReadOnlyDiscoveryPolicy Teardown: Description: Delete this CloudFormation stack to remove the role and policy after the engagement. Value: Delete the stack after the scan engagement closes or when access is no longer approved.Part 9
What you send us, and how you end it.
Send the
RoleArnoutput to Vishcore through the approved secure channel, as its description says.PolicyArnnames the policy.The
Teardownoutput is the exit plan: delete the stack, and the role and policy are gone, and with them our access.
Copied from client-read-only-discovery-role.cloudformation.yaml, with each excerpt shifted left; a shortened excerpt marks its cut with a # … comment. The template takes account IDs and principals as parameters and contains none.
verify it yourself
Check the role from your own account.
Plain AWS CLI commands you run after you deploy the stack. Steps 1 to 6 only read your configuration; step 7 removes the role. You need IAM permission to call them, including iam:GetRole and iam:SimulatePrincipalPolicy.
-
Step 1
Confirm the stack and its outputs
The stack should be complete (UPDATE_COMPLETE after an update) and list its three outputs, in any order. The second command prints the role ARN that the next steps call <role-arn>.
Run
aws cloudformation describe-stacks \ --stack-name <stack-name> \ --query "Stacks[0].[StackStatus, Outputs[].OutputKey]"Expect
[ "CREATE_COMPLETE", [ "RoleArn", "PolicyArn", "Teardown" ] ]Run
aws cloudformation describe-stacks \ --stack-name <stack-name> \ --query "Stacks[0].Outputs[?OutputKey=='RoleArn'].OutputValue" \ --output textExpect
arn:aws:iam::<CLIENT_ACCOUNT_ID>:role/VishcoreReadOnlyDiscovery -
Step 2
Check who the role trusts
One statement with one principal, the Vishcore scanner role, and the External ID condition.
Run
aws iam get-role \ --role-name VishcoreReadOnlyDiscovery \ --query "Role.AssumeRolePolicyDocument.Statement[].[Principal.AWS, Action, Condition.StringEquals]"Expect
[ [ "arn:aws:iam::<VISHCORE_ACCOUNT_ID>:role/<VISHCORE_SCANNER_ROLE_NAME>", "sts:AssumeRole", { "sts:ExternalId": "<EXTERNAL_ID>" } ] ] -
Step 3
Prove writes, secret values, and environment reads are denied
IAM's policy simulator evaluates the role's own policy without calling any service. Every action here should come back as an explicit deny: writes, Secrets Manager and SSM values, and the Lambda, AWS Config query, and Security Hub reads that can return environment values.
Run
aws iam simulate-principal-policy \ --policy-source-arn <role-arn> \ --action-names s3:PutObject \ ec2:TerminateInstances \ secretsmanager:GetSecretValue \ ssm:GetParameter \ lambda:GetFunctionConfiguration \ config:SelectResourceConfig \ securityhub:GetFindings \ --query "EvaluationResults[].[EvalActionName,EvalDecision]" \ --output textExpect
s3:PutObject explicitDeny ec2:TerminateInstances explicitDeny secretsmanager:GetSecretValue explicitDeny ssm:GetParameter explicitDeny lambda:GetFunctionConfiguration explicitDeny config:SelectResourceConfig explicitDeny securityhub:GetFindings explicitDeny -
Step 4
Prove data and keys were never granted
Object reads and KMS decrypt are not allowed anywhere, so they fall to an implicit deny. A metadata read comes back allowed, which shows the simulation works; a service control policy in your organization can still deny it.
Run
aws iam simulate-principal-policy \ --policy-source-arn <role-arn> \ --action-names s3:GetObject \ kms:Decrypt \ iam:ListUsers \ --query "EvaluationResults[].[EvalActionName,EvalDecision]" \ --output textExpect
s3:GetObject implicitDeny kms:Decrypt implicitDeny iam:ListUsers allowed -
Step 5
Prove the regional reads stay in your Regions
Simulate a regional read in a Region you approved, then in one you did not. The template's default approved Region is ca-central-1.
Run
aws iam simulate-principal-policy \ --policy-source-arn <role-arn> \ --action-names ec2:DescribeInstances \ --context-entries "ContextKeyName=aws:RequestedRegion,ContextKeyValues=ca-central-1,ContextKeyType=string" \ --query "EvaluationResults[].[EvalActionName,EvalDecision]" \ --output textExpect
ec2:DescribeInstances allowedRun
aws iam simulate-principal-policy \ --policy-source-arn <role-arn> \ --action-names ec2:DescribeInstances \ --context-entries "ContextKeyName=aws:RequestedRegion,ContextKeyValues=us-east-1,ContextKeyType=string" \ --query "EvaluationResults[].[EvalActionName,EvalDecision]" \ --output textExpect
ec2:DescribeInstances implicitDeny -
Step 6
Prove API Gateway reads stop at the scanner's paths
API Gateway reads share one action, so each run names a path with --resource-arns. The API key path comes back as an explicit deny, even in your approved Region; the API list at /restapis, one of the scanner's paths, comes back allowed.
Run
aws iam simulate-principal-policy \ --policy-source-arn <role-arn> \ --action-names apigateway:GET \ --resource-arns arn:aws:apigateway:ca-central-1::/apikeys \ --context-entries "ContextKeyName=aws:RequestedRegion,ContextKeyValues=ca-central-1,ContextKeyType=string" \ --query "EvaluationResults[].[EvalActionName,EvalDecision]" \ --output textExpect
apigateway:GET explicitDenyRun
aws iam simulate-principal-policy \ --policy-source-arn <role-arn> \ --action-names apigateway:GET \ --resource-arns arn:aws:apigateway:ca-central-1::/restapis \ --context-entries "ContextKeyName=aws:RequestedRegion,ContextKeyValues=ca-central-1,ContextKeyType=string" \ --query "EvaluationResults[].[EvalActionName,EvalDecision]" \ --output textExpect
apigateway:GET allowed -
Step 7 · changes your account
Delete the stack when we finish
This removes the role and the policy, which ends our access. The last command then fails, because the role no longer exists.
Run
aws cloudformation delete-stack \ --stack-name <stack-name>Expect
(no output)Run
aws cloudformation wait stack-delete-complete \ --stack-name <stack-name>Expect
(no output once the stack is gone)Run
aws iam get-role \ --role-name VishcoreReadOnlyDiscoveryExpect
An error occurred (NoSuchEntity) when calling the GetRole operation: The role with name VishcoreReadOnlyDiscovery cannot be found.
Placeholders in angle brackets are yours to fill in. The CLI separates text output with tabs; this page aligns it for reading. The expected decisions are computed from the published template's policy alone, so a service control policy or permissions boundary in your account can only make them stricter.
Happy with the role? Start with a short note.
We confirm fit before any access. No secrets, account IDs, or access in the first message.
How the scan works