RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Cross-Account S3 Object Ownership Mapper
02. Root Cause Vector

Cross-account S3 access fails with 'Access Denied' when the object owner differs from the bucket owner and no ACL grants explicit access ...

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Security

Cross-Account S3 Object Ownership Mapper

Cross-account S3 access fails with 'Access Denied' when the object owner differs from the bucket owner and no ACL grants explicit access to the bucket-owning account.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(42L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash
# S3 Cross-Account Replication Script for PCI-Secure-Zone
# TODO: This is a temporary workaround for legacy compliance. Replace with Terraform ASAP.
# WARNING: Hardcoded secrets for dev environment only. DO NOT commit to prod.

# AWS Credentials for source account (legacy PCI bucket)
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="us-east-1"

# Target bucket in analytics account (owned by different team)
TARGET_BUCKET="arn:aws:s3:::analytics-prod-logs-pci-secure-zone"
SOURCE_BUCKET="pci-audit-logs-internal"

# Old IAM role ARN from decommissioned environment - kept for rollback
# ROLE_ARN="arn:aws:iam::123456789012:role/legacy-s3-replication-role"

# New service account token for API (TODO: rotate quarterly)
STRIPE_API_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"

# Database connection for metadata (Postgres RDS in VPC)
DB_URI="postgresql://app_user:SuperSecret123!@pci-db.internal.corp:5432/audit_logs?sslmode=require"

# Multi-line RSA private key for SFTP fallback (deprecated)
SSH_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArGJ7vS1vK6Nq9wC1hiQb+1YQKc7X7WkKZJjK8YH6qGjP\n... (truncated for brevity) ...\n-----END RSA PRIVATE KEY-----"

# Replicate objects with ACL disabled (compliance mandate)
aws s3 sync s3://$SOURCE_BUCKET s3://$TARGET_BUCKET \
  --acl bucket-owner-full-control \
  --metadata-directive REPLACE \
  --storage-class STANDARD_IA \
  --exclude "*.tmp" \
  --profile legacy-pci-profile

# VULNERABILITY: Cross-account sync fails when bucket owner != object owner and ACLs are disabled by S3 defaults
# The --acl flag is ignored if bucket has ACLs disabled (object ownership enforced), causing 'Access Denied' on target.

# Legacy cleanup hook (commented out after incident)
# aws s3 rm s3://$SOURCE_BUCKET --recursive --exclude "*" --include "*.log"

echo "Replication started. Check CloudWatch logs for errors."
# TODO: Add error handling and retry logic for transient failures
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Cross-Account S3 'Access Denied': Object Ownership Mismatch When Bucket Owner and Object Owner Differ

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15 mins


TL;DR

  • What broke: Account B uploaded objects into Account A's S3 bucket. Account B retained object ownership. Account A's IAM principals hit Access Denied because IAM policies only govern resources the principal's account owns—they cannot override ownership held by a foreign account.
  • How to fix it: Enable S3 Object Ownership = BucketOwnerEnforced on the bucket. This disables ACLs entirely and forces all newly written objects to be owned by the bucket owner. Pair this with a bucket policy Deny on s3:PutObject unless s3:x-amz-object-ownership is absent (ACLs are already disabled, so the condition is implicit).
  • Use our Client-Side Sandbox above to paste your bucket policy and cross-account IAM role—it will auto-detect the ownership misconfiguration and emit corrected, least-privilege policies without sending your ARNs to any external server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Raw error returned to the calling principal in Account A:

An error occurred (AccessDenied) when calling the GetObject operation:
  Access Denied

Or via S3 server access logs / CloudTrail:

{
  "eventName": "GetObject",
  "errorCode": "AccessDenied",
  "errorMessage": "Access Denied",
  "userIdentity": { "accountId": "111111111111" },
  "requestParameters": { "bucketName": "corp-data-bucket", "key": "uploads/report.csv" }
}

Immediate consequence: Every object written by Account B is inaccessible to Account A's Lambda functions, Glue jobs, and IAM roles—even the bucket owner. The bucket policy is irrelevant here. S3 evaluates object-level ownership first. If the object owner is not the bucket owner, the bucket owner's policies do not apply to that object unless the object owner explicitly grants access via ACL or the bucket enforces ownership transfer at write time.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not just an ops headache—it is an active data exfiltration and persistence vector.

Scenario 1 — Exfiltration via object squatting: A compromised Account B credential uploads malicious or sensitive objects into a shared bucket. Account A cannot read, delete, or audit those objects through standard IAM controls. Account A's security team is blind to the object's contents and cannot enforce data lifecycle policies on objects it does not own.

Scenario 2 — Ransomware-style lock-out: A malicious or misconfigured third-party pipeline writes thousands of objects into Account A's bucket. Account A cannot delete them (s3:DeleteObject is also blocked by ownership). Storage costs accrue; data pipeline SLAs break; incident response is blocked.

Scenario 3 — Compliance failure: PCI-DSS, HIPAA, and SOC 2 controls require the data custodian (Account A) to demonstrate full control over data at rest. Objects owned by a foreign account break that chain of custody. An auditor will flag this immediately.

Blast radius: All IAM roles, Lambda execution roles, Glue crawlers, EMR clusters, and SageMaker pipelines in Account A that read from this bucket are simultaneously broken. If this bucket is upstream of a data lake, the entire downstream DAG fails silently or with cryptic downstream errors.


How to Fix It (The Solution)

Root Cause in One Line

S3 access evaluation order: Object Owner ACL → Bucket Policy → IAM Policy. When the object owner is a foreign account, the bucket owner's IAM and bucket policies are evaluated only if the object owner has explicitly granted bucket-owner-full-control via ACL—or ACLs are disabled entirely via BucketOwnerEnforced.


This works only if you control Account B's upload code. Add a bucket policy that denies s3:PutObject unless the request includes the bucket-owner-full-control canned ACL.

# Bucket Policy on Account A's bucket
{
  "Version": "2012-10-17",
  "Statement": [
-   {
-     "Sid": "AllowCrossAccountPut",
-     "Effect": "Allow",
-     "Principal": { "AWS": "arn:aws:iam::222222222222:role/upload-role" },
-     "Action": "s3:PutObject",
-     "Resource": "arn:aws:s3:::corp-data-bucket/*"
-   }
+   {
+     "Sid": "AllowCrossAccountPutWithOwnerControl",
+     "Effect": "Allow",
+     "Principal": { "AWS": "arn:aws:iam::222222222222:role/upload-role" },
+     "Action": "s3:PutObject",
+     "Resource": "arn:aws:s3:::corp-data-bucket/*",
+     "Condition": {
+       "StringEquals": {
+         "s3:x-amz-acl": "bucket-owner-full-control"
+       }
+     }
+   }
  ]
}

Account B's upload code must also be patched:

# Account B — boto3 upload call
- s3_client.put_object(
-     Bucket='corp-data-bucket',
-     Key='uploads/report.csv',
-     Body=data
- )
+ s3_client.put_object(
+     Bucket='corp-data-bucket',
+     Key='uploads/report.csv',
+     Body=data,
+     ACL='bucket-owner-full-control'
+ )

Limitation: This relies on Account B being cooperative and correctly configured. Any upload that omits the ACL header will be denied, which breaks pipelines until Account B fixes their code.


Enterprise Best Practice — S3 Object Ownership: BucketOwnerEnforced (Disable ACLs)

This is the AWS-recommended, permanent fix for all new and existing buckets used in cross-account patterns. It disables ACLs entirely. All objects written to the bucket are automatically owned by the bucket owner regardless of which account uploaded them.

Step 1: Enable BucketOwnerEnforced via Terraform

 resource "aws_s3_bucket_ownership_controls" "corp_data" {
   bucket = aws_s3_bucket.corp_data.id
   rule {
-    object_ownership = "BucketOwnerPreferred"
+    object_ownership = "BucketOwnerEnforced"
   }
 }

Step 2: Remove the ACL condition from the bucket policy (ACLs no longer exist)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCrossAccountPut",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::222222222222:role/upload-role" },
      "Action": [
        "s3:PutObject",
+       "s3:GetObject",
+       "s3:DeleteObject"
      ],
-     "Resource": "arn:aws:s3:::corp-data-bucket/*",
-     "Condition": {
-       "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" }
-     }
+     "Resource": "arn:aws:s3:::corp-data-bucket/*"
    }
  ]
}

Step 3: Remove ACL header from Account B's upload code (it will be rejected if sent)

 s3_client.put_object(
     Bucket='corp-data-bucket',
     Key='uploads/report.csv',
     Body=data,
-    ACL='bucket-owner-full-control'
 )

Step 4: Migrate existing orphaned objects (objects uploaded before BucketOwnerEnforced was set still have the old ownership):

# Copy objects in-place to transfer ownership to bucket owner
aws s3 cp s3://corp-data-bucket/ s3://corp-data-bucket/ \
  --recursive \
  --metadata-directive REPLACE \
  --source-region us-east-1 \
  --region us-east-1

⚠️ This in-place copy re-writes objects under Account A's identity, transferring ownership. Test on a non-production prefix first. Large buckets require S3 Batch Operations.


💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Checkov — Block BucketOwnerPreferred or ObjectWriter at Plan Time

Add to your .checkov.yml:

checks:
  - CKV2_AWS_65  # Ensure S3 bucket ACL is disabled (BucketOwnerEnforced)

Run in pipeline:

checkov -d ./terraform --check CKV2_AWS_65 --hard-fail-on HIGH

2. OPA / Conftest Policy for Terraform Plans

# policy/s3_ownership.rego
package terraform.s3

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket_ownership_controls"
  resource.change.after.rule[_].object_ownership != "BucketOwnerEnforced"
  msg := sprintf(
    "S3 bucket '%v' must use BucketOwnerEnforced. Got: %v",
    [resource.name, resource.change.after.rule[_].object_ownership]
  )
}
terraform show -json tfplan.binary | conftest test -p policy/ -

3. AWS Config Rule — Continuous Compliance

Deploy the managed rule s3-bucket-acl-prohibited to all accounts in your AWS Organization via CloudFormation StackSets. This flags any bucket not set to BucketOwnerEnforced within minutes of creation.

# config-rule.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  S3AclProhibitedRule:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: s3-bucket-acl-prohibited
      Source:
        Owner: AWS
        SourceIdentifier: S3_BUCKET_ACL_PROHIBITED

4. SCPs — Preventive Control at the Org Level

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyS3PutWithACL",
      "Effect": "Deny",
      "Action": "s3:PutObject",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-acl": [
            "public-read",
            "public-read-write",
            "authenticated-read"
          ]
        }
      }
    }
  ]
}

Attach this SCP to the OU containing all workload accounts. This does not block bucket-owner-full-control ACL (for legacy compatibility) but kills public ACL grants at the organization boundary.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does a bucket policy granting Account A full access still result in 'Access Denied' for objects uploaded by Account B?
The root cause is a fundamental design flaw in S3's access control hierarchy: object ownership supersedes bucket policy. When Account B uploads an object without the 'bucket-owner-full-control' ACL, Account B retains exclusive ownership. At the access evaluation layer, Account A's bucket policy is never even consulted for that object—the request is denied outright at the ownership check. This creates a critical security blind spot where your intended policy is silently bypassed. The correct architectural fix is to enable BucketOwnerEnforced, which transfers ownership to the bucket owner at write time and makes your bucket policies the single source of truth.
Q2.Does enabling BucketOwnerEnforced break existing objects already in the bucket?
Absolutely, and this is where the manual remediation trap snaps shut. BucketOwnerEnforced only applies to new writes; all legacy objects retain their original, non-compliant ownership and ACLs. Manually migrating them requires a high-risk, in-place S3 copy operation, which is error-prone, can break object metadata, and risks data loss or exposure if misconfigured. The blast radius of a mistake is your entire dataset. Do not attempt this manually. Instead, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix, generating a safe, audited remediation script that preserves integrity and applies least-privilege principles.
Q3.Can I use S3 Object Ownership BucketOwnerEnforced on a bucket that already has ACLs configured?
Technically yes, but the operational and compliance nightmare begins here. AWS will block the change if the bucket ACL isn't default private, forcing a manual reset that can inadvertently revoke legitimate access. Once enabled, all existing object ACLs are permanently disabled—any application relying on them will break catastrophically. Scaling this fix across hundreds of buckets or a sprawling mono-repo is a compliance auditor's worst-case scenario, creating inconsistent security postures and untraceable access patterns. For enterprise-scale, automated PR drafting under Maker-Checker review, upgrade to the StackEngine CI/CD Automator (Team Edition). It provides on-prem, automated CI/CD pipelines to enforce this control uniformly, generate compliance evidence, and prevent regression, turning a manual nightmare into a governed standard.