TellHound

What a read-only cross-account IAM role can and cannot see

· 6 min read · TellHound
Short answer

A read-only cross-account IAM role lets a vendor read CloudWatch metrics, resource configuration, resource names and tags. It cannot read S3 object contents, connect to your databases, see application data, or modify anything — those permissions are absent from the policy. No credential leaves your account: the vendor receives short-lived STS tokens, gated by an ExternalId, and you revoke access by deleting the role.

If you are considering any AWS monitoring tool, the moment that should give you pause is the one where it asks for access to your account. It is the right instinct. This post is the answer we would want if the roles were reversed — specific enough to check, rather than reassuring enough to skim.

We build one of these tools, so treat this as interested but verifiable. Every claim below is something you can confirm yourself in about five minutes, and the last section tells you how.

How can an AWS monitoring vendor get access?

Access keys. You create an IAM user, generate a key pair, paste it into their web form. The vendor now holds a long-lived credential that works from anywhere, cannot be scoped to only their infrastructure, and does not expire. If they are breached, so are you, and you find out when someone tells you.

An agent. Software installed on your instances, usually with more permission than the task needs, updating itself from a vendor repository.

A cross-account role. You create an IAM role in your account that trusts the vendor's account to assume it, restricted by an ExternalId. No credential ever leaves your account. The vendor gets short-lived STS tokens generated on demand, and you can end it unilaterally by deleting the role.

The third is the one to insist on. It is also the one AWS itself recommends for third-party access, and the difference is not cosmetic — it changes who holds the key.

What does a read-only role actually permit?

"Read-only" is doing a lot of work in most vendor marketing, so here is the concrete version. A monitoring role typically needs something close to this:

cloudwatch:GetMetricData        cloudwatch:ListMetrics
ec2:Describe*                   autoscaling:Describe*
rds:Describe*                   elasticloadbalancing:Describe*
ecs:Describe*                   ecs:List*
lambda:List*                    lambda:GetFunctionConfiguration
elasticache:Describe*           cloudfront:List*
s3:ListAllMyBuckets             s3:GetBucketLocation
ce:GetCostAndUsage              tag:GetResources

Read that list carefully, because what is absent matters more than what is present.

What this exposes

  • Metrics. CPU, connections, request counts, error rates. Numbers about behaviour, not content.
  • Configuration. Instance types, ASG min/max, RDS parameter groups, load balancer listeners, CloudFront cache behaviours, Lambda memory and timeout.
  • Resource names and tags. This is the one people underestimate. A bucket called acme-payroll-exports-2026 tells a reader that such a bucket exists. Names are metadata, and metadata leaks intent.
  • Cost and usage totals if you grant ce:GetCostAndUsage.

What it does not

  • s3:ListAllMyBuckets lists bucket names. It does not read objects. Reading an object needs s3:GetObject, which is not on that list. The distinction is the entire ballgame and it is worth confirming for yourself in the IAM policy simulator.
  • rds:Describe* returns engine version, instance class, parameter groups. It does not connect to the database. There is no IAM permission that reads table contents; that requires network access and database credentials.
  • No application data. Nothing here reads your logs, your queues, or anything your code produced.
  • No personal data about your customers. It cannot reach the systems that hold it.
  • No mutation. No Create, Update, Delete, Put, Modify, Terminate anywhere in the list. A read-only role cannot change your infrastructure even if the vendor's code has a bug that tries.

What is the ExternalId for?

The trust policy on the role looks like this:

AssumeRolePolicyDocument:
  Statement:
    - Effect: Allow
      Principal: { AWS: 'arn:aws:iam::<vendor-account>:root' }
      Action: 'sts:AssumeRole'
      Condition:
        StringEquals:
          'sts:ExternalId': '<a value unique to you>'

Without the condition, the vendor's account can assume your role — and so can anyone who persuades the vendor to assume a role on their behalf, if they learn your role ARN. That is the confused-deputy problem, and the ExternalId is the fix: the vendor must present a value that only your connection has.

Two things to check on any vendor's template:

  1. The ExternalId must be generated by them, per connection, and be unguessable. If you can choose it, or it is the same for every customer, it is decoration.
  2. The principal must be a specific account id, not a wildcard. If you see Principal: { AWS: '*' } with only an ExternalId condition, walk away.

How do I verify the permissions myself?

You do not have to take a vendor's word for any of this.

Read the template first. It should be a plain CloudFormation YAML you can open before you deploy. If it is behind a login, or generated by a script you cannot inspect, that is the answer.

Simulate the policy. IAM has a simulator that tells you exactly what a role can do. Ask it the questions that matter:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::<your-account>:role/<TheRole> \
  --action-names s3:GetObject s3:ListAllMyBuckets ec2:TerminateInstances \
                 rds:DeleteDBInstance iam:CreateUser \
  --query 'EvaluationResults[].{action:EvalActionName,decision:EvalDecision}' \
  --output table

s3:ListAllMyBuckets should be allowed. Everything else on that list should be implicitDeny. If any mutation is allowed, the role is not read-only, whatever the marketing says.

Watch what it actually does. CloudTrail records every API call made by the assumed role. After a day, look at what it really touched:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=<role-session-name> \
  --query 'Events[].{time:EventTime,event:EventName}' --output table

If the calls do not match the permissions they asked for, you have learned something useful.

How do I revoke access?

This is the part that should decide your comfort level: you can end it unilaterally and instantly, without contacting the vendor.

aws cloudformation delete-stack --stack-name <the-stack>

The role is gone. The next time they try to assume it, they get AccessDenied. There is no credential left in their systems that still works, because there never was one — only the ability to ask your account for a short-lived token, and your account will now refuse.

Compare that with access keys, where revoking means finding every place the key was stored and hoping the vendor honours a deletion request.

The short version

A read-only cross-account role is the correct shape for third-party AWS monitoring:

  • No credential leaves your account
  • Tokens are short-lived and generated per request
  • The ExternalId stops anyone else borrowing the vendor's position
  • Nothing can be modified, even by accident
  • You can revoke it in one command, without asking

What it does expose is real and worth weighing: your resource names, your configuration, your metrics, and possibly your spend. If your bucket names contain things you would not want a third party to read, that is a genuine consideration — and one no vendor's privacy policy fixes for you.

Anyone asking for access keys instead should be able to explain why. Usually the answer is that the role flow was more work to build.

Want this checked on your own account?

TellHound connects through a read-only cross-account role — no agents, no access keys — and reports findings like this one with the evidence attached.

Start a 14-day trial More posts