AWS cross-check setup (live verification)
This connects Cloudkeel-DD to the live AWS API so it can independently verify what Terraform claims, and find resources Terraform doesn’t know about at all.
It’s a read-only credential - Cloudkeel-DD never modifies AWS resources. It’s
also a separate credential from the bucket-scoped one used for
raw .tfstate in S3; the two are independent and
safely coexist.
1. Create a narrow IAM user
Section titled “1. Create a narrow IAM user”AWS Console: IAM -> Users -> Create user. No console access needed - this is an API-only credential.
2. Attach the read-only policy
Section titled “2. Attach the read-only policy”Attach a policy using the exact document in
aws-iam-policy.json. It grants only
Describe/Get/List calls, across the services Cloud Control dispatches to
for the 62 AWS types Cloudkeel-DD field-diffs.
Plus cloudformation:ListResources and cloudformation:GetResource (their own
statement, DDetectiveUniversalReader). Despite the name these have nothing to
do with CloudFormation stacks - they are the IAM actions for the Cloud
Control API, which is how Cloudkeel-DD reads most AWS resource types.
Required, not optional - omit these and most types fail. Without this statement every type read through Cloud Control returns
AccessDeniedException: ... not authorized to perform: cloudformation:ListResources, and the scan reports them as “not verifiable - not readable with this credential”. That covers RDS instances, EIPs, EKS clusters, IAM roles and policies, KMS keys, load balancers, route tables, and S3 buckets. Security groups keep working, because they are the one documented exception that reads throughec2:DescribeSecurityGroupsinstead - which is why a policy missing this statement fails partially rather than completely.These actions are read-only, but they are scoped
resource/*and so are broader than the per-service calls above. That breadth is what a single uniform read surface across every type costs.
Plus the underlying-service read actions (their own statement,
DDetectiveUniversalReaderServices). The Cloud Control API is only a dispatch
layer: cloudformation:ListResources lets Cloudkeel-DD invoke it, but each
resource type’s handler then calls that service’s own read API under the
same credential. Reading 62 types therefore means read actions across ~40
services — RDS, IAM, KMS, S3, DynamoDB, Lambda, CloudFront, EFS, Redshift,
OpenSearch and the rest.
This statement is generated from each type’s declared read permissions in
the CloudFormation registry (aws cloudformation describe-type →
handlers.read.permissions), not hand-maintained. That is why it grew: the
hand-maintained version silently stopped matching coverage.
All metadata reads - no data-plane access. The S3 actions are
GetBucket*(bucket configuration: policy, encryption, versioning, tagging, and so on), nevers3:GetObject- Cloudkeel-DD never reads object contents. Likewise nothing here reads secret values, Lambda code, or database rows.Keeping that true costs coverage on four types, and we chose the guarantee over the coverage. Their read handlers ask for permissions we deliberately do not grant:
Type Withheld Why aws_lambda_functionlambda:GetFunctionReturns a pre-signed URL to download the function’s code aws_lambda_function,aws_cognito_user_pool,aws_sfn_state_machinekms:DecryptData-plane: decrypts ciphertext aws_cloudwatch_event_ruleiam:PassRoleNot a read at all — it delegates a role Those four field-diff on the attributes that are readable and report the rest as “not verifiable”. Grant the withheld actions yourself if you want full coverage on them and accept the trade — Cloudkeel-DD never asks for them. Wildcards in this statement are confined to
Describe*/List*/Get*on services with no data-plane reach; S3, KMS, Lambda, SSM, Secrets Manager and Cognito stay enumerated action-by-action for exactly that reason.
Plus one optional action, cloudtrail:LookupEvents (its own statement,
DDetectiveActorAttributionOptional) - lets Cloudkeel-DD look up who most
recently changed a resource via CloudTrail, shown on drift findings as
“Changed by”. Skip it and scanning still works exactly the same; that field
just stays blank for AWS resources.
Nothing that writes, and no Action: "*" — every wildcard is a read verb
(Describe*/List*/Get*) scoped to one service.
POLICY_ARN=$(aws iam create-policy \ --policy-name DDetectiveReadOnly \ --policy-document file://docs/aws-iam-policy.json \ --query 'Policy.Arn' --output text)
aws iam attach-user-policy \ --user-name ddetective-connector \ --policy-arn "$POLICY_ARN"Already have the old inline policy? Remove it after attaching the managed one, so the account does not keep a stale grant around:
aws iam delete-user-policy \ --user-name ddetective-connector \ --policy-name DDetectiveReadOnly3. Generate an access key pair
Section titled “3. Generate an access key pair”Same user -> Security credentials -> Create access key. Copy both values; AWS shows the secret once.
4. Connect it in Cloudkeel-DD
Section titled “4. Connect it in Cloudkeel-DD”Settings -> Cross-check integrations -> AWS:
| Field | Value |
|---|---|
| Integration name | Any label, e.g. prod-aws |
| Access key ID | From step 3 |
| Secret access key | From step 3 |
| Region | e.g. us-east-1 |
Click Test connection, then save.
The account is detected automatically. On save, Cloudkeel-DD calls STS
GetCallerIdentity to learn the real AWS account ID and creates an enabled scope
for it - no manual account entry, no discovery step to run.
What gets verified
Section titled “What gets verified”Field-level drift verification: security groups only (aws_security_group,
including standalone aws_security_group_rule /
aws_vpc_security_group_{ingress,egress}_rule resources, merged into their
parent). Rules are canonicalized before comparison, so AWS’s rule coalescing
and Terraform’s separately-declared blocks don’t register as false drift.
Unmanaged-resource detection: full, across all 64 enumerated AWS types — not just the ones the plan path cross-checks. Anything in that set running in the account with no Terraform tracking is flagged. The coverage page lists every enumerated and spec’d type, generated at build time from the product’s own type registry.
Actor attribution (who changed it): via CloudTrail, wherever a finding has a
usable resource id. If cloudtrail:LookupEvents is granted (see step 2),
drift and unmanaged-resource findings show who last modified the resource and
when, within the last 90 days (CloudTrail’s own retention window for
LookupEvents) - unmanaged findings for any of the types above, live-diffed
drift for any type with a working normalizer, and security-group cross-check
findings. Best-effort: a missing permission, an old change, or an event
CloudTrail simply doesn’t retain leaves this blank rather than failing the
scan.
For comparison: Azure carries the most spec’d types (79) and reaches 68 of them on the plan path, where AWS reaches one. See Azure cross-check setup.
- One AWS credential per workspace. A second active AWS integration is rejected with a 409 - it would be silently ignored by every scan, so it fails loudly instead. Edit or deactivate the existing one.
- One access key = one account. AWS has no equivalent of Azure’s multi-subscription model, so multi-account scanning (via AWS Organizations assume-role) is a known gap. One credential covers one account today.
- Key rotation is a manual action. When a key is rotated or revoked, scans fail and the real AWS error appears in Inventory’s Last error; update the integration with the new key.