Skip to main content
Multi-Cloud Manager uses AWS Security Token Service (STS) to assume IAM roles in user accounts, providing secure, temporary access to AWS resources without storing long-term credentials.

Authentication Architecture

Unlike Azure and GCP which use OAuth 2.0, AWS authentication uses IAM role assumption:

Environment Variables

Configure these environment variables in your .env file:
# AWS Server Credentials (for the application's AWS account)
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_ACCOUNT_ID=123456789012

Variable Descriptions

VariableDescriptionExample
AWS_ACCESS_KEY_IDAccess key for the application’s AWS IAM userAKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEYSecret key for the application’s AWS IAM userwJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_ACCOUNT_IDThe application’s AWS account ID123456789012
These credentials belong to the application’s AWS account, not the user’s account. Keep them secure and never commit them to version control.

External ID

The application uses a fixed External ID for additional security:
APP_EXTERNAL_ID = "multi-cloud-manager-app-v1-secret"
The External ID prevents the “confused deputy problem” by ensuring only authorized parties can assume the role. This value is shared with users during setup.

Code Implementation

The AWS authentication module is located in backend/auth/aws_auth.py:

Configuration

import boto3
import os
from botocore.exceptions import ClientError

AWS_SERVER_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_SERVER_SECRET_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
AWS_SERVER_ACCOUNT_ID = os.getenv("AWS_ACCOUNT_ID")
APP_EXTERNAL_ID = "multi-cloud-manager-app-v1-secret"

API Endpoints

Get Configuration Info

Route: /api/account/aws/config Method: GET Description: Returns public configuration data needed by users to create IAM roles.
@aws_auth.route("/api/account/aws/config", methods=["GET"])
def get_aws_config_info():
    """
    Returns public data needed to configure IAM role.
    """
    if not AWS_SERVER_ACCOUNT_ID:
        return jsonify({"error": "Server configuration incomplete (missing AWS_ACCOUNT_ID)"}), 500
        
    return jsonify({
        "awsAccountId": AWS_SERVER_ACCOUNT_ID,
        "externalId": APP_EXTERNAL_ID
    }), 200
Response:
{
  "awsAccountId": "123456789012",
  "externalId": "multi-cloud-manager-app-v1-secret"
}
Usage:
curl http://localhost:5000/api/account/aws/config

Add AWS Account

Route: /api/account/aws/add Method: POST Description: Adds a user’s AWS account by assuming an IAM role and verifying access.
@aws_auth.route("/api/account/aws/add", methods=["POST"])
def add_aws_account():
    data = request.get_json()
    role_arn_from_user = data.get("roleArn")

    if not role_arn_from_user:
        return jsonify({"error": "Missing 'roleArn' in request body"}), 400

    try:
        # Create STS client with server credentials
        sts_client = boto3.client(
            'sts',
            aws_access_key_id=AWS_SERVER_ACCESS_KEY_ID,
            aws_secret_access_key=AWS_SERVER_SECRET_KEY,
            region_name='us-east-1'
        )
        
        # Assume role in user's account
        assumed_role_object = sts_client.assume_role(
            RoleArn=role_arn_from_user,
            RoleSessionName="MultiCloudManagerVerification",
            ExternalId=APP_EXTERNAL_ID
        )
        
        temp_credentials = assumed_role_object['Credentials']
        
        # Verify credentials by listing regions
        ec2_client = boto3.client(
            'ec2',
            aws_access_key_id=temp_credentials['AccessKeyId'],
            aws_secret_access_key=temp_credentials['SecretAccessKey'],
            aws_session_token=temp_credentials['SessionToken'],
            region_name='us-east-1'
        )
        
        ec2_client.describe_regions()
        
        # Extract account ID from role ARN
        user_account_id = role_arn_from_user.split(':')[4]
        
        # Create account object
        new_aws_account = {
            "provider": "aws",
            "displayName": f"AWS Account ({user_account_id})",
            "roleArn": role_arn_from_user,
            "externalId": APP_EXTERNAL_ID,
            "accountId": user_account_id
        }

        # Update or add account in session
        accounts = session.get("accounts", [])
        
        account_found = False
        for i, acc in enumerate(accounts):
            if (acc.get("provider") == "aws" and 
                acc.get("roleArn") == role_arn_from_user):
                accounts[i] = new_aws_account
                account_found = True
                break
                
        if not account_found:
            accounts.append(new_aws_account)

        session["accounts"] = accounts
        session.modified = True
        
        return jsonify({"message": f"AWS account {user_account_id} successfully added."}), 201

    except ClientError as e:
        if e.response['Error']['Code'] == 'AccessDenied':
            return jsonify({
                "error": "Access denied. Check role ARN, account ID, and ExternalId."
            }), 403
        return jsonify({"error": f"AWS error: {str(e)}"}), 400
        
    except Exception as e:
        return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
Request Body:
{
  "roleArn": "arn:aws:iam::987654321098:role/MultiCloudManagerRole"
}
Success Response (201):
{
  "message": "AWS account 987654321098 successfully added."
}
Error Response (403):
{
  "error": "Access denied. Check role ARN, account ID, and ExternalId."
}
Usage:
curl -X POST http://localhost:5000/api/account/aws/add \
  -H "Content-Type: application/json" \
  -d '{"roleArn": "arn:aws:iam::987654321098:role/MultiCloudManagerRole"}'

IAM Role Setup for Users

1

Get Configuration Info

First, users must fetch the application’s AWS account ID and External ID:
curl http://localhost:5000/api/account/aws/config
This returns:
{
  "awsAccountId": "123456789012",
  "externalId": "multi-cloud-manager-app-v1-secret"
}
2

Create IAM Role

In the AWS Console, navigate to IAM → Roles → Create role
  • Trusted entity type: Another AWS account
  • Account ID: Enter the awsAccountId from step 1 (app’s account)
  • Require external ID: Check this box
  • External ID: Enter the externalId from step 1
3

Attach Permissions

Attach policies based on required access:Read-only access (recommended):
  • ReadOnlyAccess (AWS managed policy)
Full access (use with caution):
  • PowerUserAccess or AdministratorAccess
Custom policy (most secure):
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "s3:List*",
        "rds:Describe*",
        "cloudwatch:Get*"
      ],
      "Resource": "*"
    }
  ]
}
4

Name and Create Role

  • Role name: MultiCloudManagerRole (or your preferred name)
  • Description: “Role for Multi-Cloud Manager application”
  • Click Create role
5

Copy Role ARN

After creation, copy the Role ARN from the role summary:
arn:aws:iam::987654321098:role/MultiCloudManagerRole
6

Add Account to Application

POST the role ARN to the application:
curl -X POST http://localhost:5000/api/account/aws/add \
  -H "Content-Type: application/json" \
  -d '{"roleArn": "arn:aws:iam::987654321098:role/MultiCloudManagerRole"}'

Trust Policy

The IAM role’s trust policy should look like this:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "multi-cloud-manager-app-v1-secret"
        }
      }
    }
  ]
}
Replace 123456789012 with the application’s AWS account ID obtained from /api/account/aws/config.

Role Assumption Flow

When a user adds an AWS account, the application:
  1. Authenticates with its own AWS credentials
  2. Assumes the role in the user’s account using STS
  3. Receives temporary credentials (access key, secret key, session token)
  4. Verifies access by calling ec2:DescribeRegions
  5. Stores account info in the session (not the credentials)
# Assume role
assumed_role_object = sts_client.assume_role(
    RoleArn=role_arn_from_user,
    RoleSessionName="MultiCloudManagerVerification",
    ExternalId=APP_EXTERNAL_ID
)

temp_credentials = assumed_role_object['Credentials']
# Contains: AccessKeyId, SecretAccessKey, SessionToken, Expiration

# Verify by calling AWS API
ec2_client = boto3.client(
    'ec2',
    aws_access_key_id=temp_credentials['AccessKeyId'],
    aws_secret_access_key=temp_credentials['SecretAccessKey'],
    aws_session_token=temp_credentials['SessionToken'],
    region_name='us-east-1'
)
ec2_client.describe_regions()  # Throws exception if access denied

Session Data Structure

After successfully adding an AWS account, the session contains:
session = {
    "accounts": [
        {
            "provider": "aws",
            "accountId": "987654321098",
            "displayName": "AWS Account (987654321098)",
            "roleArn": "arn:aws:iam::987654321098:role/MultiCloudManagerRole",
            "externalId": "multi-cloud-manager-app-v1-secret"
        }
    ]
}
Temporary credentials are not stored in the session. The application must call assume_role again each time it needs to access the user’s AWS account.

Account Deduplication

The system prevents duplicate AWS accounts in the session by comparing roleArn:
accounts = session.get("accounts", [])

account_found = False
for i, acc in enumerate(accounts):
    if (acc.get("provider") == "aws" and 
        acc.get("roleArn") == role_arn_from_user):
        # Update existing account
        accounts[i] = new_aws_account
        account_found = True
        break
        
if not account_found:
    # Add new account
    accounts.append(new_aws_account)

Troubleshooting

Error: “Access denied”

Cause: Role assumption failed Solution:
  • Verify the role ARN is correct
  • Check the trust policy includes the application’s AWS account ID
  • Ensure the External ID matches exactly (case-sensitive)
  • Confirm the role has sts:AssumeRole permission in the trust policy

Error: “Missing ‘roleArn’ in request body”

Cause: Request body is missing or malformed Solution: Ensure you’re sending a JSON body with the roleArn field:
{"roleArn": "arn:aws:iam::..."}

Error: “Server configuration incomplete”

Cause: Application’s AWS credentials not configured Solution: Set the required environment variables:
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_ACCOUNT_ID

Verification Failed (describe_regions)

Cause: Role lacks EC2 read permissions Solution: Ensure the role has at least ec2:DescribeRegions permission or attach ReadOnlyAccess policy

Security Best Practices

  1. Use least privilege: Only grant permissions the application needs
  2. Rotate credentials: Regularly rotate the application’s AWS access keys
  3. Monitor role usage: Use CloudTrail to audit role assumption events
  4. Set session duration: Configure maximum session duration on the IAM role (default: 1 hour)
  5. Use unique External IDs: Consider generating unique External IDs per user for enhanced security
  6. Secure server credentials: Store AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in a secrets manager

Temporary Credentials Lifecycle

Temporary credentials obtained via assume_role have a limited lifetime:
temp_credentials = {
    'AccessKeyId': 'ASIAIOSFODNN7EXAMPLE',
    'SecretAccessKey': 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
    'SessionToken': 'FwoGZXIvYXdzEBQa...',
    'Expiration': datetime(2024, 3, 6, 12, 0, 0)  # Default: 1 hour
}
Handling Expiration:
def get_aws_client_for_account(role_arn):
    sts_client = boto3.client(
        'sts',
        aws_access_key_id=AWS_SERVER_ACCESS_KEY_ID,
        aws_secret_access_key=AWS_SERVER_SECRET_KEY
    )
    
    # Always assume role fresh (credentials expire)
    assumed_role = sts_client.assume_role(
        RoleArn=role_arn,
        RoleSessionName="MultiCloudManagerSession",
        ExternalId=APP_EXTERNAL_ID,
        DurationSeconds=3600  # 1 hour (default)
    )
    
    creds = assumed_role['Credentials']
    
    return boto3.client(
        'ec2',
        aws_access_key_id=creds['AccessKeyId'],
        aws_secret_access_key=creds['SecretAccessKey'],
        aws_session_token=creds['SessionToken']
    )

Account ID Extraction

The application extracts the user’s AWS account ID from the role ARN:
role_arn = "arn:aws:iam::987654321098:role/MultiCloudManagerRole"
user_account_id = role_arn.split(':')[4]  # "987654321098"
ARN Format:
arn:aws:iam::<account-id>:role/<role-name>
         └────┬────┘
      Index [4]

Next Steps

Build docs developers (and LLMs) love