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 three 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 | Self-Hosted Only | Highest | Cross-account, production |
Option 1: Access Keys
The simplest method using AWS Access Key ID and Secret Access Key.
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!
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.
Self-Hosted Runner Required
This option requires a self-hosted runner with an Instance Profile that can assume the target role.
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
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ 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.
Setup Steps
Step 1: 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: 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: 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: 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: 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: 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: 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
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