setup

Amazon Web Services (AWS)

tip

Set up with an AI agent Using Claude Code, Cursor, Codex CLI, or another coding agent? The subimage-setup:connect-aws skill walks the agent through every choice on this page. Paste this into your agent:

Use curl to download, read and follow: https://skills.subimage.io/plugins/subimage-setup/skills/connect-aws/SKILL.md

Or install the marketplace once with claude plugin marketplace add subimagesec/skills. Full instructions on Agent Skills.

Purpose

Inventories AWS resources and IAM relationships across one or more accounts. AWS is the foundation most other security analyses build on: connect every account you want to cover.

How It Works

Each account needs a read-only IAM role named SubImageScanRole that trusts the SubImage principal for your tenant. Every sync calls sts:AssumeRole on that role and reads the account. If one configured account is the AWS Organizations management account or a delegated administrator account, SubImage also uses it once per sync to map the organization, roots, OUs, and account placement before scanning per-account resources.

Deploy the role as an AWS Organizations StackSet so that:

  • every existing account receives the role automatically
  • new accounts created later pick it up with no extra work
important

Service-managed StackSets do not deploy to the organization's management account, even when you target the org root. To scan the management account, deploy the same template there as a standalone stack.

One-time steps

  1. Confirm the SubImage principal ARN. When you view these docs inside your authenticated tenant, SubImage auto-fills your tenant AWS account ID in the snippets below. The principal looks like:

    arn:aws:iam::<ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly
  2. Deploy the scanning role using CloudFormation (service-managed StackSet) or Terraform below.

  3. Target All AWS accounts and All current and future Regions.

  4. If you want to scan the management account, deploy the same template there as a standalone stack.

Use the SubImageScanRole template below unchanged. It already includes the EKS identity/OIDC read permissions needed by EKS rows in the Kubernetes module and read access to AWS-managed public SSM parameters used for Bottlerocket and EKS optimized AMI recommendations. If you deployed this role from an older copy of the template, update the existing StackSet or standalone stack so every scanned account receives the current inline policies.

Option A – CloudFormation

AWSTemplateFormatVersion: "2010-09-09"
Description: >
  IAM role used by SubImage to inventory this AWS account.

Resources:
  SubImageScanRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "SubImageScanRole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - "arn:aws:iam::<ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly"
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/SecurityAudit
      Policies:
        - PolicyName: AllowSSOResourceSpecificActions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "sso:DescribeAccountAssignmentCreationStatus"
                  - "sso:DescribeAccountAssignmentDeletionStatus"
                  - "sso:DescribeApplication"
                  - "sso:DescribeApplicationAssignment"
                  - "sso:DescribeApplicationProvider"
                  - "sso:DescribeInstance"
                  - "sso:DescribeInstanceAccessControlAttributeConfiguration"
                  - "sso:DescribePermissionSet"
                  - "sso:DescribePermissionSetProvisioningStatus"
                  - "sso:DescribeTrustedTokenIssuer"
                  - "sso:GetApplicationAccessScope"
                  - "sso:GetApplicationAssignmentConfiguration"
                  - "sso:GetApplicationAuthenticationMethod"
                  - "sso:GetApplicationGrant"
                  - "sso:GetInlinePolicyForPermissionSet"
                  - "sso:GetPermissionsBoundaryForPermissionSet"
                  - "sso:ListAccountAssignmentCreationStatus"
                  - "sso:ListAccountAssignmentDeletionStatus"
                  - "sso:ListAccountAssignments"
                  - "sso:ListAccountAssignmentsForPrincipal"
                  - "sso:ListAccountsForProvisionedPermissionSet"
                  - "sso:ListApplicationAccessScopes"
                  - "sso:ListApplicationAssignments"
                  - "sso:ListApplicationAssignmentsForPrincipal"
                  - "sso:ListApplicationAuthenticationMethods"
                  - "sso:ListApplicationGrants"
                  - "sso:ListApplicationProviders"
                  - "sso:ListCustomerManagedPolicyReferencesInPermissionSet"
                  - "sso:ListManagedPoliciesInPermissionSet"
                  - "sso:ListPermissionSetProvisioningStatus"
                  - "sso:ListPermissionSets"
                  - "sso:ListPermissionSetsProvisionedToAccount"
                  - "sso:ListTagsForResource"
                  - "sso:ListTrustedTokenIssuers"
                Resource:
                  - "arn:aws:sso::*:application/*/*"
                  - "arn:aws:sso::*:trustedTokenIssuer/*/*"
                  - "arn:aws:sso:::account/*"
                  - "arn:aws:sso:::instance/*"
                  - "arn:aws:sso:::permissionSet/*/*"
                  - "arn:aws:sso::aws:applicationProvider/*"
        - PolicyName: AllowSSOGlobalActions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "sso:DescribeDirectories"
                  - "sso:DescribeRegisteredRegions"
                  - "sso:DescribeTrusts"
                  - "sso:GetApplicationInstance"
                  - "sso:GetApplicationTemplate"
                  - "sso:GetManagedApplicationInstance"
                  - "sso:GetMfaDeviceManagementForDirectory"
                  - "sso:GetPermissionSet"
                  - "sso:GetProfile"
                  - "sso:GetSharedSsoConfiguration"
                  - "sso:GetSsoConfiguration"
                  - "sso:GetSSOStatus"
                  - "sso:GetTrust"
                  - "sso:ListApplicationInstanceCertificates"
                  - "sso:ListApplicationInstances"
                  - "sso:ListApplications"
                  - "sso:ListApplicationTemplates"
                  - "sso:ListDirectoryAssociations"
                  - "sso:ListInstances"
                  - "sso:ListProfileAssociations"
                  - "sso:ListProfiles"
                Resource: "arn:aws:sso::*"
        - PolicyName: AllowEKSIdentityRead
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  # SecurityAudit already covers eks:DescribeCluster and
                  # eks:ListAccessEntries. These are the EKS-specific additions
                  # SubImage needs for identity metadata enrichment.
                  - "eks:DescribeAccessEntry"
                  - "eks:ListIdentityProviderConfigs"
                  - "eks:DescribeIdentityProviderConfig"
                Resource: "*"
        - PolicyName: AllowPublicSSMParameterRead
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: "ssm:GetParametersByPath"
                Resource: !Sub "arn:${AWS::Partition}:ssm:*::parameter/aws/service/*"
        - PolicyName: AllowECRRead
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "ecr:GetAuthorizationToken"
                  - "ecr:BatchCheckLayerAvailability"
                  - "ecr:GetDownloadUrlForLayer"
                  - "ecr:BatchGetImage"
                Resource: "*"
        - PolicyName: AllowGlueInspectorRead
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  # Not covered by SecurityAudit; needed by the Cartography
                  # AWS glue and inspector sync stages.
                  - "glue:GetConnections"
                  - "inspector2:ListMembers"
                Resource: "*"

Outputs:
  SubImageScanRoleArn:
    Description: ARN of the created SubImage scanning IAM Role.
    Value: !GetAtt SubImageScanRole.Arn

Option B – Terraform

resource "aws_iam_role" "subimage_scan_role" {
  name = "SubImageScanRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        AWS = [
          "arn:aws:iam::<ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly",
        ]
      }
      Action = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "attach_security_audit" {
  role       = aws_iam_role.subimage_scan_role.name
  policy_arn = "arn:aws:iam::aws:policy/SecurityAudit"
}

resource "aws_iam_role_policy" "allow_sso_resource_specific_actions" {
  name = "AllowSSOResourceSpecificActions"
  role = aws_iam_role.subimage_scan_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "sso:DescribeAccountAssignmentCreationStatus",
        "sso:DescribeAccountAssignmentDeletionStatus",
        "sso:DescribeApplication",
        "sso:DescribeApplicationAssignment",
        "sso:DescribeApplicationProvider",
        "sso:DescribeInstance",
        "sso:DescribeInstanceAccessControlAttributeConfiguration",
        "sso:DescribePermissionSet",
        "sso:DescribePermissionSetProvisioningStatus",
        "sso:DescribeTrustedTokenIssuer",
        "sso:GetApplicationAccessScope",
        "sso:GetApplicationAssignmentConfiguration",
        "sso:GetApplicationAuthenticationMethod",
        "sso:GetApplicationGrant",
        "sso:GetInlinePolicyForPermissionSet",
        "sso:GetPermissionsBoundaryForPermissionSet",
        "sso:ListAccountAssignmentCreationStatus",
        "sso:ListAccountAssignmentDeletionStatus",
        "sso:ListAccountAssignments",
        "sso:ListAccountAssignmentsForPrincipal",
        "sso:ListAccountsForProvisionedPermissionSet",
        "sso:ListApplicationAccessScopes",
        "sso:ListApplicationAssignments",
        "sso:ListApplicationAssignmentsForPrincipal",
        "sso:ListApplicationAuthenticationMethods",
        "sso:ListApplicationGrants",
        "sso:ListApplicationProviders",
        "sso:ListCustomerManagedPolicyReferencesInPermissionSet",
        "sso:ListManagedPoliciesInPermissionSet",
        "sso:ListPermissionSetProvisioningStatus",
        "sso:ListPermissionSets",
        "sso:ListPermissionSetsProvisionedToAccount",
        "sso:ListTagsForResource",
        "sso:ListTrustedTokenIssuers"
      ]
      Resource = [
        "arn:aws:sso::*:application/*/*",
        "arn:aws:sso::*:trustedTokenIssuer/*/*",
        "arn:aws:sso:::account/*",
        "arn:aws:sso:::instance/*",
        "arn:aws:sso:::permissionSet/*/*",
        "arn:aws:sso::aws:applicationProvider/*"
      ]
    }]
  })
}

resource "aws_iam_role_policy" "allow_sso_global_actions" {
  name = "AllowSSOGlobalActions"
  role = aws_iam_role.subimage_scan_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "sso:DescribeDirectories",
        "sso:DescribeRegisteredRegions",
        "sso:DescribeTrusts",
        "sso:GetApplicationInstance",
        "sso:GetApplicationTemplate",
        "sso:GetManagedApplicationInstance",
        "sso:GetMfaDeviceManagementForDirectory",
        "sso:GetPermissionSet",
        "sso:GetProfile",
        "sso:GetSharedSsoConfiguration",
        "sso:GetSsoConfiguration",
        "sso:GetSSOStatus",
        "sso:GetTrust",
        "sso:ListApplicationInstanceCertificates",
        "sso:ListApplicationInstances",
        "sso:ListApplications",
        "sso:ListApplicationTemplates",
        "sso:ListDirectoryAssociations",
        "sso:ListInstances",
        "sso:ListProfileAssociations",
        "sso:ListProfiles"
      ]
      Resource = "arn:aws:sso::*"
    }]
  })
}

resource "aws_iam_role_policy" "allow_eks_identity_read" {
  name = "AllowEKSIdentityRead"
  role = aws_iam_role.subimage_scan_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "eks:DescribeAccessEntry",
        "eks:ListIdentityProviderConfigs",
        "eks:DescribeIdentityProviderConfig"
      ]
      Resource = "*"
    }]
  })
}

data "aws_partition" "current" {}

resource "aws_iam_role_policy" "allow_public_ssm_parameter_read" {
  name = "AllowPublicSSMParameterRead"
  role = aws_iam_role.subimage_scan_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "ssm:GetParametersByPath"
      Resource = "arn:${data.aws_partition.current.partition}:ssm:*::parameter/aws/service/*"
    }]
  })
}

resource "aws_iam_role_policy" "allow_ecr_read" {
  name = "AllowECRRead"
  role = aws_iam_role.subimage_scan_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage"
      ]
      Resource = "*"
    }]
  })
}

resource "aws_iam_role_policy" "allow_glue_inspector_read" {
  name = "AllowGlueInspectorRead"
  role = aws_iam_role.subimage_scan_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "glue:GetConnections",
        "inspector2:ListMembers"
      ]
      Resource = "*"
    }]
  })
}

output "subimage_scan_role_arn" {
  description = "ARN of the created SubImage scanning IAM Role."
  value       = aws_iam_role.subimage_scan_role.arn
}

Register AWS in SubImage

Go to Modules → Add → aws and fill in:

Field Example Notes
aws_account_ids 111111111111,222222222222 Selected-account mode: every AWS account ID you want SubImage to scan. Include the management or delegated administrator account ID too if you want hierarchy mapping.
aws_organization_account_ids (optional) 111111111111 AWS management or delegated administrator account IDs that can enumerate AWS Organizations.
aws_scan_all_active_organization_accounts false Org-wide mode: set to true to scan all ACTIVE accounts discovered from aws_organization_account_ids instead of listing selected accounts.
aws_resource_functions (optional) s3,iam,ssm A subset of AWS resource collectors for faster, scoped scans. Select "Sync all resources" for all collectors.

Save the module, then use Run Sync to trigger an AWS sync immediately or wait for the hourly scheduled run.

For selected-account scans, keep aws_scan_all_active_organization_accounts disabled and list the exact accounts in aws_account_ids. If you set aws_organization_account_ids in this mode, each value must also be listed in aws_account_ids; omit it only as a fallback, in which case SubImage probes the configured scan accounts and uses the first account that can enumerate the hierarchy.

For org-wide scans, set aws_scan_all_active_organization_accounts to true, leave aws_account_ids empty, and provide one or more management or delegated administrator account IDs in aws_organization_account_ids. SubImage enumerates the organization first, scans only ACTIVE accounts, and automatically includes newly active accounts on later syncs. Use selected-account mode instead if you do not want every active account in the organization scanned.

The SecurityAudit managed policy includes AWS Organizations read actions, but AWS still restricts hierarchy APIs such as ListRoots, ListAccountsForParent, and ListOrganizationalUnitsForParent to the management account or a delegated administrator account. In selected-account mode, if no configured account can enumerate the full hierarchy, SubImage skips organization hierarchy cleanup and continues normal account-scoped resource sync. In org-wide mode, SubImage must enumerate the organization to resolve accounts to scan, so the sync fails as a configuration error if the listed org account IDs cannot enumerate the hierarchy.

Org-wide mode scans every ACTIVE account returned by AWS Organizations, including the management account when AWS reports it as active. Service-managed StackSets do not deploy to the management account automatically, so deploy the template there as a standalone stack before enabling org-wide mode.

Single-account fallback

If you prefer not to use StackSets, deploy the same template as a standalone CloudFormation stack in each account. Repeat for every account you want SubImage to scan.

Troubleshooting

  • AccessDenied on a specific service: the role is missing required read-only permissions. Verify the managed policy and inline policies.
  • AccessDenied from ssm:GetParametersByPath: update the StackSet, standalone CloudFormation stack, or Terraform role so it includes AllowPublicSSMParameterRead. If you configured prefixes outside /aws/service/..., expand the policy resource to those exact public parameter paths.
  • AccessDenied from sts:AssumeRole: the trust policy does not list the SubImage principal ARN. Re-deploy the stack or add the principal manually.
  • AWS Organizations hierarchy is missing: in selected-account mode, verify that aws_account_ids includes the management account or a delegated administrator account and that aws_organization_account_ids points at that account for predictable syncs. In org-wide mode, verify that aws_organization_account_ids contains a management or delegated administrator account ID.
  • Management account fails in org-wide mode, StackSet otherwise healthy: expected if SubImageScanRole was only deployed through a service-managed StackSet. Deploy the template in the management account as a standalone stack, or use selected-account mode if you do not want to scan it.