# Why Read-Only Is the Only Safe Way to Let AI Near Your AWS Account

> Source: <https://dev.to/cloudwiseteam/why-read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account-jj5>
> Published: 2026-08-12 13:54:04+00:00

Every AWS cost tool eventually asks you for the same terrifying thing: an IAM role. And every vendor says the same reassuring word about it: "read-only." I want to tell you exactly what that word means when CloudWise says it, because "read-only" gets used loosely enough in this industry that the word alone shouldn't be enough to trust anyone — including us. So this post is the IAM, not the marketing.

When you connect an AWS account through CloudWise's one-click setup, you launch exactly one CloudFormation stack: the monitoring stack. There is no button in onboarding that launches anything else — `startConnect('monitoring')`

is the only path the connect flow calls.

That stack's policy (`cloudwise-cur-setup-template.yaml`

) is a bespoke allow-list we wrote and maintain, not the AWS-managed `ReadOnlyAccess`

policy. That distinction matters more than it sounds like it should: `ReadOnlyAccess`

is enormous and vague — it grants read access to almost every AWS service, including ones CloudWise has no reason to ever look at. Our policy is scoped to what a cost scan actually needs, action by action: `ec2:DescribeInstances`

, `rds:DescribeDBInstances`

, `s3:ListAllMyBuckets`

, `ce:GetCostAndUsage`

, `compute-optimizer:GetEC2InstanceRecommendations`

, and so on — read the whole thing at [cloudcostwise.io/security/permissions](https://cloudcostwise.io/security/permissions?utm_source=blog&utm_medium=hub&utm_campaign=read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account), which renders the live action count straight off the template, not a number we typed into a page and forgot to update. Every single statement in that policy is a `Get*`

, `Describe*`

, `List*`

, or `BatchGet*`

call — plus `sts:GetCallerIdentity`

and `iam:SimulatePrincipalPolicy`

, which CloudWise uses to check what a role *can* do without ever calling it. There is no `Put`

, `Create`

, `Delete`

, `Update`

, `Attach`

, or `Modify`

verb anywhere in that role's policy. It cannot make a single write call against your account. Not "shouldn't" — cannot; IAM will reject the attempt at the API layer before it reaches any resource.

(One nuance, for the pedants, because I'd rather you catch it than an auditor: the CUR bucket in that same template does have a bucket policy permitting `s3:PutObject`

— granted to AWS's own `billingreports.amazonaws.com`

service principal, not to CloudWise. That's how AWS itself delivers your Cost and Usage Report into a bucket you own. It's AWS writing to you, not us writing to anything.)

If you want CloudWise to actually *fix* waste — stop an idle NAT gateway, delete an orphaned snapshot, right-size an instance — that is never part of onboarding. It requires deploying a second, separate CloudFormation stack (`CloudWise-Remediation`

, using `cloudwise-remediation-role.yaml`

) that you reach only from `settings/remediation`

or `setup/permissions`

, deliberately after the point where you've already seen what a read-only scan finds. Nothing in the sign-up or connect flow grants this role. You have to go looking for it.

That role's policy carries an explicit **Deny** block that overrides everything else, no matter what any other statement in the policy says:

`iam:*`

, `organizations:*`

, `sts:*`

— CloudWise can never touch identity, org structure, or assume other roles`secretsmanager:GetSecretValue`

/ `PutSecretValue`

/ `CreateSecret`

/ `UpdateSecret`

— no access to your secrets, ever`cloudtrail:DeleteTrail`

, `cloudtrail:StopLogging`

, `config:DeleteConfigRule`

, `config:StopConfigurationRecorder`

, `guardduty:DeleteDetector`

— your audit and detection trail can't be turned off`s3:DeleteBucket`

, `ec2:DeleteVpc`

, `ec2:DeleteSubnet`

, `ec2:DeleteSecurityGroup`

, `rds:DeleteDBInstance`

— no deleting the structural stuff that would actually hurt`ec2:AuthorizeSecurityGroupIngress/Egress`

, `ec2:RevokeSecurityGroupIngress/Egress`

, `ec2:CreateSecurityGroup`

— CloudWise cannot open, close, or create a network path`kms:CreateKey/CreateGrant/Encrypt/Decrypt/GenerateDataKey*`

, `ssm:*`

— no touching encryption or Systems ManagerA Deny statement in IAM always wins, regardless of what any Allow statement elsewhere in the same policy — or any other policy — grants. That's not a UI promise. It's how the policy evaluates at the API layer.

You'll also see a small number of `Create*`

actions in that same policy file — things like `ec2:RunInstances`

or `rds:CreateDBInstance`

. Those exist for one purpose: **rollback**. Before executing an approved action, CloudWise records enough state to reverse it — restart an instance it stopped, recreate a resource it deleted, restore a secret's scheduled deletion. If a fix goes wrong, or you change your mind, there's a way back. That's a narrower and more honest thing than "can create resources," and it's also narrower than "can never create resources" — so I'm not going to round it either direction. Read the actual file if you want the specifics; that's the point of publishing it.

Here's the part I actually care about you believing: CloudWise proposes fixes. It does not execute them on its own initiative, ever, under any circumstance we ship today.

The execution path (`lambdas/remediation_executor/handler.py`

) checks a status field before it will run a single mutating API call, and that status has to read `approved`

. The only thing that writes `approved`

is a separate approval-gateway Lambda, triggered by a verified, authenticated action from you. There is no code path today that sets an action to `approved`

automatically. If you never click approve, the action sits there, proposed, forever, and nothing happens to your account.

There's also a second, quieter layer under that: every mutating call CloudWise's execution role makes is tagged with a session tag — `aws:PrincipalTag/cloudwise-action`

— that CloudWise itself sets at execution time, and the role's policy can further condition on that tag. I want to be precise about what this is and isn't: it's real defense-in-depth inside our own execution path, not a lever you hold. You don't set that tag; we do. The thing you actually control is upstream of it — the approve click that has to happen before any of this fires at all.

So, precisely:

`Get`

/`Describe`

/`List`

/`BatchGet`

actions, verifiable line-by-line at None of that requires you to trust our intentions. It requires you to trust IAM evaluation semantics, which is a much smaller ask, and one you can verify yourself against files we publish rather than a page of prose we wrote about ourselves.

If you want to see what the read-only role actually finds in your account, [a free, read-only scan](https://cloudcostwise.io?utm_source=blog&utm_medium=hub&utm_campaign=read-only-is-the-only-safe-way-to-let-ai-near-your-aws-account) takes about five minutes, grants nothing beyond what's described above, and changes nothing without you approving it first.
