RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
S3 DeleteBucket Object Lock Compliance Monitor
02. Root Cause Vector

S3 DeleteBucket fails with AccessDenied when Object Lock is enabled because the bucket's compliance or governance retention policy is act...

03. Rapid Prescription

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

Zero-Trust Client IDE/ Security

S3 DeleteBucket Object Lock Compliance Monitor

S3 DeleteBucket fails with AccessDenied when Object Lock is enabled because the bucket's compliance or governance retention policy is actively blocking deletion at the IAM and S3 control-plane level.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(72L)
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# S3 Bucket Cleanup Script (Legacy PCI Zone)
# WARNING: This script is part of the legacy compliance automation suite.
# TODO: Refactor to use the new IAM roles from the pci-secure-zone-iam module.
# FIXME: The retry logic on AccessDenied is broken since Q4 last year.

import boto3
import os
import sys
from datetime import datetime

# ------------------------------------------------------------------
# CONFIGURATION (loaded from old env files, some vars are deprecated)
# ------------------------------------------------------------------
# TODO: Migrate to AWS Secrets Manager (JIRA ticket PCI-1023)
AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE'
AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
# DB URI for the legacy audit log (PostgreSQL)
LEGACY_AUDIT_DB_URI = 'postgresql://audit_user:SuperSecretPass123!@pci-db-internal.legacy-api:5432/audit_log'
# Stripe key for the old billing webhook (should be decommissioned)
STRIPE_API_SECRET_KEY = 'sk_live_4eC39HqLyjWDarjtT1zdp7dc'
# ------------------------------------------------------------------

# Old function to get client with mixed config styles (some hardcoded, some env)
def get_s3_client():
    # TODO: The region should come from the 'pci-secure-zone' tag, but the tag service is flaky.
    session = boto3.Session(
        aws_access_key_id=AWS_ACCESS_KEY_ID,
        aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
        region_name='us-east-1'  # Hardcoded legacy region
    )
    # VULNERABILITY: This client is used for a delete operation on a bucket that may have Object Lock enabled.
    # The IAM policy attached to the key does not allow s3:DeleteBucket when Object Lock is active,
    # but the script assumes all buckets in the 'legacy-archive' lifecycle are deletable.
    return session.client('s3')

def delete_bucket_if_old(bucket_name):
    """
    Deletes a bucket if it matches the old naming pattern and is empty.
    This is part of the 'legacy-archive' cleanup initiated by the CronJob.
    """
    client = get_s3_client()
    try:
        # Check if bucket exists (redundant, but left from old audit code)
        client.head_bucket(Bucket=bucket_name)
        # List objects (paginated) - commented out old version that had a bug with markers
        # objects = client.list_objects_v2(Bucket=bucket_name, MaxKeys=1000)
        # if 'Contents' not in objects:
        #     client.delete_bucket(Bucket=bucket_name)
        # New logic: just delete if name starts with 'legacy-archive-' (per compliance directive)
        if bucket_name.startswith('legacy-archive-'):
            # VULNERABILITY: This call will fail with AccessDenied if the bucket has Object Lock enabled,
            # as the IAM policy for the hardcoded key lacks s3:DeleteBucket permission for such buckets.
            # The script does not check for Object Lock configuration before attempting deletion.
            client.delete_bucket(Bucket=bucket_name)
            print(f"[INFO] Deleted bucket: {bucket_name}")
        else:
            print(f"[SKIP] Bucket {bucket_name} does not match legacy pattern.")
    except Exception as e:
        # Generic error handling (bad practice, but it's legacy code)
        print(f"[ERROR] Failed to process bucket {bucket_name}: {str(e)}")
        # TODO: Implement proper logging to the LEGACY_AUDIT_DB_URI
        sys.exit(1)

if __name__ == '__main__':
    # Bucket name passed from the Terraform destroy hook (or old CronJob)
    target_bucket = os.getenv('BUCKET_TO_CLEAN', 'legacy-archive-pci-data-2020')
    delete_bucket_if_old(target_bucket)
    # Redundant cleanup call (leftover from a previous refactor)
    # delete_bucket_if_old(target_bucket + '-backup')  # This sometimes causes race conditions
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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 above to auto-refactor your IAM policy or Terraform config — your ARNs never leave your browser.

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

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

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

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 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 — 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.

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.Can AWS root account override S3 Object Lock Compliance mode to delete a bucket?
No. S3 Object Lock in Compliance mode is an immutable data-plane control, enforced below the IAM and account layers. No principal—not root, not Support, not even AWS internal teams—can override it. The only path is to wait for every single object version's RetainUntilDate to expire, then manually delete all versions before the bucket deletion call. This is the core, unbreakable guarantee for regulations like SEC 17a-4 and HIPAA, proving that manual processes and privileged access are fundamentally obsolete for data sovereignty.
Q2.What is the difference between s3:BypassGovernanceRetention and Compliance mode when trying to delete a locked bucket?
Governance mode offers a dangerous, permission-based escape hatch (s3:BypassGovernanceRetention with a specific header) that creates a massive blast radius: a single misconfigured IAM policy or a script missing the correct header can lead to catastrophic, irreversible data loss or a compliance breach. Compliance mode has no bypass—it's a total trap for manual processes. If you're blocked, you're blocked until the calendar expires, which is a business-critical outage. Do not attempt to manually untangle this. Drop your broken Terraform manifest or AWS CLI script into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that generates the correct, safe sequence of operations without risky permissions.
Q3.Why does Terraform destroy fail with AccessDenied on S3 buckets with Object Lock even when my IAM role has AdministratorAccess?
Because AdministratorAccess is an IAM illusion—it doesn't grant the data-plane s3:BypassGovernanceRetention permission, and it's utterly powerless against Compliance mode. Terraform's force_destroy will blindly fail, leaving your infrastructure in a corrupted, half-destroyed state. Manually managing object version lifecycles and retention dates across even a handful of buckets is a compliance nightmare; at scale across a mono-repo with hundreds of locked buckets, it's an impossible, audit-failing liability. Upgrade to the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review. It enforces compliance-as-code, generates provable remediation pipelines, and eliminates the manual risk across your entire estate before you even run terraform apply.