Back to blog
AWSCloud SecurityMonitoring & LoggingOperations & ComplianceStorage

CloudTrail Bucket Lacks Delete Protection: Lock Down Your Audit Trail

Learn why CloudTrail log buckets need MFA Delete and deny-on-delete policies, plus step-by-step CLI and Terraform fixes to stop attackers from erasing your audit trail.

TL;DR

This check flags CloudTrail log buckets that allow object deletion without MFA. If an attacker compromises an IAM principal with S3 permissions, they can wipe your audit trail and cover their tracks. Fix it by enabling MFA Delete and adding a deny-on-delete bucket policy.

CloudTrail is the system of record for everything that happens in your AWS account. API calls, console logins, resource changes, policy edits, all of it lands in the S3 bucket your trail writes to. That bucket is one of the highest-value targets in your account, because deleting its contents is the cleanest way to erase evidence of an intrusion.

The account_cloudtrailbucketdeletepolicy check looks at the S3 bucket receiving your CloudTrail logs and verifies it has guardrails preventing those logs from being deleted without MFA. When the protection is missing, anyone with delete permissions on the bucket can quietly destroy your audit history.


What this check detects

The check inspects the destination bucket configured for your CloudTrail trails and confirms two things are in place to resist deletion:

  • MFA Delete on the bucket's versioning configuration, which requires a valid MFA token to permanently delete an object version or to disable versioning.
  • A bucket policy that explicitly denies s3:DeleteObject and related actions, ideally gated behind a condition that MFA was used.

If neither protection is present, the bucket is treated as exposed. A principal with s3:DeleteObject, s3:DeleteObjectVersion, or s3:DeleteBucket permissions could remove logs with a single API call, no second factor required.

Note: MFA Delete is a feature of S3 versioning, not a standalone setting. It only protects against permanent deletion of versions and against disabling versioning. It does not block ordinary uploads or reads, so it has no effect on CloudTrail's ability to write new logs.


Why it matters

Audit logs are only useful if you can trust them during an incident. The first thing a competent attacker does after gaining a foothold is look for ways to stay invisible. If your CloudTrail bucket has no delete protection, the attack path is short:

  1. Attacker compromises an IAM user, role, or access key with broad S3 permissions.
  2. They identify the CloudTrail bucket (often named predictably, like aws-cloudtrail-logs-123456789012-abc123).
  3. They run aws s3 rm or delete object versions to erase the log objects.
  4. Your forensic timeline now has a hole exactly where it matters most.

This is not theoretical. Anti-forensics through log deletion is a documented MITRE ATT&CK technique (T1070, Indicator Removal), and CloudTrail tampering shows up regularly in real cloud breach reports. Without the logs, your incident response team is reconstructing events from partial data, and your compliance posture takes a direct hit.

Warning: Frameworks like CIS AWS Foundations Benchmark, PCI DSS, and SOC 2 expect CloudTrail log integrity to be enforced. A bucket without delete protection can fail an audit even if no breach ever occurs.

There is also the mundane risk: a misconfigured cleanup script or a tired engineer running a recursive delete against the wrong bucket. Delete protection turns a catastrophic mistake into a blocked operation.


How to fix it

There are two layers to add. Apply both. They protect against different things and complement each other.

Step 1: Enable versioning and MFA Delete

MFA Delete can only be toggled by the bucket owner's root account using long-term credentials, and it requires a physical or virtual MFA device. This is the single most common stumbling block, so plan for it.

Danger: Enabling MFA Delete requires root account credentials and an MFA serial. Once enabled, every permanent version deletion and every versioning state change will require an MFA token. Make sure your log lifecycle and any automation accounts for this before you flip it on.

First confirm versioning is enabled, then enable MFA Delete in the same configuration call:

# Replace the bucket name, MFA device ARN, and current token code
aws s3api put-bucket-versioning \
  --bucket aws-cloudtrail-logs-123456789012-abc123 \
  --versioning-configuration Status=Enabled,MFADelete=Enabled \
  --mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456"

Verify it took effect:

aws s3api get-bucket-versioning \
  --bucket aws-cloudtrail-logs-123456789012-abc123

# Expected output:
# {
#     "Status": "Enabled",
#     "MFADelete": "Enabled"
# }

Step 2: Add a deny-on-delete bucket policy

MFA Delete covers version deletion, but a layered bucket policy makes the intent explicit and can block delete actions outright for everyone except a tightly scoped break-glass principal. Here is a policy that denies object and bucket deletion unless the request was authenticated with MFA:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDeleteWithoutMFA",
      "Effect": "Deny",
      "Principal": "*",
      "Action": [
        "s3:DeleteObject",
        "s3:DeleteObjectVersion",
        "s3:DeleteBucket",
        "s3:PutBucketVersioning"
      ],
      "Resource": [
        "arn:aws:s3:::aws-cloudtrail-logs-123456789012-abc123",
        "arn:aws:s3:::aws-cloudtrail-logs-123456789012-abc123/*"
      ],
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    }
  ]
}

Apply it:

aws s3api put-bucket-policy \
  --bucket aws-cloudtrail-logs-123456789012-abc123 \
  --policy file://cloudtrail-bucket-policy.json

Warning: Make sure your existing CloudTrail write policy statement (the one granting s3:PutObject to the CloudTrail service principal) stays in the policy. Bucket policies are not additive across calls. put-bucket-policy replaces the entire document, so merge your new deny statement into the current policy rather than overwriting it.

Console route

If you prefer the console:

  1. Open S3, select the CloudTrail bucket, then go to Properties.
  2. Under Bucket Versioning, confirm it is enabled. MFA Delete cannot be enabled from the console, so use the CLI for that piece.
  3. Go to Permissions, edit the Bucket policy, and add the deny statement above.
  4. While you are there, confirm Block Public Access is fully on for this bucket.

Terraform example

If you manage infrastructure as code, encode the protection so it cannot drift:

resource "aws_s3_bucket" "cloudtrail" {
  bucket = "aws-cloudtrail-logs-123456789012-abc123"
}

resource "aws_s3_bucket_versioning" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id
  versioning_configuration {
    status     = "Enabled"
    mfa_delete = "Enabled"
  }
}

data "aws_iam_policy_document" "cloudtrail_bucket" {
  statement {
    sid     = "DenyDeleteWithoutMFA"
    effect  = "Deny"
    actions = [
      "s3:DeleteObject",
      "s3:DeleteObjectVersion",
      "s3:DeleteBucket",
      "s3:PutBucketVersioning",
    ]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    resources = [
      aws_s3_bucket.cloudtrail.arn,
      "${aws_s3_bucket.cloudtrail.arn}/*",
    ]
    condition {
      test     = "BoolIfExists"
      variable = "aws:MultiFactorAuthPresent"
      values   = ["false"]
    }
  }
  # Keep your existing CloudTrail service write statements here too.
}

resource "aws_s3_bucket_policy" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id
  policy = data.aws_iam_policy_document.cloudtrail_bucket.json
}

Note: Terraform cannot supply the MFA token interactively, so the mfa_delete = "Enabled" argument will fail to apply on its own. Many teams enable MFA Delete manually once with the root credentials and CLI, then keep Terraform aware of the setting. Plan your workflow accordingly.


How to prevent it from happening again

Fixing one bucket is the easy part. Keeping every CloudTrail bucket protected across accounts and over time is where most teams slip. A few approaches that work:

Catch it in CI/CD with policy-as-code

Scan Terraform and CloudFormation before they merge. Tools like Checkov, tfsec, or OPA/Conftest can fail a pull request when a CloudTrail bucket lacks versioning or a delete-deny policy. A minimal Conftest rule:

package main

deny[msg] {
  resource := input.resource.aws_s3_bucket_versioning[name]
  contains(name, "cloudtrail")
  resource.versioning_configuration.mfa_delete != "Enabled"
  msg := sprintf("CloudTrail bucket '%s' must have MFA Delete enabled", [name])
}

Enforce with Service Control Policies

At the organization level, an SCP can deny anyone in member accounts from disabling versioning or deleting from log buckets, regardless of their IAM permissions. This is the strongest backstop because it overrides account-level grants.

Tip: Pair the SCP with a dedicated, centralized log archive account that only the security team can touch. Member accounts write logs cross-account but have no delete rights at all. This is the model AWS Control Tower sets up out of the box, and it removes the bucket from the blast radius of any single compromised workload account.

Continuous monitoring

Run Lensix against every account on a schedule so a newly created or reconfigured trail bucket gets flagged within hours, not at the next annual audit. Drift happens, and a bucket that was compliant last quarter can be changed by a well-meaning automation update.


Best practices

  • Enable log file validation. Turn on --enable-log-file-validation on your trails so CloudTrail writes digest files. If logs are tampered with, validation will detect it even when deletion is blocked.
  • Centralize logs in a locked-down account. Keep the destination bucket out of the accounts that generate the events. Least privilege at the account boundary beats least privilege at the IAM policy.
  • Consider S3 Object Lock for the strongest guarantee. Object Lock in compliance mode makes objects truly immutable for a retention period, with no override even by root. It is heavier than MFA Delete but appropriate for regulated workloads.
  • Block all public access on the bucket. A CloudTrail bucket should never be reachable from the internet.
  • Encrypt with SSE-KMS and scope the key policy so only CloudTrail and your security team can decrypt.
  • Send a copy to a SIEM. Even with bucket protection, streaming logs to a separate system means an attacker would have to compromise two independent platforms to fully blind you.

Delete protection on the CloudTrail bucket is a small change with an outsized payoff. It turns your audit trail from something an attacker can erase in seconds into something they cannot touch without a second factor you control. Combine it with centralized logging and log file validation, and you have an audit history you can actually rely on when it counts.