Configuring AWS Authentication
This guide covers how to securely configure AWS authentication for Controlinfra to scan your infrastructure.
Overview
Controlinfra needs AWS credentials to:
- Access your Terraform state (if using S3 backend)
- Run
terraform planto detect drift - Query AWS APIs to compare actual vs. desired state
Authentication Methods
Controlinfra supports four authentication methods, each suited for different environments and security requirements:
| Method | Runner Type | Security Level | Best For |
|---|---|---|---|
| Access Keys | Cloud or Self-Hosted | Standard | Quick setup, development |
| Instance Profile | Self-Hosted Only | High | EC2-based runners |
| Assume Role (3a — control-plane) | Cloud | High | Cross-account scanning without operating your own runner. Configure via Cloud Accounts only — not available on the legacy per-repo creds form. |
| Assume Role (3b — self-hosted runner) | Self-Hosted | High | Cross-account, all credentials inside your AWS account |
| OIDC Federation | Cloud | Highest | Enterprise, zero secrets |
Trust requirements at a glance
Each method involves a different principal on Controlinfra's side. Pick the one whose hosting model matches what you can run.
| Method | Who Controlinfra authenticates as | What you trust on Controlinfra's side | Where the scanner must run |
|---|---|---|---|
| Access Keys | The IAM user you created — Controlinfra signs requests with the user's AccessKeyId + SecretAccessKey directly. | Nothing on Controlinfra's side. The IAM user belongs to your AWS account; Controlinfra holds its keys in encrypted storage. | Cloud runner or self-hosted. |
| Instance Profile | The IAM role attached to the runner EC2 instance. The EC2 metadata service hands out short-lived credentials automatically. | Nothing — credentials never leave your AWS environment. | Self-hosted runner on EC2 only. Controlinfra's cloud runner is not on EC2. |
| Assume Role (3a — control-plane) | A Controlinfra-managed master IAM user (controlinfra-master-scanner) inside Controlinfra's AWS account. The control plane calls sts:AssumeRole on your role using the master user's keys. | Controlinfra's master IAM user ARN — listed in the Add Cloud Account form. Add it as a Principal.AWS in your role's trust policy. | Cloud runner. Configure via Cloud Accounts only — the legacy per-repo creds form rejects assume_role on a cloud runner. |
| Assume Role (3b — self-hosted runner) | The IAM role attached to your self-hosted runner (or its instance profile). The runner calls sts:AssumeRole on your target role using its own credentials. | Nothing on Controlinfra's side — the AssumeRole call comes from your runner's identity. | Self-hosted runner only. |
| OIDC Federation | Controlinfra signs a short-lived JWT (issuer = https://api.controlinfra.com, subject = org:<your-org-id>); AWS STS exchanges it via AssumeRoleWithWebIdentity for temporary credentials under your IAM role. | Controlinfra's OIDC issuer URL — register https://api.controlinfra.com as an OIDC Identity Provider in your AWS account, then trust it in your role's Principal.Federated. The trust-policy condition keys must use the api.controlinfra.com: prefix to match the issuer host. | Cloud runner (Enterprise plan). |
Why this matters
"Self-Hosted Only" methods don't degrade gracefully — they fail outright if Controlinfra's cloud runner is the executor, because the cloud runner has no AWS-side identity to assume. Access Keys or Assume Role (3a) or OIDC for cloud-runner setups; Instance Profile or Assume Role (3b) when you operate the runner yourself.
Option 1: Access Keys
The simplest method using AWS Access Key ID and Secret Access Key.
Trust requirements
- Who Controlinfra authenticates as: the IAM user you create below. Controlinfra holds its
AccessKeyId+SecretAccessKeyin encrypted credential storage and signs AWS API requests with them directly. - What you trust on Controlinfra's side: nothing — the IAM user belongs to your AWS account.
- Where this works: cloud runner or self-hosted runner.
When to Use
- Getting started quickly
- Cloud runner (Controlinfra-managed)
- Development/testing environments
Setup Steps
- Go to AWS IAM Console → Users → Create user
- Name the user (e.g.,
controlinfra-scanner) - Attach the required policy (see IAM Permissions below)
- Create access keys → Security credentials → Create access key
- Enter in Controlinfra: Add Repository → AWS Authentication → Access Keys
Security Considerations
- Credentials are encrypted at rest with AES-256-GCM
- Rotate keys regularly (recommended: every 90 days)
- Use least-privilege permissions
Option 2: Instance Profile Recommended
Use the IAM role attached to your EC2 instance. No credentials to manage!
Trust requirements & hosting constraint
- Who Controlinfra authenticates as: the IAM role attached to your EC2 instance via its instance profile. The EC2 metadata service hands out short-lived credentials and they never leave the VM.
- What you trust on Controlinfra's side: nothing — there is no Controlinfra-issued principal involved.
- Where this works: only on a self-hosted runner running on EC2 with an instance profile attached. Controlinfra's cloud runner is not on EC2, so picking this option with a cloud runner will fail at scan time.
Self-Hosted Runner Required
This option is only available when using a self-hosted runner on EC2.
When to Use
- Self-hosted runner on EC2
- Enhanced security requirements
- No credential rotation needed
How It Works
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Controlinfra │────▶│ Self-Hosted │────▶│ AWS APIs │
│ Cloud │ │ Runner (EC2) │ │ │
└─────────────────────┘ └──────────────────┘ └─────────────┘
│
IAM Instance Profile
(automatic credentials)Setup Steps
Create an IAM Role for your EC2 instance:
- Go to IAM → Roles → Create role
- Select "AWS service" → EC2
- Attach the required policy (see IAM Permissions)
- Name it (e.g.,
controlinfra-runner-role)
Attach the role to your EC2 instance:
- EC2 Console → Select instance → Actions → Security → Modify IAM role
- Select your role and save
Install the self-hosted runner on the EC2 instance (guide)
Configure in Controlinfra:
- Add Repository → AWS Authentication
- Select "Self-Hosted" runner
- Select "Instance Profile" authentication
- Choose your AWS region
Benefits
- No credentials stored - AWS SDK automatically uses EC2 metadata service
- No rotation needed - AWS manages temporary credentials
- Audit-friendly - All actions tracked under the IAM role
- Secure - Credentials never leave your AWS account
Option 3: Assume Role Advanced
Use STS AssumeRole for cross-account access or enhanced security. Two flavours, depending on which Controlinfra component performs the AssumeRole call.
Where each flavour is supported in the UI
- Cloud Accounts (Settings → Cloud Accounts → Add Cloud Account) — supports both 3a (control-plane) and 3b (self-hosted runner) flavours. This is the recommended path and the one this guide focuses on.
- Per-repository AWS credentials (Add Repository → AWS Authentication) — the legacy per-repo creds form only supports 3b (self-hosted runner). The repo-creds validator rejects
assume_roleon a cloud runner with"Assume Role authentication requires a self-hosted runner". To use 3a control-plane AssumeRole on the cloud runner, configure it via Cloud Accounts and reference that account on the repo.
Pick a flavour
| Flavour | Caller of AssumeRole | When to use | Setup steps |
|---|---|---|---|
| 3a — Control-plane AssumeRole | The Controlinfra control plane itself, using a master IAM user managed by Controlinfra | Cloud runner (Controlinfra-managed scanning); no infrastructure of your own to set up. Only available via the Cloud Accounts form, not the legacy per-repo creds form. | Trust Controlinfra's master IAM user ARN in your role's trust policy. See Setup — Control-plane variant below. |
| 3b — Runner-chained AssumeRole | A self-hosted runner you operate (e.g. EC2 with an Instance Profile) | You want to keep all credentials inside your AWS account and cross to multiple target accounts from one runner | Trust your runner's role ARN in your target role's trust policy. See Setup — Self-hosted runner variant below. |
In the Cloud Accounts form, both flavours sit under the same "Assume Role" option — you simply pick whichever runner type the surrounding workspace is configured for. The key onboarding choice is which principal to put in your trust policy, and that depends entirely on the flavour.
Where do I find Controlinfra's master IAM user ARN?
Open the Add Cloud Account form in the Controlinfra dashboard, choose AWS → Assume Role. The form surfaces the master IAM user ARN you need to add to your trust policy. The actual ARN differs between Controlinfra environments (stage / production), so always copy it from your live dashboard rather than hard-coding it.
When to Use
- Cross-account infrastructure scanning
- Centralized runner, multiple AWS accounts
- Maximum security with external ID verification
- Temporary credentials with defined duration
How It Works (Self-hosted runner variant)
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Controlinfra │────▶│ Runner Account │ │ Target │
│ Cloud │ │ (EC2 + Role A) │ │ Account │
└─────────────────────┘ └──────────────────┘ │ (Role B) │
│ └─────────────┘
│ STS AssumeRole ▲
└──────────────────────┘Technical Flow
- Runner starts with its EC2 Instance Profile (Role A) as base credentials
- Runner calls
aws sts assume-roleusing the Instance Profile credentials - AWS STS returns temporary credentials for the target role (Role B)
- Terraform runs using the temporary credentials
- Credentials expire after the session duration (default: 1 hour)
This flow ensures the runner itself only needs sts:AssumeRole permission, while all infrastructure access is granted through the target role.
How It Works (Control-plane variant)
┌──────────────────────────┐ ┌─────────────┐
│ Controlinfra control │ │ Target │
│ plane │──── STS AssumeRole ───────▶│ Account │
│ (master IAM user keys) │ │ (Role) │
└──────────────────────────┘ └─────────────┘Technical Flow (Control-plane)
- Controlinfra control plane holds long-lived access keys for a single master IAM user (
controlinfra-master-scanner) inside Controlinfra's own AWS account. - You add Controlinfra's master IAM user ARN to the trust policy of your target role.
- When a scan or validation fires, Controlinfra calls
sts:AssumeRoleon your role, using the master user's credentials as the caller. - AWS STS returns short-lived credentials for your target role (default: 1 hour).
- Controlinfra performs read-only AWS API calls with the temporary credentials.
- Credentials expire at the end of the session — Controlinfra re-assumes for the next scan.
The master IAM user is rotated by Controlinfra on a regular cadence (see AWS-MASTER-KEY-ROTATION.md in the repo for the current rotation policy). You don't need to update your trust policy when rotation happens — the user's ARN is stable; only its access keys change.
Setup — Control-plane variant (3a)
Use this when scans run from Controlinfra's managed cloud runner. Simplest setup; no infrastructure of your own to operate.
Step 1 (3a): Create the Target Role with Controlinfra's master IAM user as the principal
Open the Controlinfra dashboard → Add Cloud Account → AWS → Assume Role. Copy the master IAM user ARN the form shows you (it looks like arn:aws:iam::<controlinfra-account>:user/controlinfra-master-scanner — the exact account ID depends on your environment).
Create the trust policy file (trust-policy.json):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::CONTROLINFRA_ACCOUNT_ID:user/controlinfra-master-scanner"
},
"Action": "sts:AssumeRole",
"Condition": {}
}
]
}Create the role and attach read-only permissions:
aws iam create-role \
--role-name ControlinfraTargetRole \
--assume-role-policy-document file://trust-policy.json \
--description "Role for Controlinfra control plane to scan infrastructure"
aws iam attach-role-policy \
--role-name ControlinfraTargetRole \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccessStep 2 (3a): Configure in Controlinfra
- Add Cloud Account → AWS → Assume Role
- Enter the Role ARN of your
ControlinfraTargetRole - Optionally set an External ID (see Using External ID)
- Save — the form validates the trust relationship by performing a real AssumeRole call. A
200response confirms the trust policy is correct.
Multiple principals (control-plane + self-hosted runner)
If you also operate a self-hosted runner that needs to scan the same target, list both principals in the trust policy:
"Principal": {
"AWS": [
"arn:aws:iam::CONTROLINFRA_ACCOUNT_ID:user/controlinfra-master-scanner",
"arn:aws:iam::YOUR_RUNNER_ACCOUNT_ID:role/your-runner-role"
]
}Setup — Self-hosted runner variant (3b)
Use this when you want all scanning to originate from your own AWS infrastructure (typical for regulated environments where leaving your VPC isn't acceptable).
Step 1 (3b): Create the Target Role (in each target AWS account)
Create the trust policy file (trust-policy.json):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::RUNNER_ACCOUNT_ID:role/your-runner-role"
},
"Action": "sts:AssumeRole",
"Condition": {}
}
]
}Multiple Runners
To allow multiple runner roles to assume this role, use an array:
"Principal": {
"AWS": [
"arn:aws:iam::RUNNER_ACCOUNT_ID:role/runner-role-1",
"arn:aws:iam::RUNNER_ACCOUNT_ID:role/runner-role-2"
]
}Create the role using AWS CLI:
# Create the target role
aws iam create-role \
--role-name ControlinfraTargetRole \
--assume-role-policy-document file://trust-policy.json \
--description "Role for Controlinfra to scan infrastructure"
# Attach read-only permissions (or your custom policy)
aws iam attach-role-policy \
--role-name ControlinfraTargetRole \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccessStep 2 (3b): Create the Runner Role with Instance Profile
First, create the EC2 trust policy (ec2-trust-policy.json):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}Create the runner role and instance profile:
# Create the runner role
aws iam create-role \
--role-name ControlinfraRunnerRole \
--assume-role-policy-document file://ec2-trust-policy.json \
--description "Runner role for Controlinfra self-hosted runner"
# Create instance profile
aws iam create-instance-profile \
--instance-profile-name ControlinfraRunnerProfile
# Attach role to instance profile
aws iam add-role-to-instance-profile \
--instance-profile-name ControlinfraRunnerProfile \
--role-name ControlinfraRunnerRoleStep 3 (3b): Grant AssumeRole Permission to Runner
Create the assume role permission policy (assume-role-policy.json):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::TARGET_ACCOUNT_1:role/ControlinfraTargetRole",
"arn:aws:iam::TARGET_ACCOUNT_2:role/ControlinfraTargetRole"
]
}
]
}Attach the policy to the runner role:
aws iam put-role-policy \
--role-name ControlinfraRunnerRole \
--policy-name AssumeRolePolicy \
--policy-document file://assume-role-policy.jsonStep 4 (3b): Launch EC2 Runner with Instance Profile
# Launch EC2 instance with the runner instance profile
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type t3.small \
--iam-instance-profile Name=ControlinfraRunnerProfile \
--key-name your-key-pair \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=controlinfra-runner}]'Step 5 (3b): Verify the Setup
SSH into your runner EC2 instance and verify:
# Check the instance identity (should show the runner role)
aws sts get-caller-identity
# Expected output:
# {
# "UserId": "AROA...:i-0123456789abcdef0",
# "Account": "123456789012",
# "Arn": "arn:aws:sts::123456789012:assumed-role/ControlinfraRunnerRole/i-0123456789abcdef0"
# }
# Test assuming the target role
aws sts assume-role \
--role-arn arn:aws:iam::TARGET_ACCOUNT:role/ControlinfraTargetRole \
--role-session-name test-session \
--query 'Credentials.{AccessKeyId:AccessKeyId,Expiration:Expiration}' \
--output table
# Expected output:
# -------------------------------------------------------
# | AssumeRole |
# +-----------------------+-----------------------------+
# | AccessKeyId | Expiration |
# +-----------------------+-----------------------------+
# | ASIA... | 2024-01-01T12:00:00+00:00 |
# +-----------------------+-----------------------------+Step 6 (3b): Full End-to-End Test
Run this script on your runner to verify the complete flow:
#!/bin/bash
echo "=== Current Identity (Instance Profile) ==="
aws sts get-caller-identity
echo ""
echo "=== Assuming Target Role ==="
CREDS=$(aws sts assume-role \
--role-arn arn:aws:iam::TARGET_ACCOUNT:role/ControlinfraTargetRole \
--role-session-name controlinfra-scan \
--output json)
export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r '.Credentials.SessionToken')
echo ""
echo "=== New Identity (Assumed Role) ==="
aws sts get-caller-identity
echo ""
echo "=== Testing AWS Access ==="
aws s3 ls --region us-east-1 | head -5Step 7 (3b): Configure in Controlinfra
- Go to Add Repository → AWS Authentication
- Select "Self-Hosted" runner type
- Select your registered runner
- Choose "Assume Role" authentication
- Enter the Role ARN:
arn:aws:iam::TARGET_ACCOUNT:role/ControlinfraTargetRole - Enter the External ID (optional, see below)
- Select your AWS region
AWS China and GovCloud Support
Controlinfra supports IAM role ARNs for all AWS partitions:
- Standard AWS:
arn:aws:iam::123456789012:role/RoleName - AWS China:
arn:aws-cn:iam::123456789012:role/RoleName - AWS GovCloud:
arn:aws-us-gov:iam::123456789012:role/RoleName
Using External ID (Recommended for Production)
The External ID prevents the "confused deputy" problem and adds an extra layer of security.
Update Trust Policy with External ID
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::RUNNER_ACCOUNT_ID:role/ControlinfraRunnerRole"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "your-unique-external-id-here"
}
}
}
]
}Test with External ID
# Test assuming role with external ID
aws sts assume-role \
--role-arn arn:aws:iam::TARGET_ACCOUNT:role/ControlinfraTargetRole \
--role-session-name test-session \
--external-id "your-unique-external-id-here" \
--query 'Credentials.{AccessKeyId:AccessKeyId,Expiration:Expiration}' \
--output tableBest Practices
- Use a unique, random string (e.g., UUID:
550e8400-e29b-41d4-a716-446655440000) - Store it securely - you'll need it for configuration
- Use different external IDs for different repositories/environments
- Generate with:
uuidgenorpython -c "import uuid; print(uuid.uuid4())"
WARNING
Always use an External ID when the role can be assumed by third parties or in production environments.
Troubleshooting Assume Role
"Access Denied" when assuming role
Check trust policy - Ensure the runner role ARN is in the Principal:
bashaws iam get-role --role-name ControlinfraTargetRole \ --query 'Role.AssumeRolePolicyDocument'Check runner permissions - Ensure the runner can call sts:AssumeRole:
bashaws iam list-role-policies --role-name ControlinfraRunnerRole aws iam get-role-policy --role-name ControlinfraRunnerRole \ --policy-name AssumeRolePolicyCheck External ID - If configured, ensure it matches exactly
"InvalidIdentityToken" error
- The runner's instance profile may not be properly attached
- Verify with:
aws sts get-caller-identity
Role assumed but actions fail
- The target role may lack required permissions
- Check attached policies:
aws iam list-attached-role-policies --role-name ControlinfraTargetRole
Option 4: OIDC Federation Enterprise
The most secure method — Controlinfra authenticates with AWS using OpenID Connect (OIDC) federation. No secrets are stored or exchanged. Controlinfra signs a short-lived JWT token and exchanges it for temporary AWS credentials via AssumeRoleWithWebIdentity.
Trust requirements
- Who Controlinfra authenticates as: Controlinfra's control plane signs a short-lived JWT (issuer =
https://api.controlinfra.com, subject =org:<your-org-id>); AWS STS exchanges it viaAssumeRoleWithWebIdentityfor temporary credentials under the IAM role you create below. - What you trust on Controlinfra's side: the Controlinfra OIDC issuer URL
https://api.controlinfra.com. Register it as an OIDC Identity Provider in your AWS account, then putarn:aws:iam::<your-account>:oidc-provider/api.controlinfra.comin your role'sPrincipal.Federatedand gate it with anapi.controlinfra.com:subcondition that matchesorg:<your-org-id>. The condition key prefix MUST match the issuer host exactly —controlinfra.com:subwill not match a JWT whoseissclaim ishttps://api.controlinfra.comand AWS will reject the AssumeRoleWithWebIdentity call withInvalidIdentityToken. - Where this works: cloud runner only (Enterprise plan). The token is signed by the control plane.
Enterprise Plan Required
OIDC Federation is available exclusively on the Enterprise plan.
When to Use
- Maximum security — zero stored secrets
- Cloud runner (Controlinfra-managed) — no self-hosted runner needed
- Compliance requirements that prohibit long-lived credentials
- Enterprise environments with strict credential policies
How It Works
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Controlinfra │ │ AWS STS │ │ Your AWS │
│ Cloud │ │ │ │ Account │
│ │──1──▶ AssumeRoleWith │──2──▶ │
│ Signs JWT token │ │ WebIdentity │ │ IAM Role │
│ │◀─3──│ (temp creds) │ │ │
└─────────────────────┘ └──────────────────┘ └─────────────┘Technical Flow
- Controlinfra signs a JWT with your organization's subject claim (
sub: org:<orgId>) - AWS STS verifies the token against Controlinfra's OIDC Identity Provider
- AWS returns temporary credentials (valid for 1 hour) for the target IAM role
- Controlinfra uses the temp credentials to scan your infrastructure
- Credentials expire automatically — nothing to rotate
Setup Steps
Step 1: Create an OIDC Identity Provider in Your AWS Account
First, retrieve the TLS certificate thumbprint for the OIDC issuer:
# Get the thumbprint for api.controlinfra.com
echo | openssl s_client -servername api.controlinfra.com \
-showcerts -connect api.controlinfra.com:443 2>/dev/null \
| openssl x509 -fingerprint -noout -sha1 \
| sed 's/://g' | cut -d= -f2Then create the provider with the thumbprint:
aws iam create-open-id-connect-provider \
--url https://api.controlinfra.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list "<THUMBPRINT_FROM_ABOVE>"TIP
AWS automatically fetches and validates the TLS certificate thumbprint for HTTPS issuers.
Step 2: Create the IAM Trust Policy
Create a file called oidc-trust-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/api.controlinfra.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"api.controlinfra.com:aud": "sts.amazonaws.com",
"api.controlinfra.com:sub": "org:YOUR_ORG_ID"
}
}
}
]
}Replace Placeholders
- Replace
YOUR_ACCOUNT_IDwith your 12-digit AWS account ID. - Replace
YOUR_ORG_IDwith your Controlinfra organization ID (found in Settings > General). This ensures only your organization can assume the role.
Step 3: Create the IAM Role
aws iam create-role \
--role-name ControlinfraOIDCRole \
--assume-role-policy-document file://oidc-trust-policy.json \
--description "Controlinfra OIDC federation role for drift scanning"
# Attach read-only permissions
aws iam attach-role-policy \
--role-name ControlinfraOIDCRole \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccessStep 4: Configure in Controlinfra
- Go to Settings → Integrations → Cloud Credentials
- Click Add Configuration
- Select OIDC Federation as the authentication method
- Enter a name (e.g.,
Production OIDC) - Enter the Role ARN:
arn:aws:iam::YOUR_ACCOUNT_ID:role/ControlinfraOIDCRole - Select your AWS region
- Click Add Configuration
Benefits
- Zero secrets stored — No access keys, no secret keys, nothing to leak
- No rotation needed — Temporary credentials are issued on-demand and expire automatically
- Cloud runner compatible — Works with Controlinfra's managed infrastructure, no self-hosted runner required
- Audit-friendly — All actions are tracked under the IAM role with the session name
controlinfra-oidc - Scoped access — The trust policy condition ensures only your Controlinfra organization can assume the role
Troubleshooting OIDC
"InvalidIdentityToken" error
- Verify the OIDC Identity Provider URL is exactly
https://api.controlinfra.com(the JWT'sissclaim is signed with this exact value — even a host mismatch likecontrolinfra.comvsapi.controlinfra.comwill fail validation) - Verify the trust-policy condition keys use the
api.controlinfra.com:prefix (must match the issuer host) - Check the audience is set to
sts.amazonaws.com - Ensure the OIDC provider's thumbprint is up to date
"AccessDenied" when assuming role
- Verify the trust policy
Principal.Federatedmatches your OIDC provider ARN - Check that the
Conditionblock matches exactly - Ensure the role has the required permissions attached
"ExpiredTokenException"
- This is normal if a scan takes longer than 1 hour — Controlinfra automatically refreshes credentials
Required IAM Permissions
Controlinfra needs two types of permissions:
- Read-Only Access to AWS resources for drift detection
- Terraform State Access to read state from S3 and manage DynamoDB locks
Recommended: AWS Managed Policy + Custom State Policy
The simplest and most maintainable approach:
# Attach AWS managed ReadOnlyAccess policy
aws iam attach-role-policy \
--role-name YourRoleName \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
# Then add a custom inline policy for Terraform state access
aws iam put-role-policy \
--role-name YourRoleName \
--policy-name TerraformStateAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TerraformStateS3",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::YOUR-STATE-BUCKET",
"arn:aws:s3:::YOUR-STATE-BUCKET/*"
]
},
{
"Sid": "TerraformDynamoDBLock",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:*:*:table/terraform-*"
}
]
}'Replace Placeholder Values
Replace YOUR-STATE-BUCKET with your actual Terraform state S3 bucket name.
Why ReadOnlyAccess?
The AWS managed ReadOnlyAccess policy covers all AWS services and is automatically updated by AWS when new services are added. This ensures Controlinfra can detect drift in any resource Terraform manages without requiring policy updates.
Alternative: Minimal Custom Policy
If you prefer a more restrictive policy, use this custom policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TerraformStateAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-terraform-state-bucket",
"arn:aws:s3:::your-terraform-state-bucket/*"
]
},
{
"Sid": "DynamoDBLocking",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:*:*:table/terraform-locks"
},
{
"Sid": "ReadOnlyInfrastructure",
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"s3:GetBucket*",
"s3:GetObject*",
"s3:ListBucket*",
"rds:Describe*",
"elasticloadbalancing:Describe*",
"autoscaling:Describe*",
"cloudwatch:Describe*",
"cloudwatch:GetMetricStatistics",
"iam:GetRole",
"iam:GetPolicy",
"iam:GetUser",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
"lambda:GetFunction",
"lambda:ListFunctions",
"sns:GetTopicAttributes",
"sqs:GetQueueAttributes",
"route53:GetHostedZone",
"route53:ListResourceRecordSets",
"cloudfront:GetDistribution",
"acm:DescribeCertificate"
],
"Resource": "*"
}
]
}Full Infrastructure Policy
For comprehensive coverage of all AWS services:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOnlyAccess",
"Effect": "Allow",
"Action": [
"acm:Describe*",
"acm:Get*",
"acm:List*",
"apigateway:GET",
"autoscaling:Describe*",
"cloudfront:Get*",
"cloudfront:List*",
"cloudwatch:Describe*",
"cloudwatch:Get*",
"cloudwatch:List*",
"cognito-idp:Describe*",
"cognito-idp:List*",
"dynamodb:Describe*",
"dynamodb:List*",
"ec2:Describe*",
"ecr:Describe*",
"ecr:Get*",
"ecr:List*",
"ecs:Describe*",
"ecs:List*",
"eks:Describe*",
"eks:List*",
"elasticache:Describe*",
"elasticloadbalancing:Describe*",
"es:Describe*",
"es:List*",
"events:Describe*",
"events:List*",
"iam:Get*",
"iam:List*",
"kms:Describe*",
"kms:Get*",
"kms:List*",
"lambda:Get*",
"lambda:List*",
"logs:Describe*",
"logs:Get*",
"rds:Describe*",
"rds:List*",
"route53:Get*",
"route53:List*",
"s3:GetBucket*",
"s3:GetObject*",
"s3:ListBucket*",
"secretsmanager:Describe*",
"secretsmanager:List*",
"sns:Get*",
"sns:List*",
"sqs:Get*",
"sqs:List*",
"ssm:Describe*",
"ssm:Get*",
"ssm:List*"
],
"Resource": "*"
},
{
"Sid": "TerraformState",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-terraform-state-bucket",
"arn:aws:s3:::your-terraform-state-bucket/*"
]
},
{
"Sid": "TerraformLocking",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:*:*:table/terraform-locks"
}
]
}Security Best Practices
1. Use Least Privilege
Only grant permissions for resources Terraform manages:
{
"Effect": "Allow",
"Action": ["ec2:Describe*"],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/ManagedBy": "terraform"
}
}
}2. Rotate Credentials Regularly
- Set up a rotation schedule (e.g., every 90 days)
- Use AWS Secrets Manager for automated rotation
- Update Controlinfra settings after rotation
3. Monitor Credential Usage
Enable CloudTrail to track API calls:
{
"eventSource": "sts.amazonaws.com",
"eventName": "AssumeRole",
"userIdentity": {
"userName": "Controlinfra-scanner"
}
}4. Use Self-Hosted Runners
For production environments:
- Credentials stay in your AWS account
- Use IAM roles instead of access keys
- Better audit trail and compliance
Multiple AWS Accounts
If you manage infrastructure across multiple AWS accounts:
Option 1: Separate Credentials per Repository
Configure different AWS credentials for each repository in Controlinfra.
Option 2: Cross-Account Assume Role
- Create a role in each target account:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::SCANNER_ACCOUNT:user/Controlinfra-scanner"
},
"Action": "sts:AssumeRole"
}
]
}- Grant assume role permission to the scanner user:
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::ACCOUNT_1:role/Controlinfra-scanner",
"arn:aws:iam::ACCOUNT_2:role/Controlinfra-scanner"
]
}Credential Storage
Controlinfra secures your credentials:
- Encryption: All credentials are encrypted at rest using AES-256
- Access Control: Credentials are only decrypted during scan execution
- No Logging: Credentials are never written to logs
- Isolation: Each user's credentials are isolated
Troubleshooting
"Access Denied" Errors
- Check IAM policy is attached to the user
- Verify the resource ARNs in the policy
- Check for Service Control Policies (SCPs) in AWS Organizations
"InvalidClientTokenId" Error
- Access Key ID may be incorrect
- User may have been deleted
- Credentials may have been deactivated
"ExpiredToken" Error
- If using temporary credentials, they may have expired
- Generate new access keys
State File Access Issues
Error: Failed to load state: AccessDenied- Verify S3 bucket permissions
- Check bucket policy allows the IAM user
- Ensure the state file key path is correct
Next Steps
- Set Up Self-Hosted Runners - Enhanced security option
- Configure Terraform Backend - Backend settings
- Run Your First Scan - Start scanning