Back to blog
Best PracticesCloud SecurityGCPReliabilityStorage

Storage Bucket Versioning Not Enabled on GCP: Risks and Fixes

Learn why GCP Storage buckets without object versioning are a data loss and ransomware risk, plus CLI, Terraform, and policy-as-code fixes to enforce it.

TL;DR

This check flags GCP Storage buckets without object versioning. Without it, an overwrite or delete is permanent, leaving you exposed to accidental loss, buggy deploys, and ransomware. Fix it with gcloud storage buckets update gs://BUCKET --versioning.

Object versioning is one of those features that feels unnecessary right up until the moment you need it. A bad deploy pushes a corrupted config to a bucket, someone runs a recursive delete against the wrong path, or an attacker gets write access and starts encrypting your objects. If versioning is off, the previous state is gone. There is no undo button.

The storage_noversioning check looks at your GCP Cloud Storage buckets and reports any bucket where object versioning is disabled. This post explains what that means, why it should be on for most of your buckets, and how to fix and enforce it.


What this check detects

Cloud Storage buckets in GCP have a setting called object versioning. When it is enabled, Cloud Storage keeps older copies of an object whenever it is overwritten or deleted. Those older copies are called noncurrent versions and are stored alongside the live version, each identified by a unique generation number.

When versioning is disabled (the default for new buckets), overwriting an object replaces it in place and deleting an object removes it entirely. There is no retained history. The storage_noversioning check fires on any bucket where this setting is off.

Note: Versioning in GCP is a bucket-level setting, not an object-level one. You turn it on for the whole bucket, and it applies to every object created or modified after that point. It does not retroactively create history for existing objects.


Why it matters

The risk here is data durability against human and malicious action, not against hardware failure. Cloud Storage already replicates your data and gives you very high durability at the infrastructure level. What it does not protect against by default is someone deleting or overwriting the data on purpose or by mistake.

Accidental deletion and overwrites

The most common real-world scenario is boring but expensive. An engineer runs a sync job with the wrong flags, a CI pipeline uploads an empty artifact over a good one, or a cleanup script matches more objects than intended. With versioning off, the recovery path is restoring from a separate backup if one exists. With versioning on, you copy the previous generation back over the live object in seconds.

Ransomware and malicious tampering

If an attacker compromises a service account or set of credentials with write access to a bucket, one of the first things they may do is overwrite or delete objects, then demand payment. Versioning does not stop them from writing, but it preserves the original objects as noncurrent versions, which gives you a recovery option that does not involve negotiating with anyone.

Warning: Versioning alone is not a complete ransomware defense. An attacker with storage.objects.delete permission can also delete noncurrent versions. Pair versioning with retention policies or bucket locks for objects that must survive a full account compromise.

Compliance and audit requirements

Frameworks like SOC 2, ISO 27001, and various data retention regulations expect you to be able to recover prior states of data and demonstrate that you protect against accidental loss. Versioning, combined with lifecycle rules, is a straightforward way to satisfy those controls for object storage.


How to fix it

Enabling versioning on an existing bucket is non-destructive and takes effect immediately for future operations.

Using the gcloud CLI

gcloud storage buckets update gs://my-bucket --versioning

Confirm the setting is now on:

gcloud storage buckets describe gs://my-bucket --format="value(versioning.enabled)"

That should return True. To enable it across many buckets at once, loop over them:

for bucket in $(gcloud storage buckets list --format="value(name)"); do
  gcloud storage buckets update "gs://${bucket}" --versioning
  echo "Enabled versioning on ${bucket}"
done

Warning: Versioning increases storage cost because noncurrent versions consume space and are billed like any other object. A bucket with frequently overwritten objects can grow quickly. Plan to pair versioning with a lifecycle rule (covered below) to control this.

Using the Google Cloud Console

  1. Open Cloud Storage and select your bucket.
  2. Go to the Protection tab.
  3. Under Object versioning, click Edit.
  4. Toggle versioning on and optionally set how many noncurrent versions to keep, then save.

Using Terraform

If you manage buckets as code, set the versioning block on the resource:

resource "google_storage_bucket" "data" {
  name     = "my-bucket"
  location = "US"

  versioning {
    enabled = true
  }

  # Keep version growth in check
  lifecycle_rule {
    condition {
      num_newer_versions = 5
    }
    action {
      type = "Delete"
    }
  }

  lifecycle_rule {
    condition {
      days_since_noncurrent_time = 90
    }
    action {
      type = "Delete"
    }
  }
}

The two lifecycle rules above keep at most the five most recent noncurrent versions and delete any noncurrent version older than 90 days, whichever applies. Adjust those numbers to match your recovery window and budget.

Tip: If you want long retention without unbounded cost, transition older noncurrent versions to a cheaper storage class instead of deleting them. Add a lifecycle rule with SetStorageClass targeting NEARLINE or COLDLINE based on age.


Recovering an object after versioning is on

This is the payoff. To list all versions of an object, including noncurrent ones:

gcloud storage ls --all-versions gs://my-bucket/path/to/object.json

Each result includes a generation number. To restore a specific older version over the current one, copy it by generation:

gcloud storage cp \
  gs://my-bucket/path/to/object.json#1681234567890123 \
  gs://my-bucket/path/to/object.json

Danger: Deleting noncurrent versions is permanent. Commands like gcloud storage rm --all-versions remove the full history and undo the protection versioning provides. Never run version-purging commands against production buckets without a tested lifecycle policy and a clear understanding of what gets removed.


How to prevent it from happening again

Fixing one bucket is easy. Keeping every bucket compliant as your org grows is the real work. A few layers help here.

Enforce it with Organization Policy

GCP does not ship a built-in constraint that forces versioning on, so most teams enforce it through infrastructure as code plus a policy check rather than a hard org constraint. The most reliable approach is to make versioning a required field in your bucket module so no bucket can be created without it.

Gate it in CI/CD

If you use Terraform, run a policy-as-code check on every plan. With Conftest and OPA, a simple Rego rule rejects any bucket missing versioning:

package terraform.storage

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_storage_bucket"
  not resource.change.after.versioning[0].enabled
  msg := sprintf("bucket %q must have versioning enabled", [resource.change.after.name])
}

Wire that into the pipeline so a non-compliant plan fails before it reaches apply:

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

Detect drift continuously

Policy gates only cover resources created through your pipeline. Buckets created by hand in the console, by another team, or by a third-party tool slip past them. Continuous scanning closes that gap. Lensix runs storage_noversioning across all your GCP projects on an ongoing basis and flags any bucket where versioning is off, including ones created outside your IaC workflow.

Tip: Combine a CI/CD policy gate for new buckets with continuous scanning for existing ones. The gate keeps you from regressing, and the scan catches anything that bypassed the gate.


Best practices

  • Default versioning on for stateful buckets. Anything holding application data, user uploads, backups, Terraform state, or build artifacts should have it enabled.
  • Always pair versioning with lifecycle rules. Unbounded version growth is a cost surprise waiting to happen. Set a retention count, an age limit, or both.
  • Protect critical buckets with retention policies. For data that must survive a full credential compromise, add a retention policy or bucket lock so noncurrent versions cannot be purged before a fixed period.
  • Separate Terraform state buckets and lock them down. State files are high value and frequently overwritten. Versioning plus tight IAM on these buckets is non-negotiable.
  • Right-size IAM. Versioning is a recovery mechanism, not a substitute for least privilege. Limit who and what holds storage.objects.delete and avoid handing out broad write access to long-lived service accounts.
  • Test your recovery path. A protection feature you have never exercised is a guess. Periodically restore an object from a noncurrent version so you know the process works before you need it under pressure.

Versioning is cheap insurance for one of the most common and most embarrassing kinds of incident: data that gets clobbered and cannot be brought back. Turn it on, bound it with lifecycle rules, and enforce it so it stays on.