← Back to list

Mastering Multi-Account Security: How to Set Up IAM Role Chaining in AWS

Managing access across complex, multi-account cloud environments can quickly become a security headache. As organizations scale, directly…

Amit Gupta · 2026-06-07 18:09 · 0 claps · 6.0 min read
#aws-iam-role #aws-iam #aws-sts
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

Mastering Multi-Account Security: How to Set Up IAM Role Chaining in AWS

Managing access across complex, multi-account cloud environments can quickly become a security headache. As organizations scale, directly connecting primary user directories to high-risk production environments introduces substantial risk. Instead, security-focused architectures often require hopping through intermediate security or “jump” accounts.

This post demonstrates how to implement a secure, three-tier cross-account architecture using AWS IAM Role Chaining. We will walk through how a DevOps user in Account A can jump through an intermediate role in Account B to gain ultimate administrator access in Account C.

Why Use Role Chaining?

  • Centralized Auditing: Routing traffic through a middleman account gives security and compliance teams a single hub to monitor, log, and audit cross-account jumps.
  • Strict Isolation of Duties: By separating environments, you structurally prevent direct access from your primary identity provider to production systems.
  • No Long-Term Credentials: It eliminates the need for permanent access keys in your target accounts, relying entirely on short-lived, auto-expiring tokens.
  • Scalable Trust Infrastructure: Instead of creating distinct trust pathways from every single target account back to your identity provider, you manage single trust pathways via central bridge accounts.

Architecture & Trust Flow

The diagram below illustrates how the chain of trust moves sequentially through your AWS organization:

┌──────────────────────────────────────┐
│    Account A: Identity Account       │
│    Account ID: 111122223333          │
│    (DevOps IAM Group / User)         │
└──────────────────┬───────────────────┘
                   │
                   │ 1. sts:AssumeRole
                   ▼
┌──────────────────────────────────────┐
│  Account B: Intermediate Account     │
│  Account ID: 555555555555            │
│  Role Name:  assume-admin            │
└──────────────────┬───────────────────┘
                   │
                   │ 2. sts:AssumeRole
                   ▼
┌──────────────────────────────────────┐
│   Account C: Target Admin Account    │
│   Account ID: 999988887777           │
│   Role Name:  AccountCAdminRole      │
└──────────────────────────────────────┘

To configure this three-tier workflow, a user in Account A assumes an intermediate role (assume-admin) in Account B, and then uses that assumed identity to assume the final administrator role (AccountCAdminRole) in Account C.

The Flow: DevOps Group User (Account A) ➡️ assume-admin (Account B) ➡️ RoleC (Account C / Admin)

Step-by-Step Configuration

Step 1: Configure Account C (The Target Admin Account)

Create the final destination role (AccountCAdminRole) in Account C (999988887777).

1. Role C Trust Policy: Dictates that only the intermediate role from Account B can assume it.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::555555555555:role/assume-admin"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

2. Role C Permissions Policy: Attach the AWS-managed policy: arn:aws:iam::aws:policy/AdministratorAccess.

Step 2: Configure Account B (The Intermediate Account)

Create the assume-admin role in Account B (555555555555). This role acts as the middleman.

1. assume-admin Trust Policy: Allows IAM entities inside Account A to assume it.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::111122223333:root"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

2. assume-admin Permissions Policy: Grants permission to hop to Account C.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::999988887777:role/AccountCAdminRole"
    }
  ]
}

Step 3: Configure the DevOps Group in Account A

Attach a policy to the DevOps IAM group/user in Account A (111122223333) allowing them to initiate the chain.

DevOps Group Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::555555555555:role/assume-admin"
    }
  ]
}

yes, That’s all. Now we are ready to use Multi Account Access.

Client Setup :

A. Automation via AWS CLI Profiles

You can map this entire multi-hop chain directly in your local ~/.aws/config file. The AWS CLI will handle the background token handoffs seamlessly using a single target profile wrapper.

Update your ~/.aws/config or ~/.aws/credentials:

# Base credentials in Account A
[profile devops-user-account-a]
aws_access_key_id = YOUR_DEVOPS_USER_KEY
aws_secret_access_key = YOUR_DEVOPS_USER_SECRET
region = us-east-1

# First jump to Account B
[profile intermediate-account-b]
source_profile = devops-user-account-a
role_arn = arn:aws:iam::555555555555:role/assume-admin

# Final jump to Account C (Uses account B as the source!)
[profile final-admin-account-c]
source_profile = intermediate-account-b
role_arn = arn:aws:iam::999988887777:role/AccountCAdminRole

To execute commands seamlessly against Account C:

aws s3 ls --profile final-admin-account-c

B. Manual Scripted Execution (Alternative Method)

If you are building CI/CD pipelines or shell scripts where you cannot rely on a pre-configured ~/.aws/config, you must execute the aws sts assume-role calls sequentially, extracting the temporary keys from the first response to authorize the second request.

1. Clear Existing Environment Variables

Ensure no stale AWS credentials interfere with your terminal session:

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
export AWS_DEFAULT_REGION="us-east-1"

2. Configure Your Base Identity (Account A)

export AWS_ACCESS_KEY_ID="AKIA-ACCOUNT-A-DEV-USER"
export AWS_SECRET_ACCESS_KEY="SECRET-KEY-ACCOUNT-A-DEV-USER"

3. Execute Hop 1 (Assume Role in Account B)

Bash

RESPONSE_B=$(aws sts assume-role \
  --role-arn "arn:aws:iam::555555555555:role/assume-admin" \
  --role-session-name "HopToAccountB" \
  --output json)
# Extract and export temporary credentials for Account B
export AWS_ACCESS_KEY_ID=$(echo $RESPONSE_B | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $RESPONSE_B | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $RESPONSE_B | jq -r '.Credentials.SessionToken')

4. Execute Hop 2 (Assume Role in Account C)

Now that your terminal is operating under the identity of Account B’s assume-admin, execute the final hop:

RESPONSE_C=$(aws sts assume-role \
  --role-arn "arn:aws:iam::999988887777:role/AccountCAdminRole" \
  --role-session-name "FinalHopToAccountC" \
  --output json)
# Extract and export final administrator credentials for Account C
export AWS_ACCESS_KEY_ID=$(echo $RESPONSE_C | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $RESPONSE_C | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $RESPONSE_C | jq -r '.Credentials.SessionToken')

5. Verify Active Admin Access

# Verify your active identity reflects Account C
aws sts get-caller-identity
# Run admin commands in Account C
aws s3 ls

C. The Automation Script (assume-c.sh)

Update your ~/.aws/config or ~/.aws/credentials:

# Base credentials in Account A
[profile devops-user-account-a]
aws_access_key_id = YOUR_DEVOPS_USER_KEY
aws_secret_access_key = YOUR_DEVOPS_USER_SECRET
region = us-east-1
#!/usr/bin/env bash

# Exit immediately if a pipeline or command returns a non-zero status
set -eo pipefail

# ==============================================================================
# CONFIGURATION (No Hardcoded Secrets Allowed)
# ==============================================================================
# The local AWS profile configured with your Account A IAM User credentials
AWS_PROFILE_A="devops-user-account-a"
AWS_REGION="us-east-1"

# Target ARNs for the cross-account role chain
ROLE_B_ARN="arn:aws:iam::555555555555:role/assume-admin"
ROLE_C_ARN="arn:aws:iam::999988887777:role/AccountCAdminRole"

# Optional: Set this if Account B forces MFA on incoming connections
# MFA_DEVICE_ARN="arn:aws:iam::111122223333:mfa/your-username"

# ==============================================================================
# PREREQUISITE VALIDATION
# ==============================================================================
if ! command -v jq &> /dev/null; then
    echo "❌ Error: 'jq' utility is required but not installed." >&2
    echo "👉 Install via: 'brew install jq' (Mac) or 'sudo apt install jq' (Linux)" >&2
    return 1 2>/dev/null || exit 1
fi

if ! command -v aws &> /dev/null; then
    echo "❌ Error: AWS CLI is not installed or not in your PATH." >&2
    return 1 2>/dev/null || exit 1
fi

# ==============================================================================
# INITIALIZATION & CLEANUP
# ==============================================================================
echo "🛡️ Sanitizing active terminal session environment variables..."
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE
export AWS_DEFAULT_REGION="${AWS_REGION}"

# ==============================================================================
# HOP 1: ASSUME INTERMEDIATE ROLE (ACCOUNT B)
# ==============================================================================
echo "🚀 Authenticating to Account A using profile: [${AWS_PROFILE_A}]..."

# Build conditional MFA parameters dynamically to keep code clean
MFA_ARGS=()
if [ -n "${MFA_DEVICE_ARN:-}" ]; then
    echo -n "🔑 Enter 6-digit MFA Token Code: "
    read -r MFA_TOKEN
    if [[ ! "$MFA_TOKEN" =~ ^[0-9]{6}$ ]]; then
        echo "❌ Error: Invalid MFA format. Must be exactly 6 digits." >&2
        return 1 2>/dev/null || exit 1
    fi
    MFA_ARGS=(--serial-number "${MFA_DEVICE_ARN}" --token-code "${MFA_TOKEN}")
fi

echo "🔄 Requesting temporary session tokens from Account B..."
if ! RESPONSE_B=$(aws sts assume-role \
    --profile "${AWS_PROFILE_A}" \
    --role-arn "${ROLE_B_ARN}" \
    --role-session-name "HopToAccountB-$(date +%s)" \
    "${MFA_ARGS[@]}" \
    --output json 2>&1); then
    echo "❌ Failed to assume Role B!" >&2
    echo "${RESPONSE_B}" >&2
    return 1 2>/dev/null || exit 1
fi

# Securely bind terminal context to Account B's temporary entity
export AWS_ACCESS_KEY_ID=$(echo "$RESPONSE_B" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$RESPONSE_B" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$RESPONSE_B" | jq -r '.Credentials.SessionToken')

# ==============================================================================
# HOP 2: ASSUME ADMIN ROLE (ACCOUNT C)
# ==============================================================================
echo "🔄 Finalizing chain: Hopping from Account B to Account C..."
if ! RESPONSE_C=$(aws sts assume-role \
    --role-arn "${ROLE_C_ARN}" \
    --role-session-name "FinalHopToAccountC-$(date +%s)" \
    --output json 2>&1); then
    echo "❌ Failed to assume Role C!" >&2
    echo "${RESPONSE_C}" >&2
    # Reset terminal to avoid hanging on intermediate state
    unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
    return 1 2>/dev/null || exit 1
fi

# Securely bind final admin keys to active environment
export AWS_ACCESS_KEY_ID=$(echo "$RESPONSE_C" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$RESPONSE_C" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$RESPONSE_C" | jq -r '.Credentials.SessionToken')

# ==============================================================================
# VERIFICATION
# ==============================================================================
echo "----------------------------------------------------------------------"
echo "✅ SUCCESS: Account C Admin Context Active."
echo "⏱️ Session Valid for exactly 1 Hour."
echo "----------------------------------------------------------------------"
aws sts get-caller-identity --query "{Account:Account,Arn:Arn}" --output table

How to Run the Script

source ./assume-c.sh

Verify that your active terminal session now successfully commands Account C

  • aws sts get-caller-identity

⚠️ Critical Security Limitation: The 1-Hour Session Hard Limit

While role chaining significantly strengthens your infrastructure isolation, it comes with a major caveat: AWS enforces a hard maximum limit of 1 hour (3600 seconds) for sessions obtained via role chaining. Even if you configure the target role’s Maximum CLI/API session duration setting to 12 hours, this limit is automatically dropped back to 1 hour the moment you chain roles (i.e., assuming a role from a session that is already utilizing temporary credentials). Keep this 60-minute window in mind when planning long-running automation tasks or deployment scripts!


메타데이터
post_id
ec5293f5cdcc
slug
mastering-multi-account-security-how-to-set-up-iam-role-chaining-in-aws-ec5293f5cdcc
url
https://medium.com/@amit.active2008/mastering-multi-account-security-how-to-set-up-iam-role-chaining-in-aws-ec5293f5cdcc
canonical_url
https://medium.com/@amit.active2008/mastering-multi-account-security-how-to-set-up-iam-role-chaining-in-aws-ec5293f5cdcc
author_url
https://medium.com/@amit.active2008
status
ok
fetched_at
2026-08-19 18:15:50