Skill 79 · Setting Up EC2 Instance Profiles
Subchapter 79.1
references/ec2-instance-profile-setup.mdMarkdown33 KBView on GitHub
This SOP guides you through the complete process of granting an EC2 instance permissions to call AWS services securely using IAM roles and instance profiles. Instead of embedding AWS credentials in your application code, instance profiles allow EC2 instances to assume IAM roles and obtain temporary credentials automatically. This SOP helps identify required permissions, creates or uses an existing IAM role, sets up the instance profile, and attaches it to the target EC2 instance.
Prompt the user in a single message to provide all required parameters at once. Clearly list the required parameters and their descriptions, and include any optional parameters with their default values. Do not proceed until you have received and confirmed all required parameters. If any required parameter is missing or unclear, you MUST explicitly request the missing information before moving forward.
Only proceed to the steps below if you have all required information.
Check for required tools and warn the user if any are missing.
Constraints:
Confirm the target EC2 instance exists and retrieve its current configuration.
Constraints:
aws ec2 describe-instances --instance-ids ${instance_id} --region ${region}IamInstanceProfile field in the responsedescribe-instances output for reuse in later steps (e.g., to reference association IDs in Step 8)Determine whether to create a new IAM role or reuse an existing one, then verify the selected option.
Constraints:
You MUST verify the role exists using: aws iam get-role --role-name ${role_name} --region ${region}
You MUST retrieve the role’s trust policy to verify it allows EC2 service to assume it
You MUST check the trust policy contains:
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}You MUST list all policies attached to the role using: aws iam list-attached-role-policies --role-name ${role_name}
You MUST list all inline policies using: aws iam list-role-policies --role-name ${role_name}
You MUST present the existing permissions to the user for review
You MUST ask the user if they want to add additional permissions or use the role as-is
You MUST handle the case where the role does not exist and inform the user
You MUST verify the role has the correct trust relationship for EC2, and if not, ask the user if they want to update it
Analyze the requested services and determine appropriate IAM permissions.
Constraints:
services_needed and recommend appropriate permissionss3:GetObject, s3:PutObject, s3:ListBucket on specific bucketsdynamodb:GetItem, dynamodb:PutItem, dynamodb:Query, dynamodb:Scan on specific tablessqs:SendMessage, sqs:ReceiveMessage, sqs:DeleteMessage on specific queuessns:Publish on specific topicslambda:InvokeFunction on specific functionslogs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEventssecretsmanager:GetSecretValue on specific secretsssm:GetParameter, ssm:GetParameters on specific parameterskms:Decrypt, kms:Encrypt on specific keysec2:DescribeInstances, ec2:DescribeTagsAmazonS3FullAccess, AmazonDynamoDBFullAccess, AmazonSQSFullAccess, AmazonSNSFullAccess, SecretsManagerReadWrite, CloudWatchLogsFullAccess)AmazonS3ReadOnlyAccess)*:* actions or resources)Create a new IAM role or update an existing one with the identified permissions.
Constraints:
You MUST skip role creation if the user chooses to reuse an existing role without modifications
You MUST create the trust policy document that allows EC2 to assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}You MUST save the trust policy to a temporary file or use inline JSON
You MUST create the IAM role using: aws iam create-role --role-name ${role_name} --assume-role-policy-document file://trust-policy.json --description "IAM role for EC2 instance ${instance_id} to access ${services_needed}"
You MUST handle the case where the role already exists with a clear message
You MUST add tags to the role for better tracking: aws iam tag-role --role-name ${role_name} --tags Key=ManagedBy,Value=ec2-instance-profile-setup Key=InstanceId,Value=${instance_id} Key=CreatedDate,Value=$(date +%Y-%m-%d)
You MUST wait for the role to be created before proceeding (typically immediate, but verify with describe command)
You MUST verify the role was created successfully using: aws iam get-role --role-name ${role_name}
Attach the necessary AWS managed policies or create and attach custom inline policies.
Constraints:
You MUST attach each AWS managed policy using: aws iam attach-role-policy --role-name ${role_name} --policy-arn ${policy_arn}
You MUST use proper policy ARNs in the format: arn:aws:iam::aws:policy/${policy_name}
Common managed policy ARNs to use (PREFER LEAST PRIVILEGE — avoid FullAccess policies):
arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess (acceptable for read-only use cases)arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess (acceptable for read-only use cases)arn:aws:iam::aws:policy/AmazonSQSReadOnlyAccess (acceptable for queue monitoring/inspection only — consumers that process messages need a custom policy with sqs:ReceiveMessage and sqs:DeleteMessage)arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCoreYou MUST NOT recommend or attach FullAccess or overly broad managed policies (e.g., AmazonS3FullAccess, AmazonDynamoDBFullAccess, AmazonSQSFullAccess, AmazonSNSFullAccess, SecretsManagerReadWrite, CloudWatchLogsFullAccess). Instead, create custom policies scoped to specific resources.
You MUST prefer custom inline policies over managed policies for write access:
aws iam put-role-policy --role-name ${role_name} --policy-name ${policy_name} --policy-document file://custom-policy.jsonYou MUST validate that all policies were attached successfully
You MUST list all attached policies to confirm: aws iam list-attached-role-policies --role-name ${role_name}
You MUST also list inline policies to confirm: aws iam list-role-policies --role-name ${role_name}
You MUST handle errors such as invalid policy ARNs or permission issues
You MUST inform the user of all attached policies
Create an instance profile that wraps the IAM role for EC2 use.
Constraints:
aws iam get-instance-profile --instance-profile-name ${role_name}aws iam create-instance-profile --instance-profile-name ${role_name}aws iam add-role-to-instance-profile --instance-profile-name ${role_name} --role-name ${role_name}aws iam get-instance-profile --instance-profile-name ${role_name}Associate the instance profile with the target EC2 instance.
Constraints:
aws ec2 disassociate-iam-instance-profile --association-id ${association_id}aws ec2 associate-iam-instance-profile --instance-id ${instance_id} --iam-instance-profile Name=${role_name} --region ${region}aws ec2 describe-instances --instance-ids ${instance_id} --region ${region}IamInstanceProfile field now contains the correct instance profile ARNConfirm the instance profile is properly configured and test that credentials are accessible.
Constraints:
You MUST verify the complete configuration by checking:
You MUST provide instructions for testing the configuration from within the instance using IMDSv2 (token-based):
# SSH into the instance and run:
# 1. Get an IMDSv2 session token (valid for 6 hours)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# 2. Verify instance metadata service can provide credentials
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 3. Retrieve temporary credentials (will show role name)
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/${role_name}
# 4. Test AWS CLI with the instance profile (no credentials needed in CLI config)
aws sts get-caller-identity
# 5. Test access to a specific service (example for S3)
aws s3 lsYou MUST NOT use IMDSv1 (plain curl without token) — always use IMDSv2 with a session token
You MUST explain that applications using AWS SDKs will automatically use these credentials
You MUST provide code examples for common SDK languages to verify automatic credential resolution:
Python (boto3):
import boto3
# No credentials needed - automatically uses instance profile
s3 = boto3.client('s3')
print(s3.list_buckets())Node.js (AWS SDK v3):
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
// No credentials needed - automatically uses instance profile
const client = new S3Client({ region: "us-east-1" });
You MUST remind the user to remove any hardcoded AWS credentials from their application code
You MUST warn about credential caching - applications may need to be restarted to pick up the new credentials
Create a comprehensive report documenting the setup.
Constraints:
You MUST create a detailed summary report containing:
You MUST include security recommendations:
You MUST provide instructions for updating permissions in the future:
# To add more policies:
aws iam attach-role-policy --role-name ${role_name} --policy-arn ${new_policy_arn}
# To remove policies:
aws iam detach-role-policy --role-name ${role_name} --policy-arn ${policy_arn}
# To update inline policies:
aws iam put-role-policy --role-name ${role_name} --policy-name ${policy_name} --policy-document file://updated-policy.jsonYou MUST provide instructions for cleanup if needed:
# To remove the instance profile from the instance:
aws ec2 disassociate-iam-instance-profile --association-id ${association_id}
# To delete the instance profile:
aws iam remove-role-from-instance-profile --instance-profile-name ${role_name} --role-name ${role_name}
aws iam delete-instance-profile --instance-profile-name ${role_name}
# To delete the role (must detach policies first):
aws iam detach-role-policy --role-name ${role_name}
You MUST format the report in a clear, well-organized manner
You MUST present the report to the user
instance_id: i-0abcd1234efgh5678
region: us-east-1
services_needed: s3,dynamodb,cloudwatch
role_name: web-server-roleDuring Step 3, the user chose to create a new IAM role.
# EC2 Instance Profile Setup Report
**Instance ID:** i-0abcd1234efgh5678
**Region:** us-east-1
**IAM Role:** web-server-role
**Instance Profile:** web-server-role
## Configuration Summary
### Instance Details
- **Instance ID:** i-0abcd1234efgh5678
- **Instance Name:** web-server-prod-01
- **Instance State:** running
- **Previous Instance Profile:** None
- **New Instance Profile:** web-server-role
### IAM Role Configuration
- **Role Name:** web-server-role
- **Role ARN:** arn:aws:iam::123456789012:role/web-server-role
- **Trust Policy:** Configured to allow EC2 service to assume role
- **Created:** 2025-10-13
### Attached Policies
#### Least Privilege Policy Examples
**SECURITY BEST PRACTICE: Always use the minimum permissions required for your use case.**
#### Custom S3 Policy (Specific Bucket Access)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:log-group:/aws/ec2/my-app:*"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:*:*:table/my-app-table"
}
]
}AmazonS3ReadOnlyAccess
arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccessAmazonDynamoDBReadOnlyAccess
arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccessCloudWatchLogsWrite (scoped to specific log group):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:log-group:/aws/ec2/web-server-prod-01:*"
}
]
}Follow the test instructions below to verify from within the instance.
SSH into your instance and run these commands:
# 1. Get an IMDSv2 session token (valid for 6 hours)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# 2. Check if instance profile is available
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Expected output: web-server-role
# 3. Retrieve temporary credentials
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/web-server-role
# Expected output: JSON with AccessKeyId, SecretAccessKey, Token
# 4. Verify AWS CLI can use the credentials
aws sts get-caller-identity
# Expected output: Your account ID, user ID, and role ARN
# 5. Test S3 access
aws s3 ls
# Expected output: List of S3 buckets (if any exist)
# 6. Test DynamoDB access
aws dynamodb list-tables
# Expected output: List of DynamoDB tables (if any exist)
# 7. Test CloudWatch Logs write access
aws logs create-log-group --log-group-name /aws/ec2/web-server-prod-01
# Expected output: (none on success; ResourceAlreadyExistsException if it already exists)
import boto3
# No explicit credentials needed - boto3 automatically uses instance profile
s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
# Test S3 access
buckets = s3.list_buckets()
print(f"Found {len(buckets['Buckets'])} S3 buckets")
# Test DynamoDB access
table = dynamodb.Table('your-table-name')
response = table.get_item(Key={'id': '123'})
print(response)import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb";
// No explicit credentials needed - SDK automatically uses instance profile
const s3Client = new S3Client({ region: "us-east-1" });
const dynamoClient = new DynamoDBClient({ region: "us-east-1" });
// Test S3 access
const s3Response = await s3Client.send(new ListBucketsCommand({}));
console.log(`Found ${s3Response.Buckets.length} S3 buckets`);
// Test DynamoDB access
const dynamoResponse = await dynamoClient.send(new ListTablesCommand({}));
console.log(`Found ${dynamoResponse.TableNames.length} DynamoDB tables`);import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.regions.Region;
// No explicit credentials needed - SDK automatically uses instance profile
S3Client s3 = S3Client.builder()
.region(Region.US_EAST_1)
.build();
DynamoDbClient dynamoDb = DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
// Test S3 access
var s3Response = s3.listBuckets();
System.out.println("Found " + s3Response.buckets().size() + " S3 buckets");
// Test DynamoDB access
var dynamoResponse = dynamoDb.listTables();
System.out.println("Found " + dynamoResponse.tableNames().size() + " DynamoDB tables");Apply Least Privilege Principle
Current setup uses managed policies with broad permissions
Consider creating custom policies with specific resource ARNs
Example for S3 bucket-specific access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-specific-bucket",
"arn:aws:s3:::your-specific-bucket/*"
]
}
]
}Enable CloudTrail Logging
# Track all API calls made using this role
aws cloudtrail create-trail --name instance-audit-trail \
--s3-bucket-name your-cloudtrail-bucket
aws cloudtrail start-logging --name instance-audit-trailSet Up CloudWatch Alarms
Regular Permission Audits
Use Resource Tags
Consider Using IAM Policy Conditions
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::your-specific-bucket/*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/Environment"
Use with tag-based access control (ABAC): tag the IAM role with Environment=production and add aws:PrincipalTag conditions to the IAM policy (as shown above) to restrict access based on the role’s tags. This lets you manage access across many instances without updating policies individually.
# Attach additional managed policy
aws iam attach-role-policy \
--role-name web-server-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# Add custom inline policy (RECOMMENDED - most secure)
aws iam put-role-policy \
--role-name web-server-role \
--policy-name CustomS3Access \
--policy-document file://custom-policy.json# Detach managed policy
aws iam detach-role-policy \
--role-name web-server-role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
# Delete inline policy
aws iam delete-role-policy \
--role-name web-server-role \
--policy-name CustomS3Access# Update inline policy (overwrites existing)
aws iam put-role-policy \
--role-name web-server-role \
--policy-name CustomS3Access \
--policy-document file://updated-policy.jsonIf you need to remove this configuration:
# 1. Disassociate instance profile from instance
aws ec2 disassociate-iam-instance-profile \
--association-id iip-assoc-0abcd1234efgh5678
# 2. Remove role from instance profile
aws iam remove-role-from-instance-profile \
--instance-profile-name web-server-role \
--role-name web-server-role
# 3. Delete instance profile
aws iam delete-instance-profile \
--instance-profile-name web-server-role
# 4. Detach all policies from role
aws iam delete-role-policy \
--role-name web-server-role \
--policy-name CloudWatchLogsWrite
aws iam detach-role-policy \
--role-name web-server-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
aws iam detach-role-policy \
--role-name web-server-role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
aws iam detach-role-policy \
--role-name web-server-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# 5. Delete the IAM role
aws iam delete-role --role-name web-server-roleaws ec2 describe-instances --instance-ids i-0abcd1234efgh5678Successfully configured EC2 instance i-0abcd1234efgh5678 to securely access AWS services using IAM role web-server-role. The instance can now access S3, DynamoDB, and CloudWatch Logs without hardcoded credentials. Follow the testing instructions above to verify the configuration and remove any existing hardcoded credentials from your application code.
## Troubleshooting
### EC2 Instance Does Not Exist
If the specified instance ID is not found, verify you are using the correct instance ID and region. Use `aws ec2 describe-instances --region ${region}` to list all instances in the region.
### Instance Already Has an Instance Profile
If the instance already has an instance profile attached, the script will prompt you to confirm whether you want to replace it. Replacing an instance profile will immediately change the permissions available to applications running on the instance.
### IAM Role Name Conflicts
If a role with the specified name already exists but has different configurations, you MUST prompt the user either to choose a different name or to confirm updating the existing role. Consider using descriptive, unique names like `{application}-{environment}-{instance-name}-role`.
### Permission Denied Errors
Ensure your AWS credentials have the necessary IAM permissions to create roles, instance profiles, and attach policies. Required permissions include:
- `iam:CreateRole`
- `iam:GetRole`
- `iam:AttachRolePolicy`
- `iam:CreateInstanceProfile`
- `iam:AddRoleToInstanceProfile`
- `ec2:AssociateIamInstanceProfile`
- `ec2:DescribeInstances`
- `ec2:DisassociateIamInstanceProfile`
### Instance Profile Takes Time to Propagate
After attaching an instance profile, it may take 30-60 seconds for the credentials to become available in the instance metadata service. Applications may need to retry credential requests or be restarted.
### Trust Policy Validation Failures
If you're using an existing role and the trust policy doesn't allow EC2 to assume it, you'll need to update the trust policy using:
```bash
aws iam update-assume-role-policy --role-name ${role_name} --policy-document file://trust-policy.jsonIf you receive warnings about overly permissive policies, consider using more restrictive permissions with specific resource ARNs rather than wildcard (*) resources. This follows the principle of least privilege and reduces security risks.
Even after setting up an instance profile, applications may continue using hardcoded credentials if they are explicitly configured. You must remove any AWS credentials from:
If your application needs permissions from multiple existing roles, you cannot attach multiple instance profiles to a single instance. Instead, you must create a new role that combines all required permissions from the multiple roles.
Ensure that the policies grant access to resources in the correct regions. Some AWS services are region-specific, and you may need to specify resources with region-aware ARNs.
This file
Java (AWS SDK v2):
import software.amazon.awssdk.services.s3.S3Client;
// No credentials needed - automatically uses instance profile
S3Client s3 = S3Client.builder().region(Region.US_EAST_1).build();
s3.listBuckets();