Cross-Account Access Overview
Welcome to the most comprehensive guide onAWScross-account access! We're going to walk through every single step, every parameter, and every response you'll see when setting up cross-account IAM roles.
Think of cross-account access like giving someone from another company a visitor badge that works in your building. The trust policy is like the security desk checking their credentials, and the permissions policy determines which floors and rooms they can access.
Today we'll cover Account A (the trusting account) creating roles that Account B (the trusted account) can assume. I'll explain every singleAWSCLI parameter, what it does, why we use it, and exactly what response you'll get back.
We'll be using real account IDs throughout this demo, so you can follow along step by step.
Understanding the Architecture
Before we dive into commands, let's understand exactly what we're building. We have twoAWSaccounts that need to work together securely.
Account A for Production wants to allow specific users from Account B to access certain resources, but with strict controls.
The beauty of cross-account roles is that Account B users don't need permanent credentials in Account A. They use their own credentials to assume a temporary role, getting short-lived tokens.
This is much more secure than creating permanent users in each account or sharing access keys. The credentials expire automatically, and you can track exactly who accessed what and when.
We'll see exactly how the Security Token Service (STS) orchestrates this dance between accounts.
Setting Up the Environment
Let's start by setting up ourAWSCLI environment properly. This is crucial because we'll be working with multiple accounts, and you need to be absolutely sure which account context you're operating in.
The first command,AWSsts get-caller-identity, is your best friend when working with multiple accounts. It tells you exactly which credentials you're using right now.
aws sts get-caller-identity breakdown:
- sts: Security Token Service
- get-caller-identity: Returns details about the IAM user or role whose credentials are used to call the operation
- No parameters needed - uses your currentAWScredentials
When you run this, you'll get back three critical pieces of information: your User ID (or Role ID), your Account number, and your ARN. This confirms you're working in the right account with the right permissions.
TheAWSconfigure list command shows you exactly which credential source is being used - is it from ~/.aws/credentials, environment variables, or an IAM role? This is essential for troubleshooting.
Creating the Trust Policy Document
Now we're creating the heart of cross-account access - the trust policy. This JSON document is like a whitelist that says "these specific entities from these specific accounts are allowed to assume this role."
I'm using a here-document (EOF syntax) to create the JSON file cleanly. This is much better than trying to escape quotes in a command line.
Trust Policy Components:
- Version: Always use "2012-10-17" (the latest policy language version)
- Effect: "Allow" grants permission (vs "Deny")
- Principal: WHO can assume this role
- Action: WHAT they can do (sts:AssumeRole for role assumption)
- Condition: Optional additional restrictions
The Principal "arn:aws:iam::account B:user/DevUser" is very specific - it allows only the user named "DevUser" from account account B. You could also use "arn:aws:iam::account B:root" to allow any user from that account, but that's less secure.
The ExternalId condition is crucial for security - it prevents the "confused deputy" problem where a malicious user might trick a service into performing actions on the wrong account.
Creating the Cross-Account Role
Now we're actually creating the IAM role using the trust policy we just defined. Let me break down every single parameter in thisAWSiam create-role command.
Parameter breakdown:
- --role-name: The name of the role (must be unique in your account)
- --assume-role-policy-document: The trust policy (who can assume this role)
- file://: TellsAWSCLI to read from a local file
- --description: Human-readable description (optional but recommended)
- --path: Organizes roles hierarchically (optional, defaults to "/")
When this command succeeds,AWSreturns a complete Role object with the ARN, creation date, and the trust policy you just attached. The ARN is what you'll use to reference this role from other accounts.
If you get an error here, it's usually because: 1) You don't have iam:CreateRole permission, 2) A role with that name already exists, or 3) There's a JSON syntax error in your trust policy.
The role exists now, but it has no permissions yet - it's like creating an empty security badge. We'll add permissions next.
Adding Permissions to the Role
A role without permissions is useless, so now we're going to attach a permissions policy. I'm showing you two approaches - using anAWSmanaged policy and creating a custom policy.
AWS managed policies like "ReadOnlyAccess" are maintained byAWSand automatically updated when new services are added. They're great for common use cases and followAWSbest practices.
attach-role-policy parameters:
- --role-name: The role we just created
- --policy-arn: The Amazon Resource Name of the policy to attach
-AWSmanaged policies always start with "arn:aws:iam::aws:policy/"
For custom policies, we use create-policy first, then attach-role-policy. The custom policy I'm showing allows S3 access but only to buckets with "dev" in the name - this is principle of least privilege in action.
When you attach a policy,AWSdoesn't return any output on success - silence is golden here. You can verify the attachment worked by using get-role or list-attached-role-policies.
Setting Up the Trusted Account
Now we switch contexts to Account B. This is where the user who will assume the cross-account role lives.
The user in Account B needs specific permissions to assume roles in other accounts. The sts:AssumeRole permission is what allows them to call the AssumeRole API.
Why we need sts:AssumeRole permission:
- It's not automatic - users need explicit permission
- You can restrict which roles they can assume
- You can add conditions (MFA, IP restrictions, etc.)
- It's auditable in CloudTrail
The policy I'm showing allows assuming any role in Account A, but you could be more restrictive and specify exact role ARNs. Notice the condition requiring an ExternalId - this must match what's in the trust policy.
Creating the user and attaching this policy gives them the capability to assume cross-account roles, but they still need to be explicitly allowed by each role's trust policy.
The AssumeRole Process
This is the moment of truth - where Account B's user actually assumes the role in Account A. TheAWSsts assume-role command is the bridge between accounts.
assume-role parameters explained:
- --role-arn: The full ARN of the role to assume
- --role-session-name: A name for this session (appears in CloudTrail)
- --external-id: Must match the trust policy's condition
- --duration-seconds: How long the credentials last (900-43200 seconds)
The response contains temporary credentials: AccessKeyId, SecretAccessKey, and SessionToken. These credentials are only valid for the specified duration and only have the permissions granted to the assumed role.
The session name is crucial for auditing - it appears in all CloudTrail logs, so use meaningful names like "DevUser Deployment Task followed by date rather than generic names.
If this fails, check: 1) Trust policy allows your user, 2) ExternalId matches, 3) You have sts:AssumeRole permission, 4) The role exists in the target account.
Using the Temporary Credentials
Once you have the temporary credentials, you need to configure yourAWSCLI to use them. There are several ways to do this, and I'll show you the most practical approaches.
The export commands set environment variables that theAWSCLI automatically picks up. This is temporary - they only last for your current shell session.
Environment variables explained:
- AWS_ACCESS_KEY_ID: The temporary access key
- AWS_SECRET_ACCESS_KEY: The temporary secret key
- AWS_SESSION_TOKEN: Required for temporary credentials
- AWS_DEFAULT_REGION: Optional but recommended
Alternatively, you can useAWSconfigure set to store the credentials in a named profile. This is more permanent and allows you to switch between multiple assumed roles easily.
TheAWSsts get-caller-identity command now shows you're using the assumed role - your ARN will show the role name and session name. This confirms the role assumption worked.
Now anyAWSCLI commands you run will use the permissions of the assumed role, not your original user permissions.
Advanced Trust Policy Features
Let's explore advanced trust policy features that give you granular control over when and how roles can be assumed.
Time-based conditions are incredibly powerful for temporary access scenarios. You can create contractor roles that only work during specific time periods or maintenance windows that only activate during scheduled maintenance times.
Advanced conditions:
- DateGreaterThan/DateLessThan: Time-based access
- IpAddress: Restrict by source IP
- StringEquals: Exact string matching
- Bool: Boolean conditions (like MFA requirements)
MFA requirements are essential for privileged roles. The AWS MultiFactor AuthPresent condition ensures the user provided a second factor, and AWS MultiFactorAuthAge ensures it was recent.
IP address restrictions are great for hybrid environments where you want to ensure access only comes from your corporate network or specific VPNs.
When you have multiple conditions, they all must be true (AND logic). Use multiple statements for or logic.
Automation and Scripting
In real-world scenarios, you'll want to automate the role assumption process. Let me show you a practical script that handles the entire workflow.
This script demonstrates several best practices: error handling, credential validation, and automatic session cleanup.
Script components:
- Input validation: Check required parameters
- Error handling: Graceful failure with meaningful messages
- Credential parsing: Extract values from JSON response
- Session management: Set up and clean up credentials
The jq tool is invaluable for parsing JSON responses fromAWSCLI. The -r flag outputs raw strings (without quotes), making them suitable for shell variables.
Always validate that the role assumption worked before proceeding with other operations. A simpleAWSsts get-caller-identity check can prevent confusing errors later.
Consider adding logging to your automation scripts - knowing when roles were assumed and by whom is crucial for security auditing.
Monitoring and Auditing
Cross-account access creates additional security considerations, so monitoring and auditing become even more critical.
CloudTrail logs every AssumeRole call with details about who assumed what role, when, and from where. The sourceIPAddress field is particularly valuable for detecting anomalous access patterns.
Key CloudTrail fields for AssumeRole:
- eventName: "AssumeRole"
- sourceIPAddress: Where the request came from
- userIdentity: Who made the request
- requestParameters: Role ARN and session name
- responseElements: Whether it succeeded
TheAWSlogs filter-log-events command lets you search CloudTrail logs programmatically. You can create automated alerts for unusual patterns like role assumptions from unexpected IP addresses or outside business hours.
Access Analyzer can help you identify unused cross-account access - if a role hasn't been assumed in 90 days, maybe it's time to review whether it's still needed.
Set up CloudWatch alarms for failed AssumeRole attempts - repeated failures might indicate an attack or misconfiguration.
Troubleshooting Common Issues
Let's walk through the most common issues you'll encounter with cross-account access and how to debug them systematically.
The first step in any troubleshooting is alwaysAWSsts get-caller-identity to confirm which account and user you're operating as. Many issues stem from being in the wrong account context.
Systematic troubleshooting approach:
1. Verify current identity and account
2. Check trust policy syntax and principals
3. Confirm user has sts:AssumeRole permission
4. Validate role exists and is assumable
5. Check condition requirements (ExternalId, MFA, etc.)
TheAWSiam simulate-principal-policy command is incredibly valuable for testing permissions without actually performing actions. You can test whether a specific user can assume a specific role.
CloudTrail is your forensic tool - look for AssumeRole events to see exactly what happened and why it failed. The errorCode and errorMessage fields provide specific failure reasons.
Remember that IAM is eventually consistent - if you just created a role or policy, wait a few seconds before testing. This catches many mysterious "access denied" errors.
Security Best Practices
Cross-account access amplifies both the benefits and risks of yourAWSsecurity posture, so following best practices is crucial.
External IDs are your first line of defense against confused deputy attacks. Always use them for third-party integrations, and make them unique and unpredictable.
Security best practices:
- Use specific principals, not account-wide trust
- Require external IDs for third-party access
- Implement time-based access controls
- Monitor and alert on unusual access patterns
- Regular access reviews and cleanup
Principle of least privilege is even more important in cross-account scenarios. Start with minimal permissions and add only what's needed. The ReadOnlyAccess policy is often sufficient for initial integrations.
Consider usingAWSOrganizations SCPs (Service Control Policies) to add guardrails around cross-account access. You can prevent certain roles from being created or assumed.
Regular access reviews should include cross-account relationships. Use Access Analyzer findings to identify overly permissive trusts or unused access paths.
Real-World Implementation Guide
Let's wrap up with a comprehensive implementation checklist that you can use in production environments.
Start with a pilot implementation between non-production accounts to validate your approach. This lets you work out the kinks without risking production systems.
Implementation checklist:
✓ Document the business requirement
✓ Design the access pattern
✓ Create test accounts for validation
✓ Implement monitoring and alerting
✓ Test failure scenarios
✓ Train users on proper usage
✓ Establish regular review cycles
Always have a rollback plan. Cross-account access can be removed quickly by updating trust policies, but make sure you understand the impact on dependent systems.
Document your external IDs and session naming conventions. Future troubleshooting will be much easier if you have consistent, meaningful naming patterns.
Consider implementing automated credential rotation for long-term cross-account relationships. While assumed role credentials are temporary, the base credentials used to assume them should be rotated regularly.
Thank you for following this comprehensive guide! You now have all the tools and knowledge needed to implement secure, auditable cross-account access in AWS.
Cross-Account Access with IAM Roles
Complete Implementation Guide
Comprehensive, line-by-lineAWSCLI tutorial for secure cross-account access
Demo Accounts
- Account A (Production): account A - Creates the cross-account role
- Account B (Development): 444455556666 - Assumes the role
graph TB
A[Account B
444455556666
DevUser] -->|1. AssumeRole Request| B[AWS STS]
B -->|2. Validate Trust Policy| C[Account A
111122223333
CrossAccountRole]
C -->|3. Check Permissions| D[Trust Policy]
D -->|4. Allow/Deny| B
B -->|5. Return Temporary Creds| A
A -->|6. Access Resources| E[AWS Resources
in Account A]
What You'll Learn:
- EveryAWSCLI parameter explained in detail
- Step-by-step role creation and assumption
- Advanced security configurations
- Troubleshooting and monitoring
- Production-ready automation scripts
Cross-Account Architecture
The Cross-Account Security Model
Two accounts working together with temporary, auditable access
graph TB
subgraph "Account A: 111122223333 (Production)"
A1[IAM Role: CrossAccountRole]
A2[Trust Policy: Who can assume?]
A3[Permissions Policy: What can they do?]
A4[AWS Resources: S3, EC2, etc.]
end
subgraph "Account B: 444455556666 (Development)"
B1[IAM User: DevUser]
B2[User Policy: sts:AssumeRole]
end
B1 -->|AssumeRole| A1
A1 --> A4
A2 -.->|Controls| A1
A3 -.->|Defines| A1
Component |
Purpose |
Location |
Trust Policy |
Defines WHO can assume the role |
Account A (Target) |
Permissions Policy |
Defines WHAT the role can do |
Account A (Target) |
User Policy |
Allows sts:AssumeRole action |
Account B (Source) |
Temporary Credentials |
Short-lived access tokens |
Generated by STS |
Environment Setup & Verification
1
Verify Current Account Context
aws sts get-caller-identity
Command Breakdown:
• aws
-AWSCommand Line Interface
• sts
- Security Token Service
• get-caller-identity
- Returns details about the IAM entity making the request
• No parameters required - Uses currentAWScredentials
# Expected Response:
{
"UserId": "AIDACKCEVSQ6C2EXAMPLE",
"Account": "111122223333",
"Arn": "arn:aws:iam::111122223333:user/AdminUser"
}
# What each field means:
# UserId: Unique identifier for the IAM entity
# Account:AWSAccount ID you're operating in
# Arn: Amazon Resource Name showing entity type and name
2
CheckAWSCLI Configuration
aws configure list
Shows your credential configuration:
• Name: Configuration setting name
• Value: Current value (credentials masked)
• Type: Source of the setting (config file, env var, etc.)
• Location: Where the setting is defined
✅ Verification Complete: You're now confirmed to be operating in Account A (account A) with administrative privileges.
Creating the Trust Policy Document
3
Create Trust Policy JSON File
cat > cross-account-trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:user/DevUser"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "UniqueExternalId12345"
}
}
}
]
}
EOF
Trust Policy Component Analysis:
• cat > filename << 'EOF'
- Creates file with here-document syntax
• Version: "2012-10-17"
- Latest IAM policy language version
• Effect: "Allow"
- Grants permission (vs "Deny")
• Principal
- Specifies WHO can assume this role
• Action: "sts:AssumeRole"
- The specific permission being granted
• Condition
- Additional requirements that must be met
• sts:ExternalId
- Prevents confused deputy attacks
Security Note: The ExternalId must be unique and kept secret. It prevents unauthorized role assumptions even if someone knows your account ID and role name.
cat cross-account-trust-policy.json
Verification command: Display the file contents to verify JSON syntax before using it
Creating the Cross-Account Role
4
Create IAM Role with Trust Policy
aws iam create-role \
--role-name CrossAccountDeveloperRole \
--assume-role-policy-document file://cross-account-trust-policy.json \
--description "Role for developers in Account B to access Account A resources" \
--path /cross-account/ \
--max-session-duration 3600
Parameter Deep Dive:
• --role-name
- Name for the role (unique within account)
• --assume-role-policy-document
- Trust policy defining who can assume this role
• file://
- Prefix telling CLI to read from local file
• --description
- Human-readable description (optional but recommended)
• --path
- Hierarchical path for organization (optional, defaults to "/")
• --max-session-duration
- Maximum duration in seconds (900-43200)
# Expected Response:
{
"Role": {
"Path": "/cross-account/",
"RoleName": "CrossAccountDeveloperRole",
"RoleId": "AROA1234567890EXAMPLE",
"Arn": "arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole",
"CreateDate": "2024-12-05T10:30:00+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [...]
},
"Description": "Role for developers in Account B to access Account A resources",
"MaxSessionDuration": 3600
}
}
✅ Role Created Successfully: The ARN arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole
is what Account B will use to assume this role.
Adding Permissions to the Role
5
Option A: AttachAWSManaged Policy
aws iam attach-role-policy \
--role-name CrossAccountDeveloperRole \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
attach-role-policy parameters:
• --role-name
- Name of the role to attach policy to
• --policy-arn
- Amazon Resource Name of the policy
• arn:aws:iam::aws:policy/
- Prefix for allAWSmanaged policies
• No output on success -AWSCLI returns nothing when attachment succeeds
6
Option B: Create and Attach Custom Policy
cat > s3-dev-access-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::dev-*",
"arn:aws:s3:::dev-*/*"
]
}
]
}
EOF
aws iam create-policy \
--policy-name S3DevAccess \
--path /cross-account/ \
--policy-document file://s3-dev-access-policy.json \
--description "S3 access limited to dev buckets"
create-policy parameters:
• --policy-name
- Name for the new policy
• --path
- Organizational path (matches role path)
• --policy-document
- JSON document defining permissions
• --description
- Human-readable description
aws iam attach-role-policy \
--role-name CrossAccountDeveloperRole \
--policy-arn arn:aws:iam::111122223333:policy/cross-account/S3DevAccess
Setting Up the Trusted Account (Account B)
⚠️ Context Switch: The following commands must be run in Account B (444455556666) with appropriate credentials.
7
Verify Account B Context
aws sts get-caller-identity
# Expected Response for Account B:
{
"UserId": "AIDACKCEVSQ6C2EXAMPLE",
"Account": "444455556666",
"Arn": "arn:aws:iam::444455556666:user/AdminUser"
}
8
Create User Policy for AssumeRole Permission
cat > assume-role-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "UniqueExternalId12345"
}
}
}
]
}
EOF
Policy components explained:
• Action: "sts:AssumeRole"
- Permission to assume roles
• Resource
- Specific role ARN from Account A
• Condition
- Must provide matching ExternalId
• This policy allows assuming only the specific role we created
9
Create User and Attach Policy
aws iam create-user --user-name DevUser
aws iam create-policy \
--policy-name AssumeRolePolicy \
--policy-document file://assume-role-policy.json
aws iam attach-user-policy \
--user-name DevUser \
--policy-arn arn:aws:iam::444455556666:policy/AssumeRolePolicy
The AssumeRole Process
10
Assume the Cross-Account Role
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole \
--role-session-name DevUser-Session-20241205 \
--external-id UniqueExternalId12345 \
--duration-seconds 3600
assume-role parameter breakdown:
• --role-arn
- Full ARN of the role to assume (from Account A)
• --role-session-name
- Name for this session (appears in logs)
• --external-id
- Must match the trust policy condition
• --duration-seconds
- How long credentials last (900-43200 seconds)
• Session name should be descriptive for auditing purposes
# Expected Response:
{
"Credentials": {
"AccessKeyId": "ASIAQ3EGABCDEFGHIJKL",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"SessionToken": "IQoJb3JpZ2luX2VjEHcaCXVzLXdlc3QtMiJGMEQCIGVy...",
"Expiration": "2024-12-05T11:30:00+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROA1234567890EXAMPLE:DevUser-Session-20241205",
"Arn": "arn:aws:sts::111122223333:assumed-role/cross-account/CrossAccountDeveloperRole/DevUser-Session-20241205"
}
}
⚠️ Important: These are temporary credentials that expire after the specified duration. The SessionToken is required for all API calls.
✅ Role Assumption Successful: You now have temporary credentials to access Account A resources with the permissions granted to CrossAccountDeveloperRole.
Using the Temporary Credentials
11
Method 1: Environment Variables (Temporary)
export AWS_ACCESS_KEY_ID="ASIAQ3EGABCDEFGHIJKL"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEHcaCXVzLXdlc3QtMiJGMEQCIGVy..."
export AWS_DEFAULT_REGION="us-east-1"
Environment variables explained:
• AWS_ACCESS_KEY_ID
- Temporary access key from AssumeRole response
• AWS_SECRET_ACCESS_KEY
- Temporary secret key from response
• AWS_SESSION_TOKEN
- Required for temporary credentials
• AWS_DEFAULT_REGION
- Optional but recommended
• These override any credentials in ~/.aws/credentials
12
Method 2:AWSCLI Profile (Persistent)
aws configure set aws_access_key_id "ASIAQ3EGABCDEFGHIJKL" --profile cross-account
aws configure set aws_secret_access_key "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" --profile cross-account
aws configure set aws_session_token "IQoJb3JpZ2luX2VjEHcaCXVzLXdlc3QtMiJGMEQCIGVy..." --profile cross-account
aws configure set region us-east-1 --profile cross-account
Profile method advantages:
• --profile cross-account
- Creates named profile for these credentials
• Persists across shell sessions
• Can switch between multiple assumed roles easily
• Use with --profile
flag on subsequent commands
13
Verify Role Assumption
aws sts get-caller-identity
# or with profile:
aws sts get-caller-identity --profile cross-account
# Expected Response (showing assumed role):
{
"UserId": "AROA1234567890EXAMPLE:DevUser-Session-20241205",
"Account": "111122223333",
"Arn": "arn:aws:sts::111122223333:assumed-role/cross-account/CrossAccountDeveloperRole/DevUser-Session-20241205"
}
Advanced Trust Policy Features
14
Time-Based Access Control
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:user/ContractorUser"
},
"Action": "sts:AssumeRole",
"Condition": {
"DateGreaterThan": {
"aws:CurrentTime": "2024-01-01T00:00:00Z"
},
"DateLessThan": {
"aws:CurrentTime": "2024-12-31T23:59:59Z"
},
"StringEquals": {
"sts:ExternalId": "Contractor2024"
}
}
}
]
}
Time-based conditions:
• DateGreaterThan
- Access starts after this date
• DateLessThan
- Access ends before this date
• aws:CurrentTime
- Current UTC time
• Perfect for contractor or temporary access scenarios
15
MFA Requirement
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:user/AdminUser"
},
"Action": "sts:AssumeRole",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
},
"NumericLessThan": {
"aws:MultiFactorAuthAge": "3600"
}
}
}
]
}
MFA conditions:
• aws:MultiFactorAuthPresent
- Must be authenticated with MFA
• aws:MultiFactorAuthAge
- MFA must be within 3600 seconds (1 hour)
• Essential for privileged roles requiring additional security
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/AdminRole \
--role-session-name AdminSession \
--serial-number arn:aws:iam::444455556666:mfa/AdminUser \
--token-code 123456
MFA assume-role parameters:
• --serial-number
- ARN of the MFA device
• --token-code
- Current MFA token value
• Required when trust policy mandates MFA
Automation and Scripting
16
Production-Ready AssumeRole Script
#!/bin/bash
# assume_role.sh - Production script for role assumption
set -euo pipefail
# Configuration
ROLE_ARN="arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole"
EXTERNAL_ID="UniqueExternalId12345"
SESSION_NAME="AutomatedSession-$(date +%Y%m%d-%H%M%S)"
DURATION=3600
# Function to assume role and export credentials
assume_role() {
echo "Assuming role: $ROLE_ARN"
# Call STS assume-role
RESPONSE=$(aws sts assume-role \
--role-arn "$ROLE_ARN" \
--role-session-name "$SESSION_NAME" \
--external-id "$EXTERNAL_ID" \
--duration-seconds "$DURATION" \
--output json)
# Extract credentials using jq
ACCESS_KEY=$(echo "$RESPONSE" | jq -r '.Credentials.AccessKeyId')
SECRET_KEY=$(echo "$RESPONSE" | jq -r '.Credentials.SecretAccessKey')
SESSION_TOKEN=$(echo "$RESPONSE" | jq -r '.Credentials.SessionToken')
EXPIRATION=$(echo "$RESPONSE" | jq -r '.Credentials.Expiration')
# Export environment variables
export AWS_ACCESS_KEY_ID="$ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="$SECRET_KEY"
export AWS_SESSION_TOKEN="$SESSION_TOKEN"
echo "✅ Role assumed successfully"
echo "🕐 Credentials expire at: $EXPIRATION"
# Verify assumption
AWSsts get-caller-identity
}
# Error handling
if ! command -v jq &> /dev/null; then
echo "❌ Error: jq is required but not installed"
exit 1
fi
# Execute
assume_role
Script components explained:
• set -euo pipefail
- Strict error handling
• jq -r
- Parse JSON and output raw strings
• Dynamic session names with timestamps
• Input validation and error handling
• Automatic credential verification
chmod +x assume_role.sh
./assume_role.sh
Monitoring and Auditing
17
CloudTrail Log Analysis
aws logs filter-log-events \
--log-group-name "CloudTrail/Management" \
--filter-pattern "{ ($.eventName = AssumeRole) && ($.responseElements.assumedRoleUser.arn = \"*CrossAccountDeveloperRole*\") }" \
--start-time 1701763200000 \
--max-items 20
filter-log-events parameters:
• --log-group-name
- CloudTrail log group
• --filter-pattern
- JSON filter for AssumeRole events
• --start-time
- Unix timestamp in milliseconds
• --max-items
- Limit number of results
• Filters for specific role assumptions
18
Monitor Failed AssumeRole Attempts
aws logs filter-log-events \
--log-group-name "CloudTrail/Management" \
--filter-pattern "{ ($.eventName = AssumeRole) && ($.errorCode EXISTS) }" \
--start-time $(date -d '1 hour ago' +%s)000
Failed attempt monitoring:
• $.errorCode EXISTS
- Filter for failed attempts
• $(date -d '1 hour ago' +%s)000
- Last hour in milliseconds
• Critical for security monitoring
19
Access Analyzer for Cross-Account Review
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:111122223333:analyzer/cross-account-analyzer \
--filter resourceType=AWS::IAM::Role
Access Analyzer benefits:
• --analyzer-arn
- Specific analyzer ARN
• --filter
- Focus on IAM roles
• Identifies external access to your resources
• Helps validate intended access patterns
Security Monitoring Best Practices:
- Set up CloudWatch alarms for failed AssumeRole attempts
- Monitor AssumeRole events from unexpected IP addresses
- Review Access Analyzer findings regularly
- Track session duration and frequency patterns
Troubleshooting Common Issues
20
Systematic Debugging Approach
Error Message |
Likely Cause |
Debug Command |
"Access Denied" on AssumeRole |
Trust policy doesn't allow principal |
aws iam get-role --role-name RoleName |
"Invalid ExternalId" |
ExternalId mismatch |
Check trust policy condition |
"User not authorized" |
Missing sts:AssumeRole permission |
aws iam list-attached-user-policies |
"Role does not exist" |
Wrong account or role name |
aws iam list-roles |
21
Testing with Policy Simulator
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::444455556666:user/DevUser \
--action-names sts:AssumeRole \
--resource-arns arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole \
--context-entries ContextKeyName=sts:ExternalId,ContextKeyValues=UniqueExternalId12345,ContextKeyType=string
simulate-principal-policy parameters:
• --policy-source-arn
- ARN of the principal to simulate
• --action-names
- Action to test (sts:AssumeRole)
• --resource-arns
- Target role ARN
• --context-entries
- Condition context (ExternalId)
• Tests permissions without actually performing actions
22
Comprehensive Debug Commands
# 1. Verify current identity
aws sts get-caller-identity
# 2. Check if role exists in target account
aws iam get-role --role-name CrossAccountDeveloperRole
# 3. Verify trust policy
aws iam get-role --role-name CrossAccountDeveloperRole --query 'Role.AssumeRolePolicyDocument'
# 4. Check user's assume role permissions
aws iam list-attached-user-policies --user-name DevUser
# 5. Test assume role with debug output
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole \
--role-session-name DebugSession \
--external-id UniqueExternalId12345 \
--debug
Security Best Practices
Production Security Checklist
Security Principle |
Implementation |
CLI Command Example |
Least Privilege |
Minimal necessary permissions |
aws iam attach-role-policy --policy-arn ...:policy/ReadOnlyAccess |
External ID Usage |
Prevent confused deputy attacks |
--external-id "UniqueSecretValue123" |
Time Restrictions |
Limit access to business hours |
Trust policy with DateGreaterThan/DateLessThan |
MFA Requirements |
Require multi-factor authentication |
--serial-number arn:aws:iam::...:mfa/user --token-code 123456 |
IP Restrictions |
Allow only from trusted networks |
Trust policy with IpAddress condition |
Session Duration |
Minimize credential lifetime |
--duration-seconds 900 (15 minutes minimum) |
23
Regular Security Audits
# Check for roles not used in last 90 days
aws iam generate-service-last-accessed-details \
--arn arn:aws:iam::111122223333:role/cross-account/CrossAccountDeveloperRole \
--granularity SERVICE_LEVEL
generate-service-last-accessed-details:
• --arn
- ARN of the role to analyze
• --granularity
- SERVICE_LEVEL for detailed service usage
• Returns job ID for async processing
• Helps identify unused cross-account access
# Get the generated report
aws iam get-service-last-accessed-details \
--job-id 1234567890abcdef1234567890abcdef12345678
🔒 Critical Security Reminders:
- Never share ExternalId values in public repositories
- Rotate ExternalId values quarterly
- Monitor for AssumeRole attempts from unexpected sources
- UseAWSOrganizations SCPs for additional guardrails
- Document all cross-account relationships
Real-World Implementation Guide
24
Production Deployment Checklist
Pre-Implementation Phase
- ✅ Document business requirements and access patterns
- ✅ Design role hierarchy and permission boundaries
- ✅ Generate unique ExternalId values
- ✅ Create test accounts for validation
- ✅ Establish monitoring and alerting
25
Complete Implementation Script
#!/bin/bash
# complete_cross_account_setup.sh - Full implementation script
set -euo pipefail
# Configuration
TRUSTING_ACCOUNT="111122223333"
TRUSTED_ACCOUNT="444455556666"
ROLE_NAME="CrossAccountDeveloperRole"
USER_NAME="DevUser"
EXTERNAL_ID="$(openssl rand -hex 16)" # Generate random ExternalId
SESSION_DURATION="3600"
echo "🚀 Starting cross-account setup between accounts $TRUSTED_ACCOUNT → $TRUSTING_ACCOUNT"
echo "📝 Generated ExternalId: $EXTERNAL_ID (save this securely!)"
# Phase 1: Create trust policy
cat > trust-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$TRUSTED_ACCOUNT:user/$USER_NAME"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "$EXTERNAL_ID"
},
"Bool": {
"aws:MultiFactorAuthPresent": "true"
},
"NumericLessThan": {
"aws:MultiFactorAuthAge": "3600"
}
}
}
]
}
EOF
# Phase 2: Create role in trusting account
echo "📋 Creating role in trusting account..."
aws iam create-role \
--role-name "$ROLE_NAME" \
--assume-role-policy-document file://trust-policy.json \
--description "Cross-account role for $TRUSTED_ACCOUNT" \
--max-session-duration "$SESSION_DURATION"
# Phase 3: Attach permissions
echo "🔑 Attaching permissions..."
aws iam attach-role-policy \
--role-name "$ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
# Phase 4: Create assume role policy for trusted account
cat > assume-role-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::$TRUSTING_ACCOUNT:role/$ROLE_NAME",
"Condition": {
"StringEquals": {
"sts:ExternalId": "$EXTERNAL_ID"
}
}
}
]
}
EOF
echo "✅ Setup complete!"
echo "📋 Next steps:"
echo " 1. Switch to account $TRUSTED_ACCOUNT"
echo " 2. Create user '$USER_NAME' if not exists"
echo " 3. Attach the assume-role-policy.json to the user"
echo " 4. Test with:AWSsts assume-role --role-arn arn:aws:iam::$TRUSTING_ACCOUNT:role/$ROLE_NAME --role-session-name TestSession --external-id $EXTERNAL_ID"
# Cleanup
rm -f trust-policy.json assume-role-policy.json
26
Testing and Validation
# Test script - run in trusted account
#!/bin/bash
test_cross_account_access() {
local ROLE_ARN="arn:aws:iam::111122223333:role/CrossAccountDeveloperRole"
local EXTERNAL_ID="your-external-id-here"
echo "🧪 Testing cross-account access..."
# Test 1: Verify current identity
echo "Current identity:"
AWSsts get-caller-identity
# Test 2: Attempt role assumption
echo "Attempting role assumption..."
RESPONSE=$(aws sts assume-role \
--role-arn "$ROLE_ARN" \
--role-session-name "TestSession-$(date +%s)" \
--external-id "$EXTERNAL_ID" \
--duration-seconds 900 \
2>&1) || {
echo "❌ Role assumption failed: $RESPONSE"
return 1
}
echo "✅ Role assumption successful!"
# Test 3: Extract and test credentials
ACCESS_KEY=$(echo "$RESPONSE" | jq -r '.Credentials.AccessKeyId')
SECRET_KEY=$(echo "$RESPONSE" | jq -r '.Credentials.SecretAccessKey')
SESSION_TOKEN=$(echo "$RESPONSE" | jq -r '.Credentials.SessionToken')
# Test 4: Verify assumed role identity
AWS_ACCESS_KEY_ID="$ACCESS_KEY" \
AWS_SECRET_ACCESS_KEY="$SECRET_KEY" \
AWS_SESSION_TOKEN="$SESSION_TOKEN" \
AWSsts get-caller-identity
echo "✅ All tests passed!"
}
test_cross_account_access
🎉 Congratulations!
You've successfully implemented secure cross-account access with comprehensive monitoring, error handling, and security best practices.
Key Takeaways:
- EveryAWSCLI parameter has a specific purpose and security implication
- Trust policies and permission policies work together to control access
- ExternalId is crucial for preventing confused deputy attacks
- Comprehensive logging and monitoring are essential for security
- Automation scripts should include robust error handling
📚 Additional Resources:
- AWS IAM Best Practices Documentation
- AWS Security Token Service API Reference
- AWS CloudTrail User Guide for IAM Events
- AWS Access Analyzer User Guide