Cross-account S3 access lets principals in one AWS account read or write buckets in another, but misconfiguration can expose data, enable privilege escalation, or violate compliance.
Granting cross-account access to Amazon S3 is a common pattern for data sharing and multi-tenant architectures, but every policy you attach to a bucket or IAM role expands the attack surface. Understanding the risks, mitigation techniques, and monitoring strategies is essential for anyone responsible for securing data in S3.
Organizations use multiple AWS accounts for isolation, billing, or SDLC environments. Sooner or later those accounts must exchange data: analytics teams pull raw logs from a production account, external vendors drop CSV exports for finance, or an acquired company migrates legacy datasets. Amazon S3 makes this easy with bucket policies, IAM roles, and AWS Resource Access Manager (RAM). Unfortunately, a single overly permissive statement may expose petabytes of sensitive data to unintended principals.
The most reported S3 incident is still the “world-readable bucket.” When Account B is granted s3:GetObject
using the wildcard principal "*"
or an AWS account ID that was typed incorrectly, the data can be publicly listed and downloaded. Attackers continuously scan for such misconfigured buckets.
Cross-account write access (s3:PutObject
, s3:DeleteObject
) enables data poisoning. A compromised developer account could overwrite trusted Parquet files with malicious payloads that downstream jobs automatically load. Similarly, s3:DeleteBucket
permissions can wipe critical data.
Using S3 Replication without strict scoping can create circular replication rules. Attackers who obtain permissions in Account B can push versioned objects back to Account A, polluting or overwriting original data.
It’s common to let an external account assume an IAM role that owns a bucket. If sts:AssumeRole
is too permissive (e.g., wildcard external ID), an attacker could pivot from S3 to other services like DynamoDB or even create new IAM users.
A misconfigured VPC endpoint policy can allow principals from Account B to access private buckets in Account A’s VPC, bypassing network security controls.
Sharing data with another account may violate PCI-DSS segmentation or GDPR residency if that account is outside a defined compliance boundary. Without proper tagging and logging, auditors cannot prove who accessed which dataset.
The bucket policy is the main enforcement point. Every Principal
, Action
, and Condition
triple must be evaluated for least privilege. Object ACLs and S3 Access Points add additional layers that must align.
Roles that the external account assumes should include trust policies with specific ExternalId
conditions, restricting who can call sts:AssumeRole
. Inline or attached policies on those roles should allow only S3 actions that are required.
When S3 PrivateLink endpoints (Interface VPC Endpoints) are involved, both the endpoint policy and the bucket policy must be correct. Otherwise, data can be exfiltrated privately, making detection harder.
s3:GetObject
, s3:ListBucket
) and only on specific ARNs."*"
in Resource
and Principal
.Require a unique External ID that the third-party must supply when calling AssumeRole
. This mitigates the confused deputy problem.
Add bucket policy conditions: "aws:SecureTransport": "true"
and "aws:MultiFactorAuthPresent": "true"
where feasible.
S3 Block Public Access should be enabled at both the account and bucket level unless you are intentionally hosting public content. Cross-account access can coexist with blocked public access when using IAM roles.
For intra-organization sharing, RAM provides a higher-level abstraction with automated principal management. RAM shares respect SCPs, making them easier to audit.
Confidentiality=High
) and require IAM condition keys "s3:RequestObjectTag/Confidentiality": "High"
for writes.Enable AWS CloudTrail, S3 Server Access Logging, and Amazon GuardDuty. Write Detective controls in AWS Config to flag policies that include wildcards.
Suppose Account A owns bucket log-archive-123
and wants Account B’s Snowflake ingestion role to read only yesterday’s logs.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::222222222222:role/snowflake-reader"
},
"Action": ["s3:GetObject"],
"Resource": [
"arn:aws:s3:::log-archive-123/2023-*/parquet/*"
],
"Condition": {
"StringEquals": {
"s3:DataAccessPointAccount": "111111111111"
}
}
}]
}
Account B must then assume snowflake-reader
with an external ID like sf-prod-ingest
.
kms:Decrypt
permission can read the data.Why it’s wrong: "Principal": "*"
or "AWS": "*"
means every AWS account can access the resource.
Fix: Always specify the exact account or IAM role ARN. If multiple accounts require access, enumerate them explicitly.
Why it’s wrong: Even if the bucket policy is strict, individual objects can be made public by a writer.
Fix: Add a bucket policy denying PutObject
unless the ACL is private
and the request is encrypted.
Why it’s wrong: Without CloudTrail or GuardDuty, data theft can go unnoticed.
Fix: Enable logging in all regions and set up automated alerts for anomalous access patterns, such as a new external IP reading large volumes.
The following Terraform snippet creates an IAM role in Account A that Account B can assume, the corresponding bucket policy, and a Config rule to detect wildcard principals.
```hcl# IAM role in Account Aresource "aws_iam_role" "shared_logs_reader" { name = "shared-logs-reader" assume_role_policy = jsonencode({ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::222222222222:root" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": {"sts:ExternalId": "sf-prod-ingest"} } }] })}resource "aws_iam_role_policy" "reader_policy" { role = aws_iam_role.shared_logs_reader.id policy = jsonencode({ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::log-archive-123/*" }] })}# S3 bucket policy granting access via roleresource "aws_s3_bucket_policy" "log_archive_policy" { bucket = "log-archive-123" policy = jsonencode({ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "AWS": aws_iam_role.shared_logs_reader.arn }, "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::log-archive-123/*" }] })}# AWS Config rule to detect wildcard principals in bucket policiesresource "aws_config_config_rule" "s3_bucket_policy_no_wildcards" { name = "s3-bucket-policy-no-wildcards" source { owner = "AWS" source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED" }}```
S3 stores critical business data. Granting access to other AWS accounts is necessary for data sharing but introduces risks like public exposure, data poisoning, privilege escalation, and compliance violations. Misconfigurations can be exploited within minutes. Understanding these risks enables teams to design least-privilege policies, use encryption, and monitor for threats—essential skills for any data engineer or security professional.
Create an IAM role in your account with s3:GetObject
on the specific bucket prefix. Require an External ID and enable Server-Side Encryption. Provide the role ARN, not access keys, to the vendor.
Yes. BPA blocks public principals but explicit IAM roles from other accounts remain valid, as long as the bucket policy allows them.
Absolutely. Access Analyzer reviews bucket and IAM policies, identifying resources shared outside your organization or publicly accessible.
Galaxy is a SQL editor and doesn’t manage S3 policies directly. However, if you use S3 as external tables in a data warehouse queried via Galaxy, you must secure S3 just as rigorously to prevent unauthorized SQL reads.