Connect AWS to SubImage
What this does
Deploys a read-only IAM role named SubImageScanRole in one or more AWS accounts so SubImage can assume it and inventory the account. Walks the user through three paths and asks for the values it needs before running anything.
When to use
✅ User wants to onboard an AWS account, an Organization, or the management account into SubImage. ✅ User asks for the IAM role definition, StackSet template, or Terraform for SubImage scanning. ✅ User is in their IaC repo and wants to commit the setup as code.
❌ User wants to scan EKS clusters but already has the AWS module connected: use subimage-setup:connect-kubernetes-outpost (and the EKS RBAC steps reachable from there) instead.
❌ User wants to remove an account: this skill only adds; deletion goes through their CloudFormation/Terraform tooling.
Required inputs
Before generating any commands or HCL, collect these values. If any are missing, ask the user explicitly. Do not invent, guess, or paste a literal placeholder string into a command.
| Value | Where to find it | If missing, ask |
|---|---|---|
<TENANT_ACCOUNT_ID> |
The AWS account number SubImage runs under for this tenant. Visible at Settings → Modules → AWS in the SubImage UI, in the principal ARN line. | "What is your SubImage tenant AWS account ID? You can find it in Settings → Modules → AWS, in the principal ARN. Format: 12 digits." |
<TENANT_ID> |
The customer's SubImage tenant slug (e.g. acme). Same screen as above. |
"What is your SubImage tenant ID (the slug, e.g. acme)? It is part of the principal ARN at Settings → Modules → AWS." |
<AWS_ACCOUNT_IDS> |
The AWS account(s) to onboard. | "Which AWS account IDs should SubImage scan? Comma-separated list of 12-digit IDs." |
| Path choice | A, B, or C below. | "Which deployment path do you want? A CloudFormation StackSet (recommended for AWS Organizations), B Terraform (recommended for an IaC repo), or C manual aws-cli (one-off accounts)." |
The SubImage principal ARN that the trust policy must allow is:
arn:aws:iam::<TENANT_ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly
Permissions baseline
SubImageScanRole needs:
- AWS managed policy
arn:aws:iam::aws:policy/SecurityAudit - Inline policies for SSO read, EKS identity read, ECR read (full JSON in path A and B below)
SecurityAudit already covers most discovery actions including eks:DescribeCluster and eks:ListAccessEntries. The inline additions cover SSO assignments, EKS identity provider configs, and ECR image pulls used by the image scanner.
Gotchas
Read these before generating any commands; they correct the most common wrong assumptions.
- Service-managed StackSets skip the management account. Targeting the org root is not enough. Deploy a standalone stack on the management account separately if you want it scanned.
SecurityAuditis broad but not complete. It coverseks:DescribeClusterandeks:ListAccessEntries. The inlineAllowEKSIdentityReadadds only the three actions that are missing (DescribeAccessEntry,ListIdentityProviderConfigs,DescribeIdentityProviderConfig). Do not duplicate or you make the policy harder to audit.- Principal ARN format is non-obvious. It is
arn:aws:iam::<TENANT_ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly. The role name is<TENANT_ID>-subimage-readonly, NOTsubimage-readonlyor<tenant>-readonly. Copying the wrong form means the trust policy passesterraform planbut every sync fails withAccessDenied. - Service-managed StackSets need org-level prerequisites. AWS Organizations must be set up with all-features enabled and trusted access for CloudFormation StackSets. If
create-stack-set --permission-model SERVICE_MANAGEDfails with "trusted access is not enabled", runaws organizations enable-aws-service-access --service-principal=stacksets.cloudformation.amazonaws.comfirst. - IAM is global; pick one StackSet region. The role gets created once per account regardless of how many regions you target. Use
us-east-1and stop. Multi-region targeting on an IAM-only stack just multiplies the work. - Do not pass the placeholder strings.
<TENANT_ACCOUNT_ID>and<TENANT_ID>are typed in this skill so it is obvious you must substitute. AWS will accept the literal string in the trust policy and the trust will silently never resolve.
Path A: CloudFormation StackSet (recommended for AWS Organizations)
Deploys the role into every existing and future account in the organization. Service-managed StackSets do not deploy to the management account. Run a standalone stack there if you want it scanned too.
Save this template as
subimage-scan-role.yaml. Substitute<TENANT_ACCOUNT_ID>and<TENANT_ID>first; do not pass the angle-bracket form to AWS.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::<TENANT_ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly' Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/SecurityAudit Policies: - PolicyName: AllowEKSIdentityRead PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - eks:DescribeAccessEntry - eks:ListIdentityProviderConfigs - eks:DescribeIdentityProviderConfig Resource: '*' - PolicyName: AllowECRRead PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ecr:GetAuthorizationToken - ecr:BatchCheckLayerAvailability - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage Resource: '*' - PolicyName: AllowSSORead PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sso:Describe* - sso:Get* - sso:List* Resource: '*' Outputs: SubImageScanRoleArn: Description: ARN of the created SubImage scanning IAM Role. Value: !GetAtt SubImageScanRole.ArnThe full per-action SSO list is also valid; the wildcard form above is functionally equivalent for read-only and easier to maintain. If your organization disallows wildcard SSO actions, use the explicit list at https://app.subimage.io/docs/modules/aws.
Create the StackSet with service-managed permissions:
aws cloudformation create-stack-set \ --stack-set-name SubImageScanRole \ --template-body file://subimage-scan-role.yaml \ --permission-model SERVICE_MANAGED \ --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \ --capabilities CAPABILITY_NAMED_IAMTarget every account and every region:
aws cloudformation create-stack-instances \ --stack-set-name SubImageScanRole \ --deployment-targets OrganizationalUnitIds=<root-ou-id> \ --regions us-east-1Replace
<root-ou-id>with your org root or specific OU IDs (aws organizations list-rootsreturns the root id).For the management account, deploy the same template as a standalone stack:
aws cloudformation create-stack \ --stack-name SubImageScanRole \ --template-body file://subimage-scan-role.yaml \ --capabilities CAPABILITY_NAMED_IAM
Path B: Terraform
Use this when the IAM is owned by IaC. For a single account, drop the file in your existing module. For org-wide deployment, wrap the same template in aws_cloudformation_stack_set plus aws_cloudformation_stack_set_instance.
# subimage_scan_role.tf
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::<TENANT_ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly",
]
}
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "subimage_security_audit" {
role = aws_iam_role.subimage_scan_role.name
policy_arn = "arn:aws:iam::aws:policy/SecurityAudit"
}
resource "aws_iam_role_policy" "subimage_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 = "*"
}]
})
}
resource "aws_iam_role_policy" "subimage_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" "subimage_sso_read" {
name = "AllowSSORead"
role = aws_iam_role.subimage_scan_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"sso:Describe*",
"sso:Get*",
"sso:List*",
]
Resource = "*"
}]
})
}
output "subimage_scan_role_arn" {
value = aws_iam_role.subimage_scan_role.arn
}
Substitute <TENANT_ACCOUNT_ID> and <TENANT_ID> before terraform apply. If the user wants the values pulled from variables, suggest:
variable "subimage_tenant_account_id" { type = string }
variable "subimage_tenant_id" { type = string }
and reference var.subimage_tenant_account_id, var.subimage_tenant_id in the principal ARN.
Path C: Manual aws-cli
For a one-off account or environments without IaC. Run with credentials in the target account.
TRUST_POLICY=$(cat <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<TENANT_ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonly" },
"Action": "sts:AssumeRole"
}]
}
EOF
)
aws iam create-role \
--role-name SubImageScanRole \
--assume-role-policy-document "$TRUST_POLICY"
aws iam attach-role-policy \
--role-name SubImageScanRole \
--policy-arn arn:aws:iam::aws:policy/SecurityAudit
aws iam put-role-policy \
--role-name SubImageScanRole \
--policy-name AllowEKSIdentityRead \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["eks:DescribeAccessEntry","eks:ListIdentityProviderConfigs","eks:DescribeIdentityProviderConfig"],"Resource":"*"}]}'
aws iam put-role-policy \
--role-name SubImageScanRole \
--policy-name AllowECRRead \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["ecr:GetAuthorizationToken","ecr:BatchCheckLayerAvailability","ecr:GetDownloadUrlForLayer","ecr:BatchGetImage"],"Resource":"*"}]}'
aws iam put-role-policy \
--role-name SubImageScanRole \
--policy-name AllowSSORead \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["sso:Describe*","sso:Get*","sso:List*"],"Resource":"*"}]}'
Register the accounts in SubImage
After the role exists, the user must add the accounts to the AWS module:
- Open SubImage → Modules → Add → aws (or edit if already present).
- Set
aws_account_idsto a comma-separated list of every account ID that has the role deployed (include the management account if you ran the standalone stack there). - Optional: set
aws_resource_functionsto a subset (e.g.s3,iam,ssm) for faster targeted scans. Leave empty for all collectors. - Save. Hit Run Sync to trigger immediately, or wait for the hourly schedule.
Verification
Run from a host that has SubImage's tenant credentials, or call the MCP tool from a connected client:
aws sts assume-role \
--role-arn arn:aws:iam::<one-account-id>:role/SubImageScanRole \
--role-session-name verify
A successful response with Credentials confirms the trust policy. Then in any MCP-connected AI client:
subimageListModules()
Look for aws with status: synced and a recent lastSyncEndedAt.
Troubleshooting
AccessDeniedonsts:AssumeRole: trust policy does not list the SubImage principal ARN, or you copied a placeholder literal. Re-run path A/B/C with the substituted<TENANT_ACCOUNT_ID>/<TENANT_ID>.AccessDeniedon a service action: managed policy missing or inline policy missing. ConfirmSecurityAuditis attached and the inline policies above are present.- Management account missing from scans, StackSet otherwise healthy: expected. Service-managed StackSets skip the management account; deploy the standalone stack there.
References
- Canonical doc: https://app.subimage.io/docs/modules/aws
- StackSet limitations: AWS docs on service-managed permissions.
Source: subimagesec/skills — distributed by TomeVault.