EC2 Instance Profiles and Trust Policies
An instance profile and a trust policy are frequently discussed as alternatives. They are not: an instance profile is the delivery mechanism for credentials on EC2, and a trust policy is part of every role, including the one inside the instance profile.
EC2 instance profile
Section titled “EC2 instance profile”- A container for exactly one IAM role, attached to an EC2 instance
- The instance obtains temporary credentials for that role from the instance metadata service, and AWS rotates them automatically before they expire
- Every AWS SDK and the AWS CLI pick those credentials up from the metadata service without configuration
- Attach, replace or detach the profile on a running instance without stopping it
- The correct mechanism for any EC2 instance that needs to call an AWS service
The role inside the instance profile has a trust policy naming the EC2 service principal. That is what allows EC2 to assume it on the instance’s behalf:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ]}Trust policy
Section titled “Trust policy”A trust policy is the resource-based policy on an IAM role that says who may assume it. Every role has exactly one. Valid principals are AWS accounts and their root, IAM roles and users, role sessions, federated principals (SAML or OIDC) and AWS service principals.
An EC2 instance ARN is not a valid principal. A trust policy naming
arn:aws:ec2:...:instance/i-... is rejected, and there is no way to write a role that trusts
one particular instance directly. Constraining a role to a specific instance is done through
a condition — for example on aws:SourceVpc, aws:SourceIp or a tag — or by giving that
instance its own profile and role.
Trust policies are used for:
- Cross-account access — a role in one account trusting a principal in another
- Service-to-service access — Lambda, ECS tasks, CodeBuild and the rest assuming a role
- Federation — trusting a SAML or OIDC identity provider
- Letting an AWS service such as CloudFormation act on your behalf
Worked example: an EC2 instance reading DynamoDB
Section titled “Worked example: an EC2 instance reading DynamoDB”1. The permission policy
Section titled “1. The permission policy”{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query" ], "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable" } ]}2. The role, trusted by EC2
Section titled “2. The role, trusted by EC2”aws iam create-role --role-name EC2DynamoDBRole --assume-role-policy-document '{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ]}'3. The instance profile
Section titled “3. The instance profile”aws iam create-instance-profile --instance-profile-name EC2DynamoDBProfile
aws iam add-role-to-instance-profile \ --instance-profile-name EC2DynamoDBProfile \ --role-name EC2DynamoDBRole
aws ec2 associate-iam-instance-profile \ --instance-id i-1234567890abcdef0 \ --iam-instance-profile Name=EC2DynamoDBProfile4. The application
Section titled “4. The application”Nothing about credentials appears in the code. The SDK resolves them from the instance metadata service and refreshes them as they rotate.
import boto3
dynamodb = boto3.resource('dynamodb')table = dynamodb.Table('MyTable')
response = table.get_item(Key={'id': '123'})Worked example: reaching a resource in another account
Section titled “Worked example: reaching a resource in another account”This is where an explicit role assumption is genuinely required — not as an inferior alternative to an instance profile, but because the resource is somewhere the instance profile’s role does not reach.
The instance runs in account 123456789012; the DynamoDB table lives in account 444455556666. The instance keeps its instance profile, and the role in the table’s account trusts the instance profile’s role:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123456789012:role/EC2DynamoDBRole" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "example-external-id" } } } ]}The application chains from the instance profile’s credentials into the second role:
import boto3
sts_client = boto3.client('sts')assumed_role = sts_client.assume_role( RoleArn='arn:aws:iam::444455556666:role/CrossAccountDynamoDBRole', RoleSessionName='order-service', ExternalId='example-external-id')
credentials = assumed_role['Credentials']dynamodb = boto3.resource( 'dynamodb', aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'])
table = dynamodb.Table('MyTable')response = table.get_item(Key={'id': '123'})Note that the instance still has no stored credential. It starts from the instance profile and chains outwards, which is why the chained session is capped at one hour.
Summary
Section titled “Summary”- Same account: use an instance profile. There is no better option and no reason to manage credentials by hand.
- Another account, or a service acting on your behalf: the target role’s trust policy
names the principal that may assume it, and the caller chains into it with
sts:AssumeRole. - Either way, no long-lived access key is stored anywhere.