Skip to main content
CMDashCybersecurity Maturity Dashboard
← Back to Amazon Web Services

Amazon Web Services — Integration Setup

This is not the AWS Security Hub integration. That one reads findings from Security Hub, which you must enable and pay for. This one reads your AWS configuration directly — root MFA, S3 Block Public Access, CloudTrail, KMS rotation, EBS encryption — through a read-only IAM role in your account. You can connect either, both, or neither. If you have no Security Hub subscription, this is the one that gives you AWS evidence.

IAM role (recommended)Access key (legacy)
What you createA read-only role that trusts CMDash and requires an External ID CMDash issues for this connectionAn IAM user with a long-lived access key
What CMDash storesThe role ARNThe access key ID and secret access key (the secret encrypted)
Credential lifetime15-minute sessions, renewed while syncingNever expires until you rotate it
To revokeDelete the role, or edit its trust policyDeactivate the key
In your CloudTrailEvery call shows the session name cmdash-<connection id>Every call shows the IAM user

Use the role. The access key mode stays for connections made before the role mode existed, and for a self-hosted CMDash whose operator has not configured an integration principal (the setup form says so when you choose the role).

Prerequisites

  • An AWS account, and permission to create an IAM role (or user) with a custom policy
  • CMDash Essentials tier or above

Step 1: The read-only permissions

CMDash issues only Get/List/Describe calls. The CloudFormation template in Step 2 grants exactly these actions and nothing more; if you create the role by hand, create a policy with exactly these:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CMDashConfigRead",
      "Effect": "Allow",
      "Action": [
        "iam:GetAccountSummary",
        "iam:GetAccountPasswordPolicy",
        "s3:GetAccountPublicAccessBlock",
        "cloudtrail:DescribeTrails",
        "kms:ListKeys",
        "kms:DescribeKey",
        "kms:GetKeyRotationStatus",
        "ec2:GetEbsEncryptionByDefault",
        "backup:ListBackupPlans",
        "backup:ListBackupVaults",
        "backup:ListProtectedResources"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CMDashBackupPlanRead",
      "Effect": "Allow",
      "Action": [
        "backup:GetBackupPlan",
        "backup:ListBackupSelections"
      ],
      "Resource": "arn:aws:backup:*:111122223333:backup-plan:*"
    }
  ]
}

Why Resource: "*": every action above is an account-level or list operation that AWS does not scope to a resource ARN. Narrowing it is not possible for these calls; narrowing the actions is, and the list above is the complete set — there is no other action this integration can make use of.

Least privilege: ReadOnlyAccess and SecurityAudit would both work and both grant vastly more, including the ability to read object metadata, secrets-manager descriptions, and every resource tag in the account. Use the policy above.

Step 2: Create the role

  1. In CMDash, go to Integrations, find Amazon Web Services and click Connect.

  2. Keep Create a role with CloudFormation (recommended) selected. CMDash shows two values:

    • External ID — issued by CMDash for this connection. It stays the same if you close the form, disconnect, or reconnect, so a role you already created keeps working.
    • CMDash principal to trust — the CMDash role ARN your role's trust policy names.
  3. Create the role with the CMDash CloudFormation template, cmdash-integration-role.yaml (source: infra/cfn/cmdash-integration-role.yaml). It creates one IAM role and nothing else:

    • Trust: only the CMDash principal to trust, only sts:AssumeRole, and only with this connection's External ID (sts:ExternalId condition).
    • Permissions: exactly the eight actions in Step 1 (parameter IncludeConfigChecks, on by default), plus securityhub:GetFindings only if you set IncludeSecurityHub to true. No write action, no wildcard action, no S3 object read, no secrets read, and no AWS managed policy (not SecurityAudit, not ReadOnlyAccess).
    • Output: RoleArn, the value you paste back into CMDash.

    Two ways to launch it:

    • Launch the stack in AWS (on the form when your CMDash deployment publishes the template to Amazon S3, which CloudFormation requires for a one-click launch). It opens CloudFormation in your account with the principal and External ID filled in. Tick the IAM acknowledgement, click Create stack, and copy the RoleArn output when the stack completes.
    • Upload the template. Download cmdash-integration-role.yaml from the link on the form (or above), then CloudFormation → Create stack → With new resources → Upload a template file, and enter the two values from the form as CmdashPrincipalArn and ExternalId. Copy the RoleArn output.

    To delete CMDash's access later, delete the stack.

    Or create the role by hand:

    1. IAM → Roles → Create role → Custom trust policy, and paste this, with the two values from the form:

      json
      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": { "AWS": "<CMDash principal to trust>" },
            "Action": "sts:AssumeRole",
            "Condition": { "StringEquals": { "sts:ExternalId": "<External ID>" } }
          }
        ]
      }
      
    2. Attach the policy from Step 1 and nothing else.

    3. Name the role, e.g. CMDashIntegrationRead, and create it. Copy its ARN.

Why the External ID matters — and why the trust policy must require it. The role trusts CMDash, and CMDash serves many customers. The External ID is what stops another CMDash customer from pointing CMDash at your role: CMDash issues a different External ID for every connection, always sends the one it issued, and never accepts one typed in. That protects you only if your trust policy requires the External ID — the sts:ExternalId condition above. A role that trusts CMDash without it would accept any CMDash customer's request.

So CMDash checks it for you. When you connect (and when you click Test Connection), CMDash asks AWS for a session on your role with a random External ID it has never issued. Your role must refuse it. If it is accepted, CMDash does not connect and tells you the condition is missing; if AWS does not answer clearly, or CMDash cannot assume the role at all yet, CMDash does not connect either and tells you why — nothing is saved until the role passes. The same check runs every time CMDash starts a new 15-minute session on your role (every sync) and with every health check, so removing the condition later stops the syncs and marks the connection unhealthy. With the condition in place, someone who learns your role ARN cannot get CMDash to use it: their connection carries their External ID, which your trust policy refuses.

Step 3: Connect in CMDash

Back in the same Connect form:

  • IAM role ARN — the ARN you copied, arn:aws:iam::<12-digit account id>:role/<name>. The account in it is the account checked; you do not enter the account ID separately.
  • Primary AWS region — the region used for account-level calls, and the default for regional ones (us-east-1 if left blank)
  • Additional regions — optional list for the regional checks (CloudTrail, KMS, EBS). See below.

Click Test Connection, then Connect. CMDash assumes the role and calls iam:GetAccountSummary, which proves the role both trusts CMDash and can read something. A test that only proved authentication would pass for a role with no permissions at all — the state this integration most often lands in.

CMDash refuses a role ARN in any of CMDash's own AWS accounts, and a role ARN whose account ID is not exactly 12 digits, before contacting AWS. It then checks that the role's trust policy requires the External ID (see Why the External ID matters above) before anything is saved.

CMDash asks AWS for its session through the STS endpoint of your connection's primary region (for example sts.eu-west-1.amazonaws.com) when that is one of the regions AWS enables in every account by default; for an opt-in region (such as af-south-1 or me-central-1) it uses sts.us-east-1.amazonaws.com, which works for every account. Opening the role setup does not use one of your plan's integrations; connecting does.

Switching an existing access-key connection to a role: disconnect it, then connect again and choose the role. The External ID is issued when you choose the role, so it is not available while the access-key connection is still connected.

Access key setup (legacy)

Only for an existing connection, or a self-hosted deployment with no integration principal configured.

  1. IAM → Users → Create user, name it cmdash-config-reader. Do not grant console access.
  2. Attach the policy from Step 1.
  3. Security credentials → Create access key → Third-party service. Copy the Access key ID and Secret access key — the secret is shown once.
  4. In CMDash, click Connect on Amazon Web Services, choose Access key (legacy), and enter:
    • AWS access key ID — stored as this connection's access token
    • AWS secret access key
    • AWS account ID — your 12-digit AWS account number. Required. The S3 Control API takes it as a header, so without it the S3 public-access check cannot be made at all and is recorded as unknown.
    • Primary AWS region and Additional regions, as above
  5. Click Test Connection, then Connect.

What Syncs

CheckAWS APIControl it evidences
Root user has MFA enablediam:GetAccountSummaryCC6.1, AC-2, A.5.15
Root user has no access keysiam:GetAccountSummaryCC6.1, AC-2
Password policy requires ≥14 charactersiam:GetAccountPasswordPolicyCC6.1, IA-5
S3 Block Public Access enabled account-wides3:GetAccountPublicAccessBlockCC6.6, SC-7, A.5.10
Multi-region CloudTrail with log-file validationcloudtrail:DescribeTrailsAU-2, A.8.15, CC7.2
Customer-managed KMS keys rotate automaticallykms:ListKeys → DescribeKey → GetKeyRotationStatusSC-12, A.8.24
EBS encryption by defaultec2:GetEbsEncryptionByDefaultSC-28, A.8.24
AWS Backup posture: plans and retention, vaults (Vault Lock), protected resources and their newest recovery pointbackup:ListBackupPlans, GetBackupPlan, ListBackupSelections, ListBackupVaults, ListProtectedResourcesBackups claim on the Relying-Party Desk (v10.2.2, #845)

AWS Backup posture (v10.2.2)

The same role now reads AWS Backup so CMDash can answer a Backups question with a measured line. Five read actions, no backup:Start*, Delete*, Put*, ListRecoveryPoints* or vault-access action. ListBackupPlans, ListBackupVaults and ListProtectedResources are list calls AWS cannot scope to a resource, so they sit in a statement with Resource: "*". GetBackupPlan and ListBackupSelections are scoped to backup plans in your account (arn:aws:backup:*:<your account id>:backup-plan:*; use arn:aws-us-gov: in GovCloud).

What is recorded: whether a plan exists and selects any resources, each plan's shortest retention and longest schedule gap, the vaults and whether Vault Lock is on, and the resources enrolled in AWS Backup with the time of each one's newest recovery point (any status; AWS does not report success through this call). Only resources enrolled in AWS Backup are counted. CMDash does not list your EC2, RDS or S3 estate to find unenrolled ones, so the claim says "enrolled resources backed up in the last 7 days", never "everything is backed up". A resource whose newest recovery point is older than the longest schedule gap plus 7 days may be a deleted resource whose backups are only retained; it is reported separately and not counted, and the account is met only when every counted resource is fresh. An account with no plan, a plan that selects nothing, or a stale counted resource is reported as not covered; a role without these actions, an unhealthy or pending connection, or an unreadable region is reported as unknown.

Existing connections: re-run the Quick Launch (update the stack with the same parameters) or add the five actions to your policy by hand. Until then the Backups line reads unknown, never green. Regions follow the Regions list, like KMS and EBS.

Operator (deploy): the role template changed, so its sha256 changed. Re-upload the template to the S3 copy behind CMDASH_INTEGRATION_ROLE_TEMPLATE_URL at deploy; until then the verifier hides Quick Launch for new customers (it fails closed).

Global vs regional

The IAM and S3 checks are account-wide and run once. CloudTrail, KMS and EBS are per-region and run once per region in your Regions list, defaulting to the single configured region.

CMDash does not silently sweep all ~30 AWS regions. Doing so would multiply the KMS walk by thirty and return a wall of unknown for regions you have never opted into, which would bury the real findings. The consequence is stated plainly rather than assumed: regional coverage means the regions you listed and no others. If you run workloads in eu-west-1 and only list us-east-1, EBS encryption in eu-west-1 is not measured, and CMDash will report it as unmeasured rather than as compliant.

How the numbers are computed

  • Every check is tri-state: pass, fail, or unknown. A check CMDash could not read — a missing IAM permission, a 403, a service not enabled — is unknown, never a failure.
  • Unknown is excluded from both sides of every percentage. If your role lacks kms:GetKeyRotationStatus, key-rotation coverage reads not measured, not 0%. This is the difference between "we could not look" and "we looked and it was bad", and CMDash will not conflate them: reporting a permissions gap as a control failure would mark your controls FAILING and file risks against a cloud that may be perfectly configured.
  • AWS's documented "not configured" answers are real failures. NoSuchEntity for an absent password policy and NoSuchPublicAccessBlockConfiguration for an absent S3 block are negative answers, not unreadable ones — and they are precisely the gaps these checks exist to find. They are recorded as failing.
  • The S3 block must have all four settings on to pass. There is no half-blocked: any one of BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets left off leaves a route to public data.
  • CloudTrail needs multi-region and log-file validation together. A trail without validation records the events but cannot prove they were not edited afterwards, which is most of what an auditor wants from it.
  • Only customer-managed KMS keys are checked. AWS-managed keys (aws/s3 and friends) rotate on AWS's schedule and you cannot change it, so reporting them would add rows nobody can act on. Keys pending deletion, disabled keys, and asymmetric keys (which cannot rotate automatically at all) are skipped rather than failed.

Troubleshooting

Role mode

  • "The role's trust policy does not require the External ID": the trust policy has no sts:ExternalId condition (or it does not apply to CMDash's statement). Add "Condition": { "StringEquals": { "sts:ExternalId": "<External ID>" } } to the statement that trusts CMDash, then connect again. CMDash will not connect a role without it.
  • "CMDash could not confirm that the role's trust policy requires the External ID": AWS did not answer CMDash's check clearly (a network failure or throttling). Nothing was connected; try again in a minute.
  • "AWS did not accept CMDash's own credentials" or "AWS STS is not enabled in the region CMDash used": not a problem with your role. Try again later; if the second persists, set the connection's primary region to a region enabled in your account.
  • "AWS refused to let CMDash assume the role": AWS answers the same way for three different causes, on purpose. Check, in order: the role ARN is the one you created; the trust policy's Principal is exactly the CMDash principal to trust from the form; the sts:ExternalId condition is exactly the External ID from the form (re-open the Connect form to see it again).
  • "Role-based AWS connections are not available on this deployment": your CMDash operator has not configured the integration principal (see Operator setup below). Use an access key until they do.
  • "The role ARN points at one of CMDash's own AWS accounts": you pasted a CMDash ARN (likely the principal) instead of your role's ARN.
  • "Changing roleArn on a connected AWS integration needs a reconnect": disconnect and connect again with the new role; the External ID stays the same.

Access key mode

  • Every check reports unknown and the connection test fails with 403: the access key is wrong, disabled, or the policy is not attached. Check the key is active under IAM → Users → Security credentials.
  • InvalidSignatureException: the secret access key is wrong or has trailing whitespace. AWS signatures cover the whole request, so a single stray character fails every call.
  • Only the S3 check is unknown: the Account ID field is empty. The S3 Control API requires it.

Both modes

  • Signature errors that come and go: check the clock on your side is accurate. AWS rejects a request whose x-amz-date is more than 5 minutes out.
  • Only the KMS checks are unknown: the policy is missing one of the three kms: actions. All three are needed — ListKeys alone cannot tell a customer-managed key from an AWS-managed one.
  • Key-rotation coverage is lower than your key count: expected. AWS-managed, disabled, pending-deletion and asymmetric keys are excluded from the denominator.
  • EBS or CloudTrail results are missing for a region you use: add it to the Regions list. Regional checks only cover listed regions.

Rate Limits & Sync Behavior

SettingValue
Sync frequencyEvery 4 hours by default (syncInterval is configurable per connection)
Request volume~8 calls, plus two per customer-managed KMS key per region (DescribeKey + GetKeyRotationStatus); in role mode, one sts:AssumeRole per 12 minutes of syncing
KMS ceiling200 keys per region per sync; beyond that the walk truncates, keyRotationCoverage is computed over the truncated set, and the sync log carries a warning
Rate limit handlingRouted through CMDash's shared token-bucket limiter at a deliberately conservative 300 req/min — control-plane APIs are throttled far more tightly than data-plane ones
Circuit breakerRepeated failures open the breaker and the sync is skipped
Failed sync retryJob queue retries 3 times with exponential backoff (starting at 30s) — see docs/developers/dead-letter.md

Credential Rotation

Role mode: nothing to rotate. CMDash's sessions last 15 minutes and are renewed after 12. To cut CMDash off, delete the role or remove CMDash from its trust policy.

Access key mode: AWS access keys do not expire. Rotate at least annually.

  1. In AWS: IAM → Users → cmdash-config-reader → Security credentials → Create access key. Both keys work simultaneously.
  2. In CMDash: Settings → Integrations → Amazon Web Services → Edit, enter the new pair, Save.
  3. Confirm a successful sync, then deactivate and delete the old key in AWS.

Better still, move the connection to a role (disconnect, then connect with Create a role).

Operator setup (self-hosted, or the CMDash SaaS operator)

Role mode needs a CMDash integration principal: an IAM role in the operator's AWS account that tenant roles trust. These settings in the API's environment turn it on; with none of them the AWS connectors take access keys only (reported at boot as a warning on SaaS), and an incomplete set is reported at boot as an error and by deploy/onprem/preflight.sh.

SettingValue
CMDASH_INTEGRATION_PRINCIPAL_ARNThe principal role's ARN. This is the value tenants' trust policies name.
CMDASH_INTEGRATION_ACCESS_KEY_ID / CMDASH_INTEGRATION_SECRET_ACCESS_KEYA bootstrap key pair whose only permission is sts:AssumeRole on the principal role. Not needed on a self-hosted install on EC2 (DEPLOYMENT_MODE=customer-aws): leave both unset and CMDash uses the instance's own role, read over IMDSv2 (token first; when CMDash runs in a container, the instance's metadata hop limit must be at least 2). That instance role needs only sts:AssumeRole on the principal, and the principal's trust policy names the instance role.
CMDASH_OWN_AWS_ACCOUNT_IDSComma-separated 12-digit ids of every AWS account the operator owns. On SaaS, a tenant role in any of them — or in the principal's own account — is refused.
CMDASH_INTEGRATION_ROLE_TEMPLATE_URLOptional. The https:// Amazon S3 object URL of a copy of infra/cfn/cmdash-integration-role.yaml in a bucket the operator owns. Set, the connect form shows Launch the stack in AWS (a CloudFormation quick-create link); unset, tenants download the template or create the role by hand. CloudFormation reads quick-create templates only from S3, so the site copy at https://www.cmdash.com/cfn/cmdash-integration-role.yaml cannot be used here. The bucket must be in the principal's partition (a us-gov-* bucket for an arn:aws-us-gov: principal). Upload the file unchanged: the template is the same for every deployment, and the principal is a parameter. A value that is not an S3 template URL is reported at boot and by deploy/onprem/preflight.sh, and the button is not shown. The S3 copy is also fetched at boot and every 6 hours and its sha256 compared with this release's template: a copy that differs (a stale upload, a bucket someone else registered) or cannot be fetched is never offered, and the API logs a warning naming both hashes.

The principal role's own policy allows sts:AssumeRole on tenant roles, only with an External ID, and never into the operator's own accounts:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AssumeTenantRolesWithExternalIdOnly",
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::*:role/*",
      "Condition": { "Null": { "sts:ExternalId": "false" } }
    },
    {
      "Sid": "NeverAssumeIntoOperatorAccounts",
      "Effect": "Deny",
      "Action": "sts:AssumeRole",
      "Resource": ["arn:aws:iam::<operator account id>:role/*"]
    }
  ]
}

(Use arn:aws-us-gov: in GovCloud.) A self-hosted operator whose principal lives in the very account CMDash checks drops that account from the Deny statement (and leaves CMDASH_OWN_AWS_ACCOUNT_IDS unset); the Deny is for a SaaS operator's own accounts. Its trust policy names the bootstrap user (or, on EC2, the instance role); on SaaS, also restrict the bootstrap user's policy and the principal's trust with an aws:SourceIp condition listing the API hosts' egress addresses, so a leaked bootstrap key is useless anywhere else. On a self-hosted install the principal usually lives in the account being checked, and that is allowed.

Known Limitations

  • One account per connection. AWS Organizations is not walked. A multi-account estate needs one connection per account; coverage numbers are per connection and are not aggregated across them.
  • No per-bucket S3 audit. The account-level Block Public Access setting is read; individual bucket policies and ACLs are not. This is deliberate — a per-bucket walk over an estate of unknown size can only be truncated, and a truncated answer to "is anything public?" reads as a clean result. The account-level block is the one call that answers the question for the whole account, which is why CIS asks for it.
  • No security-group or NACL analysis. Open-ingress auditing is an unbounded walk with the same truncation problem. networkExposureFailureCount will read as unmeasured for AWS.
  • No IAM principal analysis. Over-privileged users, unused credentials, and policy simulation are not evaluated. Root hygiene and the password policy are the account-level facts available in one call each.
  • Regional checks cover only the regions you list. See Global vs regional above. This is the limitation most likely to mislead if you skim, which is why it appears twice.
  • No cross-check against Security Hub. If you connect both, they report independently; CMDash does not reconcile a disagreement between them.

Ready to see your posture?

Start a 30-day free trial of any paid plan — a card is required, and it converts unless you cancel — or sign up for Free with no card at all.