Back to blog
Best PracticesCloud SecurityGCPOperations & ComplianceStorage

GCP Storage Bucket Retention Policy Not Locked: Why It Matters and How to Fix It

Learn why an unlocked GCP Storage retention policy is a compliance and security risk, and how to lock it with gcloud, Terraform, and CI policy gates.

TL;DR

This check flags GCP Storage buckets that have a retention policy set but not locked, meaning the policy can be shortened or removed at any time. Lock the policy with gcloud storage buckets update gs://BUCKET --lock-retention-period to make retention immutable and tamper proof.

Retention policies are how you tell Cloud Storage to keep objects around for a minimum period before anyone can delete or overwrite them. They are the backbone of compliance requirements like SEC 17a-4, HIPAA record retention, and your own legal hold processes. But a retention policy on its own is only a soft control. Until you lock it, the policy is just a setting that an administrator, a compromised service account, or an automation script can roll back in seconds.

The storage_retentionnotlocked check catches exactly that gap: a bucket where retention is configured but the lock has never been applied.


What this check detects

Lensix inspects each Cloud Storage bucket and reads its retention policy. A bucket can be in one of three states:

  • No retention policy — objects can be deleted at any time (not flagged by this check).
  • Retention policy set, not locked — a minimum retention period exists, but it can be reduced or removed. This is what the check flags.
  • Retention policy set and locked — the policy is permanent and cannot be shortened or removed. This is the passing state.

You can see the current state with a quick describe:

gcloud storage buckets describe gs://my-compliance-bucket \
  --format="json(retention_policy)"

A flagged bucket returns something like this, with isLocked absent or false:

{
  "retention_policy": {
    "effectiveTime": "2024-02-11T18:30:00.000Z",
    "retentionPeriod": "2592000"
  }
}

A locked bucket includes "isLocked": true. That single field is the difference between a control that holds up under audit and one that does not.

Note: Retention period in the API is expressed in seconds. The 2592000 above is 30 days. A common mistake is reading it as days and assuming a far longer window than you actually have.


Why it matters

An unlocked retention policy gives you a false sense of security. The bucket looks protected in the console, the policy is right there, and everyone moves on. The problem is that anyone with storage.buckets.update permission can quietly change it.

The insider and ransomware angle

Picture an attacker who has phished a service account key or escalated into a role with storage admin rights. If your retention policy is unlocked, their path to destroying evidence or holding your data hostage is trivial:

# Attacker removes the retention policy...
gcloud storage buckets update gs://my-compliance-bucket --clear-retention-period

# ...then deletes everything
gcloud storage rm --recursive gs://my-compliance-bucket/**

With a locked policy, both of those commands fail. The lock survives even an account with full project owner rights, because Google enforces it at the platform level rather than through IAM.

Danger: An unlocked retention policy provides zero protection against a malicious or compromised administrator. If the bucket holds audit logs, financial records, or backups, treat an unlocked policy as an open door to evidence tampering and data destruction.

The compliance angle

Regulations that mandate write-once-read-many (WORM) storage do not accept a policy that can be reversed. SEC Rule 17a-4(f) and FINRA both require that records be stored in a non-rewriteable, non-erasable format. An unlocked retention policy does not satisfy that. If an auditor pulls the bucket metadata and sees isLocked is false, you are out of compliance regardless of how long the retention period is set to.

The accidental angle

Not every incident is an attack. A Terraform refactor, a misfired cleanup script, or an engineer trying to "fix" a bucket they think is misconfigured can all strip retention from an unlocked bucket. Locking removes that entire class of human error.


How to fix it

Locking is a one-way action. Before you run it, make sure the retention period is exactly what you want, because you can extend it later but you can never reduce it or remove it.

Warning: Locking a retention policy is permanent and cannot be undone. Once locked, the bucket cannot be deleted until every object inside it has met the retention period. Double check the period before you commit.

Step 1: Set the retention period (if not already set)

# Example: 7 years (220752000 seconds) for financial records
gcloud storage buckets update gs://my-compliance-bucket \
  --retention-period=220752000s

Step 2: Verify the current policy

gcloud storage buckets describe gs://my-compliance-bucket \
  --format="json(retention_policy)"

Confirm the retentionPeriod matches your requirement. This is your last chance to change it.

Step 3: Lock the policy

Danger: The command below is irreversible. After it runs, the retention period on this bucket can never be shortened or removed for the lifetime of the bucket.

gcloud storage buckets update gs://my-compliance-bucket \
  --lock-retention-period

You will be prompted to confirm. After it completes, re-run the describe command and confirm you now see:

{
  "retention_policy": {
    "effectiveTime": "2024-02-11T18:30:00.000Z",
    "isLocked": true,
    "retentionPeriod": "220752000"
  }
}

Doing it in Terraform

If you manage buckets as code, set both the period and the lock in the resource so the policy is locked at creation time:

resource "google_storage_bucket" "compliance" {
  name          = "my-compliance-bucket"
  location      = "US"
  force_destroy = false

  retention_policy {
    retention_period = 220752000 # 7 years in seconds
    is_locked        = true
  }

  uniform_bucket_level_access = true
}

Warning: Setting is_locked = true in Terraform locks the policy on apply. Any future plan that tries to lower retention_period will fail, and a plan that tries to remove the block entirely will force resource recreation, which Terraform cannot do on a locked bucket containing un-aged objects. Decide your retention period carefully before the first apply.


How to prevent it from happening again

Fixing one bucket is easy. Making sure the next hundred buckets ship locked is the real win. Bake the requirement into the places where buckets get created and reviewed.

Catch it in CI with Terraform policy checks

Use Open Policy Agent (OPA) with Conftest to fail any plan that creates a retention policy without locking it:

package storage

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_storage_bucket"
  policy := resource.change.after.retention_policy[_]
  policy.retention_period > 0
  not policy.is_locked
  msg := sprintf("Bucket '%s' sets a retention period but does not lock it", [resource.change.after.name])
}

Wire it into your pipeline against a plan output:

terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json --policy policy/

Tip: If you do not want to lock buckets in dev or staging where you frequently tear things down, scope the policy by environment using a label or naming convention so only production compliance buckets are required to be locked.

Enforce with an Organization Policy

GCP does not yet offer a built in constraint specifically for retention locks, so the most reliable continuous enforcement comes from scheduled scanning. Run Lensix on a schedule so any bucket that gets a retention policy without a lock is flagged within your normal review window, then route the finding to the owning team.

Scripted sweep across a project

For a one-time audit of everything you already have, list buckets and check their lock status:

for bucket in $(gcloud storage buckets list --format="value(name)"); do
  locked=$(gcloud storage buckets describe gs://$bucket \
    --format="value(retention_policy.isLocked)" 2>/dev/null)
  period=$(gcloud storage buckets describe gs://$bucket \
    --format="value(retention_policy.retentionPeriod)" 2>/dev/null)
  if [ -n "$period" ] && [ "$locked" != "True" ]; then
    echo "UNLOCKED: gs://$bucket (period: ${period}s)"
  fi
done

Best practices

  • Decide the retention period before you create the bucket. Because locking is permanent, choosing the right period up front avoids being stuck with a value that is too long for the data.
  • Separate retention buckets by policy. Do not mix 30 day logs and 7 year financial records in one bucket. Different retention requirements belong in different buckets so each can be locked to the correct period.
  • Pair retention locks with object versioning and bucket lock together. Versioning protects against overwrites, retention protects against early deletion, and the lock makes the retention guarantee unbreakable.
  • Restrict storage.buckets.update. Even though a locked policy survives an admin, you still want to limit who can shorten retention on unlocked buckets and who can change policies before they are locked.
  • Document the lock in your compliance evidence. Export the bucket metadata showing isLocked: true and store it with your audit records. Auditors want proof, not assurances.
  • Watch the bucket deletion implications. A locked bucket cannot be deleted until all objects have aged past the retention period. Plan lifecycle and cost accordingly, since you are committing to store that data for the full term.

Note: A retention lock and a per-object hold are different tools. The retention lock enforces a minimum age for every object. A hold (event based or temporary) pins specific objects regardless of age, which is what you reach for during litigation. Use both where the situation calls for it.

The fix here is a single command, but the discipline behind it is what matters. A retention policy you can undo is a sticky note that says "please do not delete." A locked policy is a contract the platform enforces on your behalf, even against your own administrators. For any bucket holding records you are legally or operationally obligated to keep, that distinction is the whole point.