Skip to main content

IAM Role Cross-Account ReadOnlyAccess Policy

Overview

This check identifies IAM roles that have the AWS-managed ReadOnlyAccess policy attached and allow external AWS accounts (or wildcards) to assume them. The combination of broad read permissions with cross-account access creates a significant security risk.

Risk

Severity: High

When an IAM role grants ReadOnlyAccess to external accounts, those accounts can:

  • View sensitive data in S3 buckets, DynamoDB tables, and other storage services
  • Enumerate your infrastructure to understand your AWS environment
  • Read logs and configurations that may contain secrets or internal details
  • Perform reconnaissance that enables future attacks or privilege escalation

Even "read-only" access can expose confidential information and help attackers plan more targeted attacks.

Remediation Steps

Prerequisites

  • AWS Console access with IAM permissions, or AWS CLI configured with appropriate credentials
  • The name of the IAM role flagged by Prowler

AWS Console Method

  1. Open the IAM Console and select Roles from the left menu
  2. Search for and click on the role name flagged by Prowler
  3. Go to the Permissions tab
  4. Find ReadOnlyAccess in the list of attached policies
  5. Click the X or Remove button next to it
  6. Confirm the detachment when prompted

Next steps (recommended):

  • If cross-account access is still needed, create a custom policy with only the specific permissions required (see examples below)
  • Review the role's trust policy to ensure it only allows specific, known accounts
AWS CLI Method

Identify the Problem

First, check if the role has ReadOnlyAccess attached:

aws iam list-attached-role-policies \
--role-name <ROLE_NAME> \
--region us-east-1

Review the trust policy to see who can assume the role:

aws iam get-role \
--role-name <ROLE_NAME> \
--region us-east-1 \
--query 'Role.AssumeRolePolicyDocument'

Detach the ReadOnlyAccess Policy

aws iam detach-role-policy \
--role-name <ROLE_NAME> \
--policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess \
--region us-east-1

Replace <ROLE_NAME> with the actual role name (e.g., CrossAccountReadRole).

Verify the Change

aws iam list-attached-role-policies \
--role-name <ROLE_NAME> \
--region us-east-1

The ReadOnlyAccess policy should no longer appear in the output.

CloudFormation - Secure Alternative

Instead of using ReadOnlyAccess, create a role with scoped permissions. This template demonstrates a secure cross-account role with:

  • Specific trusted account (no wildcards)
  • External ID requirement for additional security
  • Least-privilege permissions limited to specific resources
AWSTemplateFormatVersion: '2010-09-09'
Description: IAM role with scoped read-only permissions (least privilege)

Parameters:
RoleName:
Type: String
Description: Name for the IAM role
Default: ScopedReadOnlyRole
TrustedAccountId:
Type: String
Description: AWS Account ID allowed to assume this role
AllowedPattern: '^\d{12}$'
ConstraintDescription: Must be a valid 12-digit AWS account ID
ExternalId:
Type: String
Description: External ID for additional security
NoEcho: true

Resources:
ScopedReadOnlyRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Ref RoleName
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub 'arn:aws:iam::${TrustedAccountId}:root'
Action: 'sts:AssumeRole'
Condition:
StringEquals:
'sts:ExternalId': !Ref ExternalId
Policies:
- PolicyName: ScopedReadOnlyPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: LimitedS3Read
Effect: Allow
Action:
- 's3:GetObject'
- 's3:ListBucket'
Resource:
- 'arn:aws:s3:::example-bucket'
- 'arn:aws:s3:::example-bucket/*'
Tags:
- Key: Purpose
Value: ScopedCrossAccountAccess

Outputs:
RoleArn:
Description: ARN of the scoped read-only role
Value: !GetAtt ScopedReadOnlyRole.Arn

Deploy the template:

aws cloudformation deploy \
--template-file template.yaml \
--stack-name scoped-readonly-role \
--parameter-overrides \
TrustedAccountId=123456789012 \
ExternalId=YourSecretExternalId \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
Terraform - Secure Alternative

This Terraform configuration creates a secure cross-account role with scoped permissions:

terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0"
}
}
}

variable "role_name" {
description = "Name for the IAM role"
type = string
default = "ScopedReadOnlyRole"
}

variable "trusted_account_id" {
description = "AWS Account ID allowed to assume this role"
type = string
validation {
condition = can(regex("^\\d{12}$", var.trusted_account_id))
error_message = "Must be a valid 12-digit AWS account ID."
}
}

variable "external_id" {
description = "External ID for additional security"
type = string
sensitive = true
}

variable "allowed_s3_bucket_arn" {
description = "ARN of the S3 bucket to grant read access to"
type = string
}

data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]

principals {
type = "AWS"
identifiers = ["arn:aws:iam::${var.trusted_account_id}:root"]
}

condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
}
}

data "aws_iam_policy_document" "scoped_read" {
statement {
sid = "LimitedS3Read"
effect = "Allow"
actions = [
"s3:GetObject",
"s3:ListBucket"
]
resources = [
var.allowed_s3_bucket_arn,
"${var.allowed_s3_bucket_arn}/*"
]
}
}

resource "aws_iam_role" "scoped_readonly" {
name = var.role_name
assume_role_policy = data.aws_iam_policy_document.assume_role.json

tags = {
Purpose = "ScopedCrossAccountAccess"
}
}

resource "aws_iam_role_policy" "scoped_readonly" {
name = "ScopedReadOnlyPolicy"
role = aws_iam_role.scoped_readonly.id
policy = data.aws_iam_policy_document.scoped_read.json
}

output "role_arn" {
description = "ARN of the scoped read-only role"
value = aws_iam_role.scoped_readonly.arn
}

Apply the configuration:

terraform init
terraform apply \
-var="trusted_account_id=123456789012" \
-var="external_id=YourSecretExternalId" \
-var="allowed_s3_bucket_arn=arn:aws:s3:::your-bucket-name"

Verification

After remediation, confirm the fix:

  1. Re-run the Prowler check:

    prowler aws -c iam_role_cross_account_readonlyaccess_policy
  2. Verify in the console: Go to IAM > Roles > select the role > Permissions tab. The ReadOnlyAccess policy should not be listed.

CLI Verification Commands

Check attached policies:

aws iam list-attached-role-policies \
--role-name <ROLE_NAME> \
--region us-east-1

Expected output should not include:

{
"PolicyName": "ReadOnlyAccess",
"PolicyArn": "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

Additional Resources

Notes

  • Do not delete the role entirely unless you are certain it is no longer needed. Detaching the policy is safer than deletion.
  • If cross-account access is required, replace ReadOnlyAccess with a custom policy granting only the specific permissions needed for the use case.
  • Use External IDs when granting cross-account access to prevent the "confused deputy" problem.
  • Consider using AWS Organizations SCPs to prevent overly permissive cross-account roles from being created in the future.
  • Audit trust policies regularly to ensure only authorized accounts can assume your roles.