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
| Variable | Description | Example |
|---|
AWS_ACCESS_KEY_ID | Access key for the application’s AWS IAM user | AKIAIOSFODNN7EXAMPLE |
AWS_SECRET_ACCESS_KEY | Secret key for the application’s AWS IAM user | wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY |
AWS_ACCOUNT_ID | The application’s AWS account ID | 123456789012 |
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
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"
}
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
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": "*"
}
]
}
Name and Create Role
- Role name:
MultiCloudManagerRole (or your preferred name)
- Description: “Role for Multi-Cloud Manager application”
- Click Create role
Copy Role ARN
After creation, copy the Role ARN from the role summary:arn:aws:iam::987654321098:role/MultiCloudManagerRole
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:
- Authenticates with its own AWS credentials
- Assumes the role in the user’s account using STS
- Receives temporary credentials (access key, secret key, session token)
- Verifies access by calling
ec2:DescribeRegions
- 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
- Use least privilege: Only grant permissions the application needs
- Rotate credentials: Regularly rotate the application’s AWS access keys
- Monitor role usage: Use CloudTrail to audit role assumption events
- Set session duration: Configure maximum session duration on the IAM role (default: 1 hour)
- Use unique External IDs: Consider generating unique External IDs per user for enhanced security
- 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']
)
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