AWS Cloud Account
Connect an AWS account so DriftWise can scan live infrastructure and detect drift against your Terraform state.
Prerequisites
- AWS CLI installed and authenticated
- An AWS account with permission to create IAM resources
- A DriftWise API key or OIDC login
1. Enable Resource Explorer
DriftWise discovers AWS resources via Resource Explorer 2. It must be enabled in the account before your first scan — this is a one-time, free setup.
- Open the Resource Explorer console in the region you want as the aggregator index.
- Click Turn on Resource Explorer → Quick setup (recommended).
- Wait 5–15 minutes for the aggregator index to populate.
- Confirm the aggregator shows all of your regions in the Indexes tab.
The region where you enable the aggregator must match the region field on the DriftWise credential below. If Resource Explorer is not enabled, scans fail with error code aws.resource_explorer.not_enabled (message: Resource Explorer 2 is not enabled or has no default view).
See Cloud Discovery for the full model, including how enrichment and resource categorization work.
2. Grant scan permissions
DriftWise needs read-only access to Resource Explorer, CloudControl, STS, and the per-service read APIs that CloudControl dispatches into (for example s3:GetBucketPolicy for AWS::S3::Bucket, ec2:DescribeInstances for AWS::EC2::Instance). The AWS-managed arn:aws:iam::aws:policy/ReadOnlyAccess policy covers all of these and auto-extends as AWS adds new services — attach it to the IAM user (Option A) or role (Option B) you create in step 3.
A minimal custom policy is possible but requires listing every per-service read action you expect to use and maintaining that list as AWS grows; for most operators, the breadth of ReadOnlyAccess is an acceptable trade for zero maintenance. Open an issue if you need a tight-scope example.
CloudControl can return properties for most but not all AWS resource types. Resources whose ARN shape CloudControl does not support are still stored (identity and metadata) but their properties column stays empty. This is a permanent CloudControl limitation, not a DriftWise bug — the row is marked with enrichment_status = failed and enrichment_failure_reason = unsupported_identifier. See Cloud Discovery for the full failure-reason taxonomy.
The service prefix for Cloud Control API in IAM is cloudformation, not cloudcontrol — the API was grafted onto CloudFormation's resource-type registry and reuses its namespace. cloudcontrol:GetResource is not a valid IAM action. ReadOnlyAccess already includes cloudformation:GetResource.
Reading Terraform state from S3
When you link an AWS cloud account to an S3 state source, DriftWise uses the linked account's IAM principal to read the state object. This requires additional permissions beyond the scan policy above.
The same grants apply regardless of credential type: whether the
account is registered as aws_static (static access key), aws_role
(static key that assumes a role), or aws_oidc (OIDC-federated
sts:AssumeRoleWithWebIdentity), the principal that DriftWise ends
up making the s3:GetObject call with — the IAM user in the static
case, the assumed role in the other two — needs the state-read
statement below attached.
Option 1 — rely on ReadOnlyAccess (simple)
For same-account state buckets, the ReadOnlyAccess policy attached in step 2 already grants s3:GetObject, s3:GetObjectVersion, and s3:ListBucket on every bucket in the account — no extra permissions needed.
Not needed: write actions, any DynamoDB action. DriftWise reads state — it does not acquire the Terraform state lock.
Option 2 — dedicated state-read cloud account (least privilege)
For operators who want scan and state-read credentials fully separated:
-
Create a second IAM role (or user) with a minimal
ReadTerraformStatepolicy granting onlys3:GetObject,s3:GetObjectVersion, ands3:ListBucketon the state bucket — no Resource Explorer, no CloudControl, no broadReadOnlyAccess:{
"Sid": "ReadTerraformState",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::<STATE_BUCKET>",
"arn:aws:s3:::<STATE_BUCKET>/*"
]
} -
Register that role as a second DriftWise cloud account.
-
When creating the state source, link it to this dedicated account instead of your scanner account.
Same cloud_account_id mechanism, two different rows — no new
DriftWise feature needed.
Cross-account state buckets
If the state bucket lives in a different AWS account than the scan role, IAM alone is not enough — attach a bucket policy allowing the scan principal:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDriftWiseStateRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<SCAN_ACCOUNT_ID>:role/DriftWiseScanner"
},
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::<STATE_BUCKET>",
"arn:aws:s3:::<STATE_BUCKET>/*"
]
}
]
}
For static-key credentials, use the IAM user ARN as the Principal
instead.
SSE-KMS encrypted buckets
If the state bucket is encrypted with a customer-managed KMS key,
the scan role additionally needs kms:Decrypt:
{
"Sid": "DecryptStateBucketKMS",
"Effect": "Allow",
"Action": ["kms:Decrypt", "kms:DescribeKey"],
"Resource": "arn:aws:kms:<REGION>:<KMS_ACCOUNT_ID>:key/<KEY_ID>"
}
Cross-account KMS additionally needs the key policy (on the key itself) to grant the scan principal — KMS always requires both-sides-allow when the caller and the key are in different accounts.
SSE-S3 (AWS-managed keys) requires no extra permissions —
s3:GetObject alone is sufficient.
State-bucket troubleshooting
| Error | Cause | Fix |
|---|---|---|
AccessDenied on s3:GetObject for the state bucket | IAM or bucket policy missing state-read grant | Attach the state-read statement; if cross-account, also add the bucket policy above |
NoSuchBucket | Config bucket typo or wrong region | Verify the bucket name |
NoSuchKey | Config key typo or workspace path mismatch | Verify the state key; Terraform prefixes env:/<workspace>/ for non-default workspaces |
KMS.AccessDeniedException | Bucket SSE-KMS encrypted; scan role lacks kms:Decrypt | Add KMS to IAM; if cross-account key, also update the KMS key policy |
state file too large (>10 MiB) | Monorepo state exceeds the parser cap | Split state into workspaces; open an issue if you have a legitimate use case above the cap |
3. Choose a Credential Type
Export your 12-digit AWS account ID — the commands below reference it as $ACCOUNT_ID:
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
Option A: Access Key (simple, for dev/test)
Create an IAM user with the policy attached:
aws iam create-user --user-name driftwise-scanner
aws iam attach-user-policy \
--user-name driftwise-scanner \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
aws iam create-access-key --user-name driftwise-scanner
Save the AccessKeyId and SecretAccessKey from the output.
Option B: OIDC Federation (recommended for production)
The commands below use the production app.driftwise.ai trust values. For self-hosted or non-production deployments, fetch the equivalents from GET /api/v2/federation-info — see OIDC Federation.
Create an IAM role that DriftWise can assume via OIDC. This avoids long-lived static credentials.
AWS allows only one OIDC provider per issuer URL per account. If you already use federation.driftwise.ai for another workload, the provider likely exists — check first:
aws iam list-open-id-connect-providers
If a provider for https://federation.driftwise.ai exists, add DriftWise's audience to its list:
aws iam add-client-id-to-open-id-connect-provider \
--open-id-connect-provider-arn <existing-arn> \
--client-id "https://app.driftwise.ai/federation"
If not, create it:
aws iam create-open-id-connect-provider \
--url https://federation.driftwise.ai \
--client-id-list "https://app.driftwise.ai/federation" \
--thumbprint-list "0000000000000000000000000000000000000000"
AWS fetches the JWKS from the issuer's /.well-known/openid-configuration to validate tokens. The thumbprint is required by the CLI but AWS overrides it with the actual TLS certificate thumbprint for HTTPS issuers.
Open Cloud Scan → + Add Account → AWS in DriftWise. The "Setup Instructions" panel renders the exact trust-policy JSON with your org UUID already filled in — copy from there instead of editing this template by hand. The block below is reference; the UI is canonical.
# Replace <YOUR-ORG-UUID> with the value shown in the UI's Setup Instructions.
cat > trust-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/federation.driftwise.ai"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"federation.driftwise.ai:sub": "org-<YOUR-ORG-UUID>",
"federation.driftwise.ai:aud": "https://app.driftwise.ai/federation",
"federation.driftwise.ai:dw_org_id": "<YOUR-ORG-UUID>"
}
}
}
]
}
EOF
aws iam create-role \
--role-name DriftWiseScanner \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name DriftWiseScanner \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
federation.driftwise.ai:sub(org-<YOUR-ORG-UUID>) — without this, any DriftWise org's JWT could assume your role.federation.driftwise.ai:dw_org_id(<YOUR-ORG-UUID>) — defense-in-depth pin on the org-id claim.federation.driftwise.ai:aud— without this, a token minted for a different audience could assume your role.
4. Add the account in DriftWise
Via the UI
Echo the Role ARN so you can paste it directly into the form (assumes ACCOUNT_ID is exported from step 3):
echo "arn:aws:iam::$ACCOUNT_ID:role/DriftWiseScanner"
Then:
- Open Cloud Scan in the left sidebar.
- Click + Add Account and select Amazon Web Services.
- Fill in the form:
- Display Name — any label you'll recognize (e.g.
Production AWS). - Account ID — your 12-digit AWS account ID.
- Default Region — optional; used as the starting region for new scans.
- Credential Type —
OIDC Federation(recommended) orStatic Access Key. - OIDC Federation only:
- Role ARN — paste the output of the
echocommand above. - External ID — leave blank unless you pinned one in the role's trust policy.
- Region — the region STS dials for
AssumeRoleWithWebIdentity.
- Role ARN — paste the output of the
- Static Access Key only: Access Key ID, Secret Access Key, Region.
- Display Name — any label you'll recognize (e.g.
- Click Register Account. The backend validates against AWS (STS
GetCallerIdentity) before saving — a bad Role ARN or trust-policy mismatch fails here, not at first scan.
Via the API
Call POST /orgs/:id/accounts with provider: "aws" and one of two
credential types:
credential_type: "aws_static"—credential_refis a JSON-encoded object withaccess_key_id,secret_access_key, andregion. Optionally addrole_arnto chain throughsts:AssumeRole, andexternal_idif the target role's trust policy enforces confused- deputy protection viasts:ExternalId.credential_type: "aws_oidc"—credential_refis a JSON-encoded object withrole_arn,region, and_credential_type: "aws_oidc"(see note below).
_credential_type field inside credential_refToday the server parses credential_ref independently of the top-level
credential_type field, so OIDC credentials must also include
_credential_type inside the inner JSON to trigger the federated code path.
Without it, the request fails with access_key_id is required because the
parser falls through to the static-key branch. This duplication will be
removed once the backend merges the fields at request time.
For the static + role_arn chain, the base IAM user needs
sts:AssumeRole on the target role, and the target role's trust
policy must allow the base user as a principal. Example
credential_ref:
{
"access_key_id": "AKIA...",
"secret_access_key": "...",
"region": "us-east-1",
"role_arn": "arn:aws:iam::123456789012:role/DriftWiseScanner",
"external_id": "<shared-secret-from-role-trust-policy>"
}
Credentials are validated against AWS before save — bad keys fail at
registration time, not at first scan. The provider-reported identity
must match the external_account_id you pass (closes an account-ID
spoofing gap). Credentials are envelope-encrypted at rest; plaintext
never touches the DB.
See the accounts tag of the API reference for the full request and response shapes.
Supported resources
DriftWise discovers every resource Resource Explorer returns for the account — there is no per-type allowlist. Resources are normalized into eight broad categories (compute, storage, database, network, iam, serverless, messaging, other) regardless of provider. See Cloud Discovery for the full list with examples.
The underlying AWS resource type (for example AWS::EC2::Instance) is preserved alongside the category for exact-match drift detection.
Troubleshooting
Scan completes with 0 resources and errors
Every error maps to a resource type. Check the scan details in the UI or query the scan directly:
SELECT jsonb_pretty(scan_errors) FROM scan_runs WHERE id = '<scan_id>';
Common errors:
| Error | Cause | Fix |
|---|---|---|
aws.resource_explorer.not_enabled | Resource Explorer is not enabled or has no default view in the credential's region | Follow step 1 above to enable Resource Explorer |
aws discoverer: Resource Explorer 2 view not found | Aggregator index exists but no default view is configured | Open the Resource Explorer console and create a default view |
aws.resource_explorer.access_denied | IAM policy missing resource-explorer-2:Search | Attach ReadOnlyAccess to the user/role |
AccessDenied on cloudformation:GetResource | IAM policy missing CloudControl API permission | Add cloudformation:GetResource (the Cloud Control API's IAM prefix is cloudformation, not cloudcontrol) |
AccessDenied on a per-service read action (e.g. s3:GetBucketPolicy, ec2:DescribeInstances) | IAM policy has cloudformation:GetResource but lacks the underlying read permission CloudControl dispatches into | Attach ReadOnlyAccess, or add the specific per-service Describe*/Get*/List* actions for the affected resource type |
ExpiredToken | Static access key was deactivated or deleted | Rotate the key and update the credential in DriftWise |
InvalidClientTokenId | Access key ID doesn't exist | Check for typos in the access key ID |
InvalidIdentityToken | OIDC provider URL or trust policy conditions don't match DriftWise's issuer | Verify the provider URL is https://federation.driftwise.ai, --client-id-list is https://app.driftwise.ai/federation, and trust policy conditions match sub and aud above |
AssumeRoleWithWebIdentity failure (other) | OIDC trust policy misconfigured | Verify the trust policy principal and conditions match federation.driftwise.ai:sub and federation.driftwise.ai:aud |
enrichment_status=failed + enrichment_failure_reason=unsupported_identifier | CloudControl does not support this resource type's ARN shape | Permanent; resource identity is still tracked. See Cloud Discovery |
Scan completes with 0 resources and 0 errors
The credentials are valid but Resource Explorer returned nothing. Common causes:
- Aggregator index still populating — Resource Explorer takes 5–15 minutes to populate after initial setup. Wait and re-run.
- Wrong region — The
regionon your DriftWise credential must match the region where the Resource Explorer aggregator lives. Check the Indexes tab in the Resource Explorer console. - Empty account — The account genuinely has no resources.
- SCP restrictions — An Organization SCP may be blocking
resource-explorer-2:Searcheven though IAM allows it. Check with your AWS administrator.
Scan stuck in "pending"
The scan worker hasn't picked it up yet. Check that the scan worker is running:
kubectl logs -f deploy/scan-worker
If the worker is running but scans stay pending, check the database for stuck scans:
SELECT id, status, retry_count, started_at FROM scan_runs
WHERE status = 'running' AND started_at < NOW() - INTERVAL '10 minutes';
The sweeper automatically resets stuck scans after 10 minutes (up to 3 retries).