To enable an on-premises CMDB application to inventory resources across multiple AWS accounts, use cross-account IAM role assumption. Create a single IAM user in a central account with only sts:AssumeRole permissions, then deploy a read-only role to all member accounts using CloudFormation StackSets. This approach eliminates the need for IAM users and access keys in each account.
Introduction
When your organization runs workloads across multiple AWS accounts, keeping your Configuration Management Database (CMDB) up-to-date becomes a challenge. How do you give an on-premises application access to inventory resources in 10, 50, or even hundreds of AWS accounts without creating a security nightmare?
The answer is cross-account IAM role assumption - a pattern that allows a single set of credentials to access multiple accounts securely. This post walks through the architecture, implementation with CloudFormation StackSets, and a complete Python example for your CMDB integration.
Architecture Overview
The key principle is simple: never create IAM users or access keys in each account. Instead, use a single IAM user in a central account with permission only to assume roles, then deploy a read-only role to all member accounts.
The central IAM user has minimal permissions - only sts:AssumeRole. All actual resource access happens through the assumed role in each member account.
The architecture consists of:
- On-Premises CMDB Application - Stores credentials for the central account IAM user and assumes roles into each member account
- Central Account - Contains the IAM user with only AssumeRole permissions
- Member Accounts - Each contains a CMDBReadRole that trusts the central account
Authentication Options
Option 1: IAM User + Cross-Account Roles (Recommended)
Create a single IAM user in your central/management account with minimal permissions (only sts:AssumeRole). The user assumes a read-only role deployed to each member account.
Pros:
- Simple to set up and understand
- Only one set of credentials to manage
- Minimal permissions in central account
- Easy to audit and rotate
Cons:
- Long-lived access keys
- Keys must be stored securely on-prem
- Manual key rotation required
Option 2: IAM Roles Anywhere (More Secure)
Uses your on-premises PKI/certificates instead of static access keys. Your CMDB app authenticates using X.509 certificates signed by your CA.
Pros:
- No long-lived access keys
- Certificate-based authentication
- Temporary credentials only
- Leverages existing PKI infrastructure
Cons:
- More complex setup
- Requires PKI/CA infrastructure
- Additional AWS configuration
Implementation Steps
Step 1: Create IAM User in Central Account
Create a service account with minimal permissions - only the ability to assume the CMDBReadRole in other accounts.
IAM User Policy (cmdb-assume-role-policy):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAssumeRoleInMemberAccounts",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::*:role/CMDBReadRole"
}
]
}
AWS CLI Commands:
# Create the IAM user
aws iam create-user --user-name cmdb-service-account
# Create and attach the policy
aws iam create-policy \
--policy-name cmdb-assume-role-policy \
--policy-document file://cmdb-assume-role-policy.json
aws iam attach-user-policy \
--user-name cmdb-service-account \
--policy-arn arn:aws:iam::CENTRAL_ACCOUNT_ID:policy/cmdb-assume-role-policy
# Create access keys (store these securely!)
aws iam create-access-key --user-name cmdb-service-account
Step 2: Create CloudFormation Template for Cross-Account Role
This template defines the IAM role that will be deployed to each member account.
The template defines a single AWS::IAM::Role named CMDBReadRole, parameterized on TrustedAccountId and TrustedUserName. The trust policy grants sts:AssumeRole to the central IAM user with an sts:ExternalId condition. The role attaches the AWS-managed ReadOnlyAccess policy plus a small inline policy adding organizations:Describe* and organizations:List* so the CMDB can enumerate accounts. MaxSessionDuration is 3600. Outputs export the role ARN for cross-stack reference.
Step 3: Deploy Role to All Accounts Using StackSets
Use CloudFormation StackSets to deploy the role template to all accounts in your organization automatically.
# Enable trusted access for StackSets (run once from management account)
aws organizations enable-aws-service-access \
--service-principal stacksets.cloudformation.amazonaws.com
# Create the StackSet
aws cloudformation create-stack-set \
--stack-set-name CMDBReadRole \
--description "Deploy CMDB read-only role to all member accounts" \
--template-body file://cmdb-role.yaml \
--parameters \
ParameterKey=TrustedAccountId,ParameterValue=123456789012 \
ParameterKey=TrustedUserName,ParameterValue=cmdb-service-account \
--permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
--capabilities CAPABILITY_NAMED_IAM
# Deploy to all accounts in the organization
aws cloudformation create-stack-instances \
--stack-set-name CMDBReadRole \
--deployment-targets OrganizationalUnitIds=ou-xxxx-xxxxxxxx \
--regions us-east-1 \
--operation-preferences \
FailureTolerancePercentage=10,MaxConcurrentPercentage=25
Replace ou-xxxx-xxxxxxxx with your root OU ID to deploy to all accounts, or specify individual OU IDs to target specific accounts. New accounts added to the OU will automatically receive the role.
Step 4: Configure CMDB Application
Configure your on-premises CMDB application to use the credentials and assume roles in each account.
Python sketch: The script pulls the central account credentials from a secrets manager, calls organizations.list_accounts to enumerate active accounts, then for each one calls sts.assume_role with the role ARN, an ExternalId, and a session name. The returned temporary credentials seed a new boto3 client per service/region, and from there it's standard describe_vpcs, describe_subnets, describe_instances calls that get flattened into your CMDB schema. The gotcha worth highlighting: paginate everything (list_accounts, describe_instances) and wrap assume_role in try/except for accounts where the role hasn't deployed yet.
creds = sts.assume_role(
RoleArn=f'arn:aws:iam::{account_id}:role/CMDBReadRole',
RoleSessionName='CMDBInventorySession',
ExternalId=EXTERNAL_ID,
DurationSeconds=3600,
)['Credentials']
Security Best Practices
| Practice | Description |
|---|---|
| Use External ID | Add an external ID to the role trust policy to prevent confused deputy attacks |
| Minimal Permissions | The central IAM user should ONLY have sts:AssumeRole permission - nothing else |
| Secure Key Storage | Store access keys in a secrets manager (HashiCorp Vault, CyberArk, etc.), never in code or config files |
| Regular Key Rotation | Rotate access keys at least every 90 days. Automate this process if possible |
| ReadOnly Access | The CMDBReadRole should only have read permissions. Never grant write access for inventory purposes |
| Session Duration | Keep assumed role session duration short (1 hour). The app can re-assume as needed |
| CloudTrail Logging | Ensure CloudTrail is enabled to audit all AssumeRole calls and API activity |
| IP Restrictions | Consider adding IP conditions to the role trust policy to only allow assumption from known on-prem IPs |
Optional: IP-Restricted Trust Policy
For additional security, restrict role assumption to specific IP addresses:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::CENTRAL_ACCOUNT_ID:user/cmdb-service-account"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "cmdb-external-id-change-me"
},
"IpAddress": {
"aws:SourceIp": [
"203.0.113.0/24",
"198.51.100.10/32"
]
}
}
}
]
}
Monitoring & Troubleshooting
Check StackSet Deployment Status
# List stack instances
aws cloudformation list-stack-instances \
--stack-set-name CMDBReadRole
# Check for failures
aws cloudformation list-stack-instances \
--stack-set-name CMDBReadRole \
--filters Name=DETAILED_STATUS,Values=FAILED
Test Role Assumption
# Test assuming role from CLI
aws sts assume-role \
--role-arn arn:aws:iam::MEMBER_ACCOUNT_ID:role/CMDBReadRole \
--role-session-name TestSession \
--external-id cmdb-external-id-change-me \
--profile central-account-profile
CloudTrail Query for AssumeRole Events
-- Athena query for AssumeRole events
SELECT
eventtime,
useridentity.arn as caller_arn,
requestparameters.rolearn as assumed_role,
sourceipaddress,
errorcode,
errormessage
FROM cloudtrail_logs
WHERE eventsource = 'sts.amazonaws.com'
AND eventname = 'AssumeRole'
AND requestparameters.rolearn LIKE '%CMDBReadRole%'
ORDER BY eventtime DESC
LIMIT 100;
Conclusion
This architecture provides a secure, scalable way for your on-premises CMDB application to access all AWS accounts with minimal credential management:
- One IAM user in a central account with minimal permissions
- One set of access keys to manage and rotate
- Roles deployed automatically via CloudFormation StackSets
- New accounts automatically included through StackSet auto-deployment
The key takeaway: resist the temptation to create IAM users in each account. Cross-account role assumption is the AWS-recommended pattern for this use case, and combined with StackSets, it scales to hundreds of accounts with minimal operational overhead.
For even stronger security, consider using IAM Roles Anywhere with your existing PKI infrastructure to eliminate long-lived access keys entirely.
Want Help With This?
If you're working on something similar and want a second set of eyes, or you'd like to talk through how this applies to your environment, reach out via the contact form. Happy to help.