S3 Role External Access#

It provides the mechanism to create an IAM role with access on a specific location (path) within a S3 bucket. The access policy is assigned to the root of the external account, which is responsible for creating the internal policies to allow its internal roles to assume the role.

Features#

  • Fine-grained S3 access: Grant access to a specific bucket and prefix, not the entire bucket

  • External account trust: Only the specified AWS account ID can assume the role

  • Optional External ID: Support for External ID to prevent confused deputy attacks (recommended for third-party access)

  • Configurable access level: Choose between READ, WRITE, or READ_WRITE permissions

  • Optional bucket listing: Optionally allow the external account to list all buckets

  • Secure by default: No public access, and permissions are limited to the specified path

  • Tagging support: Apply custom tags to IAM resources for organization and cost tracking

Outputs#

This module exports the following outputs:

Name

Description

iam_role_arn

The ARN of the IAM role created for external access

iam_policy_arn

The ARN of the IAM policy attached to the role

Access Levels#

  • READ: Allows listing and reading objects under the specified prefix

  • WRITE: Allows uploading objects under the specified prefix

  • READ_WRITE: Allows both reading and writing (full access) under the specified prefix

Access Level

s3:GetObject

s3:PutObject

s3:ListBucket

READ

WRITE

READ_WRITE

Security Considerations#

  • The role can only be assumed by the specified external AWS account

  • The policy restricts access to the given bucket and prefix only

  • Use external_id parameter for third-party access to prevent confused deputy attacks

  • The external account should create internal policies limiting which users/roles can assume this role

  • Account ID validation ensures only valid 12-digit AWS account IDs are accepted

How to Use#

Variables#

Name

Description

Type

Default

Required

role_name

Name of the IAM role

string

n/a

Yes

aws_external_account

AWS Account ID (12 digits) of the external account

string

n/a

Yes

bucket_name

Name of the S3 bucket to grant access to

string

n/a

Yes

prefix

Prefix/path within the bucket where access is granted

string

n/a

Yes

access_level

Access level: READ, WRITE, or READ_WRITE

string

“READ”

No

allow_list_buckets

Allow listing all S3 buckets

bool

false

No

external_id

Optional External ID for additional security

string

null

No

tags

Tags to apply to IAM resources

map(string)

{}

No

Basic Example#

resource "aws_s3_bucket" "s3_bucket" {
  bucket = "shared-bucket-xcfu-uysters"
}

resource "aws_s3_bucket_public_access_block" "block_public_access" {
  bucket = aws_s3_bucket.s3_bucket.id

  block_public_acls = true
  block_public_policy = true
  ignore_public_acls = true
  restrict_public_buckets = true
}

module "some_external_access" {
  source = "./modules/s3/role_external_access"

  aws_external_account = "123456789012"
  role_name = "role_for_client"
  bucket_name = aws_s3_bucket.s3_bucket.bucket
  prefix = "uploads/client"
  access_level = "READ"
}

Advanced Example with External ID and Tags#

module "third_party_access" {
  source = "./modules/s3/role_external_access"

  aws_external_account = "987654321098"
  role_name = "ThirdPartyDataAccess"
  bucket_name = "company-shared-data"
  prefix = "external/partner-a"
  access_level = "READ"

  # Security: External ID for confused deputy protection
  external_id = "unique-external-id-from-third-party"

  # Optional: Allow them to list all buckets
  allow_list_buckets = true

  # Tags for organization
  tags = {
    Environment = "production"
    ManagedBy = "terraform"
    Partner = "partner-a"
    CostCenter = "data-sharing"
  }
}

# Use the output
output "partner_role_arn" {
  value = module.third_party_access.iam_role_arn
}

Configuration on the External Account#

Because the access is granted to the root of the external account, the administrator of it could create an IAM policy allowing other users to access the bucket location.

IAM Policy#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::{{account_own_bucket}}:role/{{role_name}}"
    }
  ]
}

AWS CLI Commands#

Accessing the bucket location using the AWS CLI console commands.

Assuming the Role#

aws sts assume-role --role-arn arn:aws:iam::060923676874:role/role_for_nissan --role-session-name testing-s3-role

Listing the Buckets#

aws s3 ls

We will get an error because the lack of permission, we can grant it via: allow_list_buckets = true.

An error occurred (AccessDenied) when calling the ListBuckets operation: User:
arn:aws:sts::******:assumed-role/role_for_nissan/testing-s3-role is not authorized to
perform: s3:ListAllMyBuckets because no identity-based policy allows
the s3:ListAllMyBuckets action.

Listing Objects in Unauthorized Locations#

aws s3 ls s3://shared_bucket
aws s3 ls s3://shared_bucket/uploads

Errors will be raised:

An error occurred (AccessDenied) when calling the ListObjects operation: User:
arn:aws:sts::******:assumed-role/role_for_nissan/testing-s3-role is not authorized to
perform: s3:ListBucket on resource: "arn:aws:s3:::shared_bucket" because no
identity-based policy allows the s3:ListBucket action.

Listing Objects in Authorized Locations#

aws s3 ls s3://shared_bucket/uploads/nissan/

Outputs example:

2024-01-10 15:30:09          0
2025-01-15 10:21:32         21 report.txt
2025-01-21 10:28:06      19565 sample_data.csv

Downloading Files#

aws s3 cp s3://shared_bucket/uploads/nissan/report.txt .

For READ and READ_WRITE permissions we will get download: s3://shared_bucket/uploads/nissan/report.txt to ./report.txt, otherwise fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden.

Uploading Files#

aws s3 cp ./report.txt s3://shared_bucket/uploads/nissan/report.txt

For WRITE and READ_WRITE permissions we will get upload: ./report.txt to s3://shared_bucket/uploads/nissan/report.txt, otherwise:

upload failed: ./report.txt to s3://shared_bucket/uploads/nissan/report.txt
An error occurred (AccessDenied) when calling the PutObject operation: User:
arn:aws:sts::******:assumed-role/role_for_nissan/testing-s3-role is not authorized to
perform: s3:PutObject on resource: "arn:aws:s3:::shared_bucket/uploads/nissan/report.txt"
because no identity-based policy allows the s3:PutObject action.