Every consultant or contractor has experienced this: “We need you to work on our AWS infrastructure but we can’t give you our AWS keys.” Clever clients! Here’s the full guide to setting up secure cross-account access with AWS AssumeRole.
The Problem
Sharing AWS access keys is a security nightmare.
- Long-lived credentials = permanent security risk
- No audit trail of who did what .
- Access cannot be revoked without changing keys everywhere
- Compliance violations (SOC2, ISO 27001 etc.)
The Solution: AssumeRole
AssumeRole needs configuration on BOTH sides:
Your AWS Account (123456789012) Client's AWS Account (987654321098)
| |
Your IAM User Target IAM Role
| |
Needs Permission <------- AssumeRole -----> Trust Relationship
to AssumeRole trusts your account
Complete Setup Guide
Step 1: Client Creates the Role (Client’s Side)
The client needs to create an IAM role with two parts:
A. Trust Relationship (Who can assume this role)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root" // Your AWS account
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-external-id-xyz123" // Shared secret
}
}
}
]
}
Important: The trust relationship goes in the “Trust relationships” tab, NOT in the regular permissions!
B. Permissions Policy (What the role can do)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"rds:Describe*",
"s3:ListBucket",
"s3:GetObject"
],
"Resource": "*"
}
]
}
Step 2: You Need AssumeRole Permission (Your Side)
This is the part often missed! Your IAM user needs permission to assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::987654321098:role/ContractorRole"
}
]
}
Or if you work with multiple clients:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::987654321098:role/ContractorRole",
"arn:aws:iam::876543210987:role/ConsultantRole",
"arn:aws:iam::765432109876:role/DeveloperRole"
]
}
]
}
Step 3: Test the Setup
# First, verify you have the permission to assume roles
aws iam get-user
aws iam list-attached-user-policies --user-name your-username
# Then try to assume the role
aws sts assume-role \
--role-arn "arn:aws:iam::987654321098:role/ContractorRole" \
--role-session-name "test-session" \
--external-id "unique-external-id-xyz123"
Step 4: Configure AWS CLI Profiles
Add to ~/.aws/config:
[profile default]
region = us-east-1
[profile client-prod]
role_arn = arn:aws:iam::987654321098:role/ContractorRole
source_profile = default
external_id = unique-external-id-xyz123
region = us-east-1
Now you can use:
aws s3 ls --profile client-prod
Using AssumeRole in Go
Here’s how to use AssumeRole programmatically:
package main
import (
"fmt"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
func main() {
// Create base session with your credentials
sess := session.Must(session.NewSession())
// Create credentials using AssumeRole
creds := stscreds.NewCredentials(sess,
"arn:aws:iam::987654321098:role/ContractorRole",
func(p *stscreds.AssumeRoleProvider) {
p.ExternalID = aws.String("unique-external-id-xyz123")
p.Duration = 3600 * time.Second
},
)
// Create new session with assumed role
clientSess := session.Must(session.NewSession(&aws.Config{
Credentials: creds,
Region: aws.String("us-east-1"),
}))
// Use the session
svc := s3.New(clientSess)
result, err := svc.ListBuckets(nil)
if err != nil {
panic(err)
}
for _, bucket := range result.Buckets {
fmt.Println(*bucket.Name)
}
}
Common Gotchas
- Session Timeout: Default is 1 hour and max is 12 hours based on Role Configuration
- Token Size : Session tokens are big - some older tools have trouble with the size of the token
- External ID: Case sensitive and should match exactly
- Trust Policy vs Permissions Trust policy controls WHO can assume the role, permissions controls What the role can offer
How it all fits together
Here is the complete flow:
- Your IAM user has a policy allowing
sts:AssumeRoleon the client’s role - Client’s role is one of a trust relationship which allows your account to assume it
- You call AssumeRole with the role ARN and external ID
- AWS returns temporary credentials (access key, secret key, and session token)
- Use the temporary credentials to access the client’s resources.
- Credentials expire in the given time (1-12 hours)
Why This Is Better
- No permanent credentials shared - Everything is temporary
- Full audit trail - Every action is logged with your role session name
- Easy to revoke - Client just deletes the role or removes the trust relationship
- Principle of least privilege - Client controls exactly what you can access
- Compliance friendly - Meets most security framework requirements
Conclusion
AssumeRole requires setup on both sides:
- Client side: Create role with trust relationship
- Your side: Have permission to assume roles
- Both sides: Agree on external ID
This approach keeps everyone secure with full audit trails and no shared credentials. Next time a client asks “how do we give you access?”, you’ll know exactly what to do.