Initializing Enclave...

Fixing S3 AccessDenied on DeleteBucket: Object Lock Compliance Mode Blocking Deletion

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–45 mins depending on retention mode


TL;DR

  • What broke: s3:DeleteBucket is hard-blocked because the bucket has S3 Object Lock active — either objects are under a Compliance-mode retention period, the bucket still contains locked objects, or the IAM principal lacks s3:BypassGovernanceRetention.
  • How to fix it: Identify the lock mode (Governance vs. Compliance), purge all object versions and delete markers after retention expires or with bypass, then delete the empty bucket using a principal with the correct permissions.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor your IAM policy or Terraform config — your ARNs never leave your browser.

The Incident (What Does the Error Mean?)

You called DeleteBucket and AWS returned:

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

Or via the SDK:

botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling
the DeleteBucket operation: Access Denied

Immediate consequence: The bucket cannot be deleted. This is not a transient permission error — S3 Object Lock is a write-once-read-many (WORM) enforcement mechanism that operates at the S3 data-plane level, below IAM. Even a root account cannot delete a bucket in Compliance mode while locked objects exist. Your pipeline is blocked, your Terraform destroy is failing, and the bucket is accruing cost.

The three distinct failure paths that produce this error:

Root Cause Lock Mode Fixable Without Waiting?
Objects under Governance retention Governance Yes, with s3:BypassGovernanceRetention
Objects under Compliance retention Compliance No — must wait for retention expiry
Bucket not empty (locked versions exist) Either Only after objects are removed

The Attack Vector / Blast Radius

This is a data durability control that doubles as an operational trap. Here is why it is dangerous in both directions:

From a security standpoint — this is working as intended. Object Lock Compliance mode is the primary defense against ransomware actors who obtain AWS credentials and attempt to wipe S3 buckets. Even s3:DeleteBucket with s3:DeleteObject and s3:DeleteObjectVersion will all fail. An attacker with full S3 permissions cannot destroy Compliance-locked data. This is the blast radius you want for backup and audit buckets.

From an operational standpoint — the blast radius is your own pipeline:

  • Terraform destroy hangs or fails, leaving dangling state and orphaned resources that block reprovisioning.
  • Account cleanup automation breaks — automated decommission scripts silently fail, and the bucket continues billing.
  • Governance mode misconfiguration is the dangerous middle ground: if your team thinks they have Compliance mode protecting backups but actually has Governance mode, a compromised IAM role with s3:BypassGovernanceRetention can delete everything. Audit this immediately.
  • Compliance mode with an incorrect retention period (e.g., 7300 days set by accident) means you are legally and operationally locked out for 20 years. There is no AWS support escalation path to override Compliance mode retention.

How to Fix It (The Solution)

Step 0: Diagnose the Lock Mode First

# Check bucket-level Object Lock configuration
aws s3api get-object-lock-configuration --bucket YOUR_BUCKET_NAME

# Check a specific object version's retention
aws s3api get-object-retention \
  --bucket YOUR_BUCKET_NAME \
  --key YOUR_OBJECT_KEY \
  --version-id YOUR_VERSION_ID

If the mode is COMPLIANCE and RetainUntilDate is in the future: stop here. You cannot delete these objects until the date passes. Contact your compliance team.

If the mode is GOVERNANCE: proceed to the fix below.


Basic Fix — Governance Mode: Delete with Bypass

You need the s3:BypassGovernanceRetention permission AND must pass the x-amz-bypass-governance-retention: true header.

Step 1: Grant the permission to your IAM principal

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
-       "s3:DeleteObject",
-       "s3:DeleteObjectVersion",
-       "s3:DeleteBucket"
+       "s3:DeleteObject",
+       "s3:DeleteObjectVersion",
+       "s3:DeleteBucket",
+       "s3:BypassGovernanceRetention",
+       "s3:ListBucketVersions",
+       "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::YOUR_BUCKET_NAME",
        "arn:aws:s3:::YOUR_BUCKET_NAME/*"
      ]
    }
  ]
}

Step 2: Delete all object versions and delete markers, then the bucket

# Delete all object versions with bypass header
aws s3api list-object-versions --bucket YOUR_BUCKET_NAME \
  --query 'Versions[].{Key:Key,VersionId:VersionId}' \
  --output text | while read KEY VERSIONID; do
    aws s3api delete-object \
      --bucket YOUR_BUCKET_NAME \
      --key "$KEY" \
      --version-id "$VERSIONID" \
      --bypass-governance-retention
done

# Delete all delete markers
aws s3api list-object-versions --bucket YOUR_BUCKET_NAME \
  --query 'DeleteMarkers[].{Key:Key,VersionId:VersionId}' \
  --output text | while read KEY VERSIONID; do
    aws s3api delete-object \
      --bucket YOUR_BUCKET_NAME \
      --key "$KEY" \
      --version-id "$VERSIONID"
done

# Now delete the empty bucket
aws s3api delete-bucket --bucket YOUR_BUCKET_NAME

Enterprise Best Practice — Least Privilege with Condition Keys

Never grant s3:BypassGovernanceRetention broadly. Scope it to a break-glass role with MFA enforcement and CloudTrail alerting.

{
  "Version": "2012-10-17",
  "Statement": [
-   {
-     "Effect": "Allow",
-     "Principal": {"AWS": "arn:aws:iam::ACCOUNT_ID:role/DeployRole"},
-     "Action": "s3:*",
-     "Resource": "*"
-   }
+   {
+     "Sid": "AllowGovernanceBypassOnlyWithMFA",
+     "Effect": "Allow",
+     "Principal": {"AWS": "arn:aws:iam::ACCOUNT_ID:role/BreakGlassS3Role"},
+     "Action": [
+       "s3:BypassGovernanceRetention",
+       "s3:DeleteObjectVersion",
+       "s3:DeleteBucket"
+     ],
+     "Resource": [
+       "arn:aws:s3:::YOUR_BUCKET_NAME",
+       "arn:aws:s3:::YOUR_BUCKET_NAME/*"
+     ],
+     "Condition": {
+       "Bool": {
+         "aws:MultiFactorAuthPresent": "true"
+       },
+       "NumericLessThan": {
+         "aws:MultiFactorAuthAge": "3600"
+       }
+     }
+   }
  ]
}

Terraform equivalent for the break-glass role:

 resource "aws_iam_role_policy" "s3_object_lock_bypass" {
   name = "s3-object-lock-bypass"
   role = aws_iam_role.break_glass.id
 
   policy = jsonencode({
     Version = "2012-10-17"
     Statement = [{
       Effect = "Allow"
       Action = [
-        "s3:*"
+        "s3:BypassGovernanceRetention",
+        "s3:DeleteObjectVersion",
+        "s3:DeleteBucket",
+        "s3:ListBucketVersions",
+        "s3:ListBucket"
       ]
-      Resource = "*"
+      Resource = [
+        "arn:aws:s3:::${var.bucket_name}",
+        "arn:aws:s3:::${var.bucket_name}/*"
+      ]
+      Condition = {
+        Bool = {
+          "aws:MultiFactorAuthPresent" = "true"
+        }
+      }
     }]
   })
 }

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Checkov — Catch overly permissive Object Lock bypass in Terraform plans:

# .checkov.yml
checks:
  - CKV_AWS_145   # Ensure S3 bucket has versioning enabled
  - CKV2_AWS_61   # Ensure S3 bucket has MFA delete enabled
  - CKV_AWS_372   # Custom: Block s3:* wildcard on Object Lock buckets

2. OPA/Conftest policy — Block wildcard S3 actions on Object Lock buckets:

package terraform.s3_object_lock

deny[msg] {
  resource := input.resource.aws_iam_role_policy[_]
  policy := json.unmarshal(resource.policy)
  statement := policy.Statement[_]
  statement.Effect == "Allow"
  statement.Action == "s3:*"
  msg := sprintf(
    "IAM policy '%v' grants s3:* wildcard — forbidden on Object Lock buckets. Scope to explicit actions.",
    [resource]
  )
}

deny[msg] {
  resource := input.resource.aws_s3_bucket_object_lock_configuration[_]
  resource.rule.default_retention.mode == "COMPLIANCE"
  days := resource.rule.default_retention.days
  days > 3650
  msg := sprintf(
    "Compliance retention of %v days exceeds 10 years. Verify this is intentional — it cannot be reduced.",
    [days]
  )
}

3. GitHub Actions gate — Validate Object Lock config before apply:

- name: Validate S3 Object Lock retention sanity
  run: |
    LOCK_CONFIG=$(aws s3api get-object-lock-configuration \
      --bucket ${{ env.BUCKET_NAME }} 2>/dev/null || echo "NONE")
    if echo "$LOCK_CONFIG" | grep -q '"Mode": "COMPLIANCE"'; then
      echo "::warning::COMPLIANCE mode Object Lock detected on ${{ env.BUCKET_NAME }}. Destroy will require retention expiry."
      exit 1
    fi

4. CloudTrail Alarm — Alert on BypassGovernanceRetention usage:

{
  "source": ["aws.s3"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventName": ["DeleteObject", "DeleteObjectVersion"],
    "requestParameters": {
      "x-amz-bypass-governance-retention": ["true"]
    }
  }
}

Route this EventBridge rule to SNS → PagerDuty. Any governance bypass in production should wake someone up.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →