Skip to main content

IAM Best Practices for CloudWise

This guide covers AWS Identity and Access Management (IAM) best practices when setting up CloudWise to access your AWS account securely.

Overviewโ€‹

CloudWise connects through a read-only IAM role in your account. The role reads your cost data and describes your resources so the waste checks can run. It cannot create, change or delete anything. Executing fixes is a separate, opt-in role on the Agentic plan (see the AI Remediation Guide).

Core Principlesโ€‹

1. Least privilege, listed by nameโ€‹

The role's policy names every action it allows. There is no AWS managed policy attached and no wildcard write action:

  • Read-only access to cost data (Cost Explorer, your Cost and Usage Report, Savings Plans, Compute Optimizer)
  • Read-only describe, list and metric calls on the services the waste checks cover, such as EC2, EBS, RDS and S3. These read configuration, usage metrics and bucket listings. The only S3 objects the role can read are the cost reports in its own cost-report bucket
  • No write permissions to any AWS resource

Every action is on the public permission list.

What the role can read about your Lambda functionsโ€‹

The Lambda checks need two read actions, lambda:ListFunctions and lambda:ListProvisionedConcurrencyConfigs, and the role from template 1.39.0 grants only those. It does not grant lambda:GetFunction, which also returns a link to download a function's code. A stack on an older template also grants lambda:GetFunction until you update it.

lambda:ListFunctions returns each function's configuration, and that includes its environment variables, as plain text. AWS has no read-only action that lists functions without them, so anyone who can assume this role can read them. If your functions keep secrets in environment variables, move them to AWS Secrets Manager or Parameter Store, which this role cannot read.

A scan through the read-only role does not store or log them. When a scan lists your functions (get_lambda_functions in scan-core's data_providers/online.py), it keeps only the name, ARN, runtime, memory, timeout, code size, last-modified time, handler, description and architecture. The environment variables are not copied into a finding, a report, the scan's result files or a log line, and they are never sent to the AI model. The Air-Gapped export script replaces their values with REDACTED before it writes the file (script 1.29.5 and later; see Air-Gapped Mode).

Two exceptions, outside the read-only scan:

  • Older fix records on the Agentic plan. A fix that changes a Lambda function's configuration, or undoes such a change, now replaces the environment variable values in AWS's response with [redacted by CloudWise] before anything is kept. A fix recorded before that change keeps what it stored, which can include those values, and so can its failure message, audit log and notification. See the AI Remediation Guide.
  • Older Air-Gapped exports. An export made with a script older than 1.29.5 and without --anonymize included the values, and the upload keeps them until its retention ends (see Air-Gapped Mode).

2. Use IAM Roles Instead of Usersโ€‹

CloudWise never asks for access keys. It assumes a role with temporary credentials:

  • Roles provide temporary, rotating credentials
  • Nothing long-lived is stored on our side
  • Easier to audit and manage access

Required Permissionsโ€‹

Don't hand-build the policy from memory. The role and its policy come from the public CloudFormation template, cloudcostwise.io/cloudwise-cur-setup-template.yaml, which the Connect flow deploys for you (see the AWS Setup Guide). If your team reviews templates before deploying them, review that file; it is the source of truth for every permission.

Role Trust Policyโ€‹

The role trusts one principal, CloudWise's access service role, and only with the External ID passed into the stack. One-click setup mints a single-use External ID for each connection; a manual setup uses the External ID the app generates for your login and that AWS account.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/cloudwise-customer-access-service-role"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "<the External ID for this AWS account>"
}
}
}
]
}

123456789012 stands in for CloudWise's account; the template fills in the real one. With one-click setup the flow passes the External ID into the stack for you. With manual setup you paste the one the app shows for that account. Either way each AWS account gets its own, and CloudWise refuses a manual connection that kept the template's default.

Security Best Practicesโ€‹

1. Regular Access Reviewsโ€‹

  • Monthly reviews: Check which roles have access to billing data
  • Quarterly audits: Review CloudWatch logs for any unusual access patterns
  • Annual assessments: Validate that permissions are still necessary and minimal

2. Enable CloudTrail Loggingโ€‹

Monitor CloudWise access to your AWS account:

{
"eventTime": "2024-01-15T10:30:00Z",
"eventName": "AssumeRole",
"eventSource": "sts.amazonaws.com",
"requestParameters": {
"roleArn": "arn:aws:iam::your-account:role/CloudWise-Cost-Monitoring-Role",
"roleSessionName": "cw-scan-..."
}
}

The role name is CloudWise-Cost-Monitoring-Role for a manual setup; a one-click setup adds a suffix. The session name starts with ce-, cw-scan-, CloudWise-Cust- or CloudWise-PermVerify. The caller is CloudWise's own AWS account, not a role in yours.

3. Set Up Monitoring Alertsโ€‹

Create CloudWatch alarms for:

  • Unusual role assumption patterns
  • Failed authentication attempts
  • High-frequency API calls

4. Use Resource Tagsโ€‹

Tag your IAM resources for better organization:

{
"Tags": [
{
"Key": "Purpose",
"Value": "CloudWise-CostOptimization"
},
{
"Key": "Environment",
"Value": "Production"
},
{
"Key": "Owner",
"Value": "FinOps-Team"
}
]
}

Multi-Account Setupโ€‹

For organizations with multiple AWS accounts:

1. One connection per accountโ€‹

Run the Connect flow once in each account you want covered:

  • Each account gets its own CloudWise role from the same template
  • Each account gets its own External ID, from one-click setup or shown in the app for a manual setup
  • Free covers 1 account; Shield and Agentic cover up to 5. Compliance connects none by design: it works from Air-Gapped uploads

2. AWS Organizationsโ€‹

If you use AWS Organizations:

  • Make sure no Service Control Policy blocks the read-only actions the role uses, or sts:AssumeRole on it
  • See Multi-Account Setup for how connected accounts are grouped

3. Account Isolationโ€‹

Maintain security boundaries:

  • Each account has its own CloudWise role
  • Monitor cross-account access patterns in CloudTrail

Troubleshooting Common Issuesโ€‹

Permission Denied Errorsโ€‹

AccessDenied: User is not authorized to perform: ce:GetCostAndUsage

Solution: Ensure the IAM role includes all required Cost Explorer permissions.

Role Assumption Failuresโ€‹

AccessDenied: ... is not authorized to perform: sts:AssumeRole

Solution: Check that the trust policy names CloudWise's access service role and the External ID the stack was created with. Re-running the Connect flow creates a fresh, correct stack.

Missing Cost Dataโ€‹

If CloudWise can't retrieve cost data:

  1. Verify billing permissions in the IAM policy
  2. Check that Cost Explorer is enabled in your AWS account
  3. Ensure the role has access to the correct billing period

Security Monitoringโ€‹

Set Up Alertsโ€‹

Monitor these CloudTrail events:

  • AssumeRole events for the CloudWise role
  • Failed authentication attempts
  • Changes to the CloudWise IAM role or policy

Regular Security Assessmentsโ€‹

  • Access patterns: Review when and how often CloudWise accesses your account
  • Permission usage: Check which permissions are actually being used
  • Compliance checks: Ensure setup meets your organization's security policies

Cost Explorer Charges on Your Billโ€‹

CloudWise reads your account through the read-only role above, so its calls to the AWS Cost Explorer API land on your AWS bill, not ours. AWS charges $0.01 per request.

On our test account, which holds no Reserved Instances or Savings Plans, a hosted connection makes about 36 billed requests a month, about $0.36. They come from refreshing Reserved Instance and Savings Plans recommendations. A scan refreshes them when they're more than 7 days old, and each refresh makes 9 billed requests.

We measured this on one account, our own test account, by matching its AWS billing data to CloudTrail. In October 2026, its CloudWise connection made 67 billed requests, which came to $0.67:

  • 36 from refreshing Reserved Instance and Savings Plans recommendations: 4 refreshes at 9 billed requests each.
  • 31 from a daily request for historical cost data, 1 a day. That was a bug, and we've fixed it.

Historical cost data now takes 1 request at connection, 1 re-read at least a week later, plus 1 per real gap. A long answer adds 1 per extra page. An account with no Cost and Usage Report files, or more than about 12 months of them, sends none of these. We counted this from our code and haven't re-measured it since the fix.

That account has no Reserved Instances or Savings Plans, so your number may differ. An active Savings Plan adds about 1 more request per refresh. We worked that out from our code and haven't measured it.

If the same AWS account ends up connected twice, the extra connection repeats only the historical cost requests: 1 at connection, 1 re-read at least a week later, plus 1 per real gap. Recommendation refreshes aren't repeated.

Some other actions also make Cost Explorer calls. We counted these from our code, and they aren't cached:

  • Opening the Savings page: up to 4 billed requests each time it loads. An earlier version of the page billed 5 on the one day we measured it.
  • Asking the CloudWise agent about your commitments: up to 4 billed requests each time it reads your commitments and 1 each time it reads Reserved Instance recommendations, with one connected account in scope, and none with more than one.
  • Checking a role's permissions: 1 billed request, only when the role can't call iam:SimulatePrincipalPolicy, as on older role templates.

That's small compared to most AWS bills, but real, and worth stating plainly since the whole point of CloudWise is to shrink your AWS bill, not add hidden line items to it.

To check it yourself, filter Cost Explorer's usage report by the AWSCostExplorer service. That shows your total, including any API calls of your own. To see only CloudWise's calls, filter CloudTrail by event source ce.amazonaws.com and the role you created for CloudWise. Our sessions start with ce-, cw-scan-, CloudWise-Cust- or CloudWise-PermVerify. CloudTrail also lists calls AWS rejected, which aren't billed.

Additional Resourcesโ€‹


Need Help?โ€‹

If you encounter issues with IAM setup: