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

  1. Session Timeout: Default is 1 hour and max is 12 hours based on Role Configuration
  2. Token Size : Session tokens are big - some older tools have trouble with the size of the token
  3. External ID: Case sensitive and should match exactly
  4. 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:

  1. Your IAM user has a policy allowing sts:AssumeRole on the client’s role
  2. Client’s role is one of a trust relationship which allows your account to assume it
  3. You call AssumeRole with the role ARN and external ID
  4. AWS returns temporary credentials (access key, secret key, and session token)
  5. Use the temporary credentials to access the client’s resources.
  6. 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:

  1. Client side: Create role with trust relationship
  2. Your side: Have permission to assume roles
  3. 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.