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
- Open the IAM Console and select Roles from the left menu
- Search for and click on the role name flagged by Prowler
- Go to the Permissions tab
- Find ReadOnlyAccess in the list of attached policies
- Click the X or Remove button next to it
- 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:
-
Re-run the Prowler check:
prowler aws -c iam_role_cross_account_readonlyaccess_policy -
Verify in the console: Go to IAM > Roles > select the role > Permissions tab. The
ReadOnlyAccesspolicy 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
- AWS IAM Best Practices
- AWS Managed Policy: ReadOnlyAccess
- How to Use External ID for Cross-Account Access
- Grant Least Privilege
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
ReadOnlyAccesswith 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.