Vishcore Inc. vishcore. Start discovery

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)
  1. 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.

    RoleName and PolicyName default to VishcoreReadOnlyDiscovery and VishcoreReadOnlyDiscoveryPolicy; you can rename them. VishcorePrincipalArn is the one Vishcore scanner role allowed in. We send you its value, shaped like arn: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.

    ExternalId must be at least 16 characters, and NoEcho masks it wherever CloudFormation shows the stack's parameters. ApprovedRegions (default ca-central-1) limits the regional reads to the Regions you list.

  2. 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 VishcoreReadOnlyDiscoveryPolicy

    Part 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:ExternalId condition 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.

  3. 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, and inspector2: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.

  4. 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:POST and iam:PassRole, are refused because no allow names them. The allow lists name only reads, so discovery cannot change production.

  5. 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 needs secretsmanager:GetSecretValue on each secret, which this statement denies.

    The next four cover Lambda and ECS configuration: lambda:ListFunctions, lambda:GetFunctionConfiguration, and ecs:DescribeTaskDefinition can return environment values, and ecs:ListTaskDefinitions, which returns only task-definition ARNs, is denied with them as the first step of that path. The last two, config:SelectResourceConfig and securityhub: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.

  6. 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 by Resource path instead.

    This statement denies apigateway:GET on /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.

  7. 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 ApprovedRegions
    See 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:GetReplicationConfiguration

    AllowRegionalDiscoveryMetadata (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:GetEventSelectors

    Part 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 the aws:RequestedRegion condition. 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.

  8. 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 ApprovedRegions

    Part 8

    API Gateway reads, only on the scanner's paths.

    This statement allows apigateway:GET on 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:RequestedRegion condition keeps it to the Regions in ApprovedRegions. 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.

  9. 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 RoleArn output to Vishcore through the approved secure channel, as its description says. PolicyArn names the policy.

    The Teardown output 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.

  1. 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 text

    Expect

    arn:aws:iam::<CLIENT_ACCOUNT_ID>:role/VishcoreReadOnlyDiscovery
  2. 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>"
            }
        ]
    ]
  3. 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 text

    Expect

    s3:PutObject                     explicitDeny
    ec2:TerminateInstances           explicitDeny
    secretsmanager:GetSecretValue    explicitDeny
    ssm:GetParameter                 explicitDeny
    lambda:GetFunctionConfiguration  explicitDeny
    config:SelectResourceConfig      explicitDeny
    securityhub:GetFindings          explicitDeny
  4. 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 text

    Expect

    s3:GetObject   implicitDeny
    kms:Decrypt    implicitDeny
    iam:ListUsers  allowed
  5. 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 text

    Expect

    ec2:DescribeInstances  allowed

    Run

    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 text

    Expect

    ec2:DescribeInstances  implicitDeny
  6. 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 text

    Expect

    apigateway:GET  explicitDeny

    Run

    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 text

    Expect

    apigateway:GET  allowed
  7. 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 VishcoreReadOnlyDiscovery

    Expect

    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
Start discovery