Skip to main content

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.

  1. Open the Resource Explorer console in the region you want as the aggregator index.
  2. Click Turn on Resource ExplorerQuick setup (recommended).
  3. Wait 5–15 minutes for the aggregator index to populate.
  4. 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 coverage

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.

CloudControl's IAM prefix

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:

  1. Create a second IAM role (or user) with a minimal ReadTerraformState policy granting only s3:GetObject, s3:GetObjectVersion, and s3:ListBucket on the state bucket — no Resource Explorer, no CloudControl, no broad ReadOnlyAccess:

    {
    "Sid": "ReadTerraformState",
    "Effect": "Allow",
    "Action": [
    "s3:GetObject",
    "s3:GetObjectVersion",
    "s3:ListBucket"
    ],
    "Resource": [
    "arn:aws:s3:::<STATE_BUCKET>",
    "arn:aws:s3:::<STATE_BUCKET>/*"
    ]
    }
  2. Register that role as a second DriftWise cloud account.

  3. 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

ErrorCauseFix
AccessDenied on s3:GetObject for the state bucketIAM or bucket policy missing state-read grantAttach the state-read statement; if cross-account, also add the bucket policy above
NoSuchBucketConfig bucket typo or wrong regionVerify the bucket name
NoSuchKeyConfig key typo or workspace path mismatchVerify the state key; Terraform prefixes env:/<workspace>/ for non-default workspaces
KMS.AccessDeniedExceptionBucket SSE-KMS encrypted; scan role lacks kms:DecryptAdd KMS to IAM; if cross-account key, also update the KMS key policy
state file too large (>10 MiB)Monorepo state exceeds the parser capSplit 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.

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.

Register the OIDC provider first

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.

Self-service setup — copy from the UI

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
Pin all three conditions
  • 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:

  1. Open Cloud Scan in the left sidebar.
  2. Click + Add Account and select Amazon Web Services.
  3. 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 TypeOIDC Federation (recommended) or Static Access Key.
    • OIDC Federation only:
      • Role ARN — paste the output of the echo command above.
      • External ID — leave blank unless you pinned one in the role's trust policy.
      • Region — the region STS dials for AssumeRoleWithWebIdentity.
    • Static Access Key only: Access Key ID, Secret Access Key, Region.
  4. 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_ref is a JSON-encoded object with access_key_id, secret_access_key, and region. Optionally add role_arn to chain through sts:AssumeRole, and external_id if the target role's trust policy enforces confused- deputy protection via sts:ExternalId.
  • credential_type: "aws_oidc"credential_ref is a JSON-encoded object with role_arn, region, and _credential_type: "aws_oidc" (see note below).
The _credential_type field inside credential_ref

Today 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:

Diagnostic query — internal schema, column names may change
SELECT jsonb_pretty(scan_errors) FROM scan_runs WHERE id = '<scan_id>';

Common errors:

ErrorCauseFix
aws.resource_explorer.not_enabledResource Explorer is not enabled or has no default view in the credential's regionFollow step 1 above to enable Resource Explorer
aws discoverer: Resource Explorer 2 view not foundAggregator index exists but no default view is configuredOpen the Resource Explorer console and create a default view
aws.resource_explorer.access_deniedIAM policy missing resource-explorer-2:SearchAttach ReadOnlyAccess to the user/role
AccessDenied on cloudformation:GetResourceIAM policy missing CloudControl API permissionAdd 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 intoAttach ReadOnlyAccess, or add the specific per-service Describe*/Get*/List* actions for the affected resource type
ExpiredTokenStatic access key was deactivated or deletedRotate the key and update the credential in DriftWise
InvalidClientTokenIdAccess key ID doesn't existCheck for typos in the access key ID
InvalidIdentityTokenOIDC provider URL or trust policy conditions don't match DriftWise's issuerVerify 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 misconfiguredVerify the trust policy principal and conditions match federation.driftwise.ai:sub and federation.driftwise.ai:aud
enrichment_status=failed + enrichment_failure_reason=unsupported_identifierCloudControl does not support this resource type's ARN shapePermanent; 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 region on 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:Search even 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:

Diagnostic query — internal schema, column names may change
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).