Skip to content

Amazon Web Services

Connecting from the web app?

Follow the prerequisites and credential creation instructions below, then return to Cloud Security → Settings → Providers → Add provider. Enter the provider IDs under Configuration and save the credential using New secret under Permissions. Run Test Provider, fix required failures, and save. The LimaCharlie CLI examples below are an alternative. First-time setup and verification explains the full journey.

Read-only inventory via an IAM identity that assumes a read-only role. Two topologies:

  • Single account (below): one IAM user plus one role in that account.
  • AWS Organization: deploy the same role to every account via a service-managed CloudFormation StackSet and set aws_member_role_name; the management-account role named by aws_role_arn additionally needs organizations:List* / Describe*. The base IAM user still needs nothing beyond sts:AssumeRole — member-account discovery is performed with the assumed role, not with the user's own credentials.

Architecture (least-privilege)

An IAM user whose only permission is sts:AssumeRole on a read-only role (SecurityAudit + ViewOnlyAccess), gated by an external ID. LimaCharlie stores the user's access key, assumes the role, and reads. The user itself can do nothing but assume that one role.

Create the identity (single account)

Use a dedicated test account for your first connection if you have one. Ask an AWS administrator who can create IAM users, roles, policies, and access keys to run the steps below. The created user is the application's identity, not a human login. The role grants the read permissions, and the external ID is a value that must match between the role's trust policy and LimaCharlie.

Choose one tab; both create the same user, role, and access key.

  1. Sign in to the AWS console as an administrator, open IAM → Users → Create user, and name the user lc-cloudsec. Leave console access off. Create the user without attaching policies; its permission is added below.
  2. Copy your 12-digit account ID from the account menu. Generate and save a unique alphanumeric external ID (for example, using a password manager). Replace <ACCOUNT_ID> and <EXTERNAL_ID> in the following policy with those values.
  3. Open IAM → Roles → Create role → Custom trust policy and paste:

    {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": {"AWS": "arn:aws:iam::<ACCOUNT_ID>:user/lc-cloudsec"},
        "Action": "sts:AssumeRole",
        "Condition": {"StringEquals": {"sts:ExternalId": "<EXTERNAL_ID>"}}
      }]
    }
    
  4. Continue to permissions and select both SecurityAudit and ViewOnlyAccess. Name the role LimaCharlieCloudSecRO, create it, and copy its ARN from the role details. Keep the external ID for the LimaCharlie wizard.

  5. Open IAM → Users → lc-cloudsec → Permissions → Add permissions → Create inline policy. Select the JSON editor and paste this policy, replacing <ACCOUNT_ID> with the same account ID:

    {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": "arn:aws:iam::<ACCOUNT_ID>:role/LimaCharlieCloudSecRO"
      }]
    }
    
  6. Name the policy lc-assume-ro and create it. The user now has permission to assume this role; the read permissions belong to the role.

  7. On the user's Security credentials tab, choose Create access key. Review the use-case guidance, select Other, continue through the prompts, and save the Access key ID and Secret access key. AWS shows the secret only at creation; keep it for the credential step below.

AWS documents custom trust policies and access key creation.

  1. Sign in to the AWS console with that administrator identity (not root).
  2. Open CloudShell from the console toolbar and use its Bash shell. The AWS CLI is already installed and uses your console identity. See AWS CloudShell setup.
  3. Run aws sts get-caller-identity and confirm the Account is the one you intend to connect.
  4. Run the commands below in that same shell. Stop if a command fails rather than continuing with an incomplete role.
  5. Keep the generated external ID and the access key's AccessKeyId and SecretAccessKey. They are used in different fields, as shown below.
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
EXTERNAL_ID=$(openssl rand -hex 16)          # save this

aws iam create-user --user-name lc-cloudsec

cat > trust.json <<EOF
{ "Version": "2012-10-17", "Statement": [{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:user/lc-cloudsec" },
  "Action": "sts:AssumeRole",
  "Condition": { "StringEquals": { "sts:ExternalId": "${EXTERNAL_ID}" } }
}] }
EOF
aws iam create-role --role-name LimaCharlieCloudSecRO \
  --assume-role-policy-document file://trust.json
aws iam attach-role-policy --role-name LimaCharlieCloudSecRO \
  --policy-arn arn:aws:iam::aws:policy/SecurityAudit
aws iam attach-role-policy --role-name LimaCharlieCloudSecRO \
  --policy-arn arn:aws:iam::aws:policy/job-function/ViewOnlyAccess

cat > assume.json <<EOF
{ "Version": "2012-10-17", "Statement": [{
  "Effect": "Allow", "Action": "sts:AssumeRole",
  "Resource": "arn:aws:iam::${ACCOUNT_ID}:role/LimaCharlieCloudSecRO"
}] }
EOF
aws iam put-user-policy --user-name lc-cloudsec \
  --policy-name lc-assume-ro --policy-document file://assume.json

aws iam create-access-key --user-name lc-cloudsec   # capture AccessKeyId + SecretAccessKey

Create the credentials secret

In the LimaCharlie wizard's Permissions step, select New secret. Choose a name such as aws-credentials, then paste this JSON with the two values from AWS: the access key ID and secret access key. If you used the CLI, do not paste the entire command output.

{"access_key_id": "AKIA...", "secret_access_key": "..."}

No aws_ prefix, and no other keys

aws_access_key_id / aws_secret_access_key are silently ignored — the SDK then falls back to the default credential chain and the auth check fails with no EC2 IMDS role found. Use the bare access_key_id / secret_access_key keys. Those two are the only keys read: a session_token is not supported, so temporary/session credentials cannot be used here (they are dropped, and the assume then fails with InvalidClientTokenId). Use the long-lived access key of the dedicated IAM user — the role it assumes is where the read permissions live.

For CLI setup, save the credential JSON above as aws-secret.json and install jq.

jq -Rs '{secret: .}' aws-secret.json \
  | limacharlie secret set --key aws-credentials --enabled \
  && rm -f aws-secret.json

jq -Rs wraps the file contents into the secret record for stdin — the equivalent of limacharlie hive set --hive-name secret with {"secret": "<the credential JSON as a string>"}.

Create the provider record

In the wizard's Configuration step, enter the role ARN from IAM → Roles → LimaCharlieCloudSecRO and the external ID you generated. An ARN is AWS's full identifier for a resource. Leave the member role field empty for a single-account setup; it is for collecting across an AWS Organization. Then test and save.

The equivalent CLI configuration is below.

provider.yaml:

provider_type: aws
aws_role_arn: "arn:aws:iam::<ACCOUNT_ID>:role/LimaCharlieCloudSecRO"
aws_external_id: "<EXTERNAL_ID>"
credentials: hive://secret/aws-credentials
# aws_regions: [us-east-1, ...]                 # optional; omit = all enabled regions
# aws_member_role_name: LimaCharlieCloudSecRO   # ONLY for AWS Organization member accounts

Verify & coverage

limacharlie cloudsec provider test --input-file provider.yaml

The report opens with the config and credential checks common to every provider, documented once in Provider Setup; the AWS-specific checks follow.

Check Required Meaning if it fails
auth sts:AssumeRole failed — wrong external ID, trust policy, or credentials. Nothing else is probed.
ec2 Compute inventory unavailable.
iam IAM inventory unavailable — the CIEM access graph cannot be built.
iam_boundaries IAM permission boundaries unreadable (iam:GetAccountAuthorizationDetails). A principal whose boundary caps it below its policies is scored on the uncapped policies, so it can be reported as an administrator when it is not.
s3 Storage inventory unavailable.
regions Only meaningful when aws_regions is unset (with it set the check is skipped as unnecessary, and still counts as passing). Enabled-region enumeration was denied, so the sweep falls back to us-east-1 alone — list the regions you want in aws_regions.
organizations Member-account discovery unavailable; only the connected account is swept.
org_policies The organization's service control policies (SCPs) and resource control policies (RCPs) are unreadable, so they are not collected. Passes when the account is not in an organization, or no policy type is enabled on the organization root — in both cases there is nothing to read.
inspector Workload vulnerability findings unavailable.
secrets_manager Secret-store inventory unavailable.
data_stores RDS / DynamoDB / Redshift inventory unavailable.
front_doors Lambda / API Gateway / load-balancer inventory unavailable — a public function URL, API stage or internet-facing load balancer stays invisible to the exposure model.
ai_services SageMaker / Bedrock inventory unavailable.

With SecurityAudit + ViewOnlyAccess, every optional surface above also passes — no extra policies needed.

Why front_doors probes more than one call

Lambda's exposure verdict needs three separate grants — lambda:ListFunctions, lambda:ListFunctionUrlConfigs and lambda:GetPolicy — and a role missing any one of them still passes a single representative list call. The check therefore exercises the real per-function reads against one existing function, so a hand-rolled least-privilege policy is caught at connect time instead of leaving the Lambda inventory unreadable on every sweep.

The two managed policies grant these BETWEEN them, not each on its own: SecurityAudit supplies lambda:List* and lambda:GetPolicy, ViewOnlyAccess the ELBv2 describes, and both supply apigateway:GET. Attach both, as the setup above does.

Organization guardrails (SCPs and RCPs)

For an account in an AWS Organization, LimaCharlie records where the account sits in the organization tree and every service control policy and resource control policy that applies to it — those attached to the account itself, plus each organizational unit above it, plus the root. The policy documents are stored verbatim, each labelled with the level it is attached at.

Collected, not yet applied

These policies are inventory today, not part of the access calculation. Permission and attack-path answers are still the union of the permissions that were granted, without subtracting what an SCP forbids — so they can over-state access. If the collection role cannot read the policies at all, that is reported as a finding ("effective-permission analysis is unconstrained") rather than left implicit.

Collecting them uses these read-only actions on the role named by aws_role_arn, all of which SecurityAudit already grants:

organizations:DescribeOrganization    organizations:ListPolicies
organizations:DescribePolicy          organizations:ListTargetsForPolicy
organizations:ListRoots               organizations:ListAccounts
organizations:ListOrganizationalUnitsForParent
organizations:ListAccountsForParent

If you attached a hand-written least-privilege policy instead of SecurityAudit, add the actions above — the org_policies check tells you whether they are in place. A standalone account (not in an organization) needs none of this and reports no finding: with no organization there are no guardrails to account for.

Which account the role must live in

AWS only allows these reads from the organization's management account, or from an account you have registered as a delegated administrator for AWS Organizations. From any other member account they are refused with the same AccessDenied you would get from a missing permission, and adding IAM permissions will not change that. If org_policies keeps failing with a fully-granted role, check which account aws_role_arn points at.

Propagation

Fresh IAM keys and role trust can take a few seconds to propagate; retry once on a transient AccessDenied / InvalidClientTokenId.

Troubleshooting

provider test error Cause Fix
auth fails: … no EC2 IMDS role found Secret used the wrong key names → no static creds → default chain → IMDS Use access_key_id / secret_access_key (no aws_ prefix)
AccessDenied on sts:AssumeRole External ID mismatch, wrong trust-policy principal, or propagation Confirm aws_external_id matches the trust condition; retry after a few seconds