Back to blog
Best PracticesCloud SecurityGCPOperations & ComplianceStorage

GCP Storage Bucket Not Using CMEK: Why It Matters and How to Fix It

Learn why GCP Storage buckets should use customer-managed KMS keys (CMEK), the risks of default encryption, and step-by-step CLI and Terraform fixes.

TL;DR

This check flags GCP Storage buckets that rely on Google's default encryption instead of a customer-managed encryption key (CMEK). Without CMEK you give up control over the key lifecycle, rotation, and revocation. Fix it by creating a Cloud KMS key and setting it as the bucket's default encryption key.

Every object you write to a Google Cloud Storage bucket is encrypted at rest by default. That part is not optional, and it is not the problem this check is about. The question is who controls the key. With Google-managed encryption keys, Google owns the entire key lifecycle and you have no direct say in rotation, access, or revocation. The storage_nocmk check looks for buckets that have not been configured to use a customer-managed encryption key (CMEK) backed by Cloud KMS.

For a personal project or a bucket holding public assets, the default is fine. For buckets holding regulated data, customer records, or anything subject to a compliance mandate, that lack of control becomes a real gap.


What this check detects

The check inspects each Cloud Storage bucket and reads its default encryption configuration. If the bucket has no defaultKmsKeyName set, it is using a Google-managed key (GMEK) rather than a customer-managed one, and the check fails.

You can see the same thing yourself with the CLI:

gcloud storage buckets describe gs://my-bucket \
  --format="value(encryption.defaultKmsKeyName)"

An empty result means the bucket is on the default Google-managed key. A value like projects/my-project/locations/us/keyRings/my-ring/cryptoKeys/my-key means CMEK is already in place.

Note: CMEK does not mean Google never touches your data. It means Google encrypts your objects with a key stored in Cloud KMS that you create, control, and can disable. There is a third tier, customer-supplied encryption keys (CSEK), where you provide raw key material on every request, but CMEK is the practical middle ground for most teams.


Why it matters

The difference between GMEK and CMEK comes down to control, and control matters most when something goes wrong.

Key revocation and the "kill switch"

With CMEK, you can disable or destroy the key. The moment you do, every object encrypted with it becomes unreadable, including by attackers who somehow gained read access to the bucket. With Google-managed keys you have no such lever. If you suspect a breach involving a sensitive bucket, the ability to instantly cut off decryption is a meaningful incident response capability.

Rotation on your terms

CMEK lets you set automatic rotation schedules and force rotations on demand. Many compliance frameworks expect documented, controllable key rotation. Pointing to "Google rotates it somehow" rarely satisfies an auditor.

Separation of duties

Because the key lives in Cloud KMS, access to the data and access to the key can be governed by separate IAM policies. A user with storage.objects.get still cannot read object contents unless they also have cloudkms.cryptoKeyVersions.useToDecrypt on the key. That second gate stops a single over-permissioned service account from being enough to exfiltrate data.

Note: Frameworks like CIS GCP Benchmark, PCI DSS, and HIPAA-aligned controls frequently call for customer-controlled keys on sensitive data stores. CMEK on Storage buckets is one of the most common findings flagged in cloud audits.


How to fix it

The fix has three parts: create a KMS key, grant the Storage service agent permission to use it, then point the bucket at it.

1. Create a key ring and key

Place the key in the same location as your bucket. A key in us works for a bucket in the us multi-region.

# Create a key ring
gcloud kms keyrings create storage-keyring \
  --location=us

# Create a symmetric encryption key with 90-day rotation
gcloud kms keys create my-bucket-key \
  --location=us \
  --keyring=storage-keyring \
  --purpose=encryption \
  --rotation-period=90d \
  --next-rotation-time=$(date -u -d "+90 days" +%Y-%m-%dT%H:%M:%SZ)

2. Grant the Cloud Storage service agent access to the key

Every project has a Cloud Storage service agent that performs encryption on your behalf. It needs the cloudkms.cryptoKeyEncrypterDecrypter role on the key, or the bucket update will fail.

# Get the service agent email for your project
SERVICE_AGENT=$(gcloud storage service-agent --project=my-project)

# Grant it permission on the key
gcloud kms keys add-iam-policy-binding my-bucket-key \
  --location=us \
  --keyring=storage-keyring \
  --member="serviceAccount:${SERVICE_AGENT}" \
  --role=roles/cloudkms.cryptoKeyEncrypterDecrypter

3. Set the key as the bucket default

gcloud storage buckets update gs://my-bucket \
  --default-encryption-key=projects/my-project/locations/us/keyRings/storage-keyring/cryptoKeys/my-bucket-key

Warning: Setting a default key only encrypts new objects written after the change. Existing objects keep their old encryption. To migrate them, rewrite each object in place. There is a cost and time impact for large buckets, and rewrites trigger object creation events that may fire downstream triggers.

To re-encrypt existing objects with the new key:

# Rewrites objects in place using the bucket's current default key
gcloud storage objects update gs://my-bucket/** \
  --encryption-key=projects/my-project/locations/us/keyRings/storage-keyring/cryptoKeys/my-bucket-key

Terraform example

If you manage infrastructure as code, wire the key into the bucket definition so the configuration is enforced on every apply:

resource "google_kms_key_ring" "storage" {
  name     = "storage-keyring"
  location = "us"
}

resource "google_kms_crypto_key" "bucket_key" {
  name            = "my-bucket-key"
  key_ring        = google_kms_key_ring.storage.id
  rotation_period = "7776000s" # 90 days

  lifecycle {
    prevent_destroy = true
  }
}

data "google_storage_project_service_account" "gcs" {}

resource "google_kms_crypto_key_iam_member" "gcs_binding" {
  crypto_key_id = google_kms_crypto_key.bucket_key.id
  role          = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
  member        = "serviceAccount:${data.google_storage_project_service_account.gcs.email_address}"
}

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

  encryption {
    default_kms_key_name = google_kms_crypto_key.bucket_key.id
  }

  depends_on = [google_kms_crypto_key_iam_member.gcs_binding]
}

Danger: Never disable or destroy a KMS key that a bucket still depends on unless you intend to make those objects permanently unreadable. Destroying a key version is reversible only within the scheduled-for-destruction window. After that, the data encrypted with it is gone for good. Use prevent_destroy in Terraform and restrict who holds cloudkms.cryptoKeyVersions.destroy.


How to prevent it from happening again

Manual remediation fixes today's buckets. To stop the finding from reappearing, push the requirement upstream.

Organization Policy constraint

GCP has a built-in constraint that requires CMEK and restricts which keys are allowed. Apply it at the org or folder level so new buckets cannot be created without an approved key:

# Restrict which KMS keys can be used (CMEK enforcement)
gcloud resource-manager org-policies allow \
  constraints/gcp.restrictNonCmekServices \
  storage.googleapis.com \
  --organization=ORG_ID

This makes the API reject any bucket creation that does not specify a CMEK for Cloud Storage.

Policy as code in CI/CD

If you provision through Terraform, gate merges with a policy check. An OPA/Conftest rule that fails when a bucket has no default_kms_key_name catches the problem before it reaches production:

package storage

deny[msg] {
  resource := input.resource.google_storage_bucket[name]
  not resource.encryption
  msg := sprintf("Bucket '%s' must use a customer-managed KMS key", [name])
}

Tip: Run continuous detection alongside preventive controls. Lensix flags storage_nocmk across all your projects on a schedule, which catches buckets created outside your IaC pipeline, in sandbox projects, or by a teammate who clicked through the console.


Best practices

  • Use a dedicated key per data sensitivity tier. Do not share one key across unrelated buckets. Separate keys give you finer-grained revocation and cleaner audit trails.
  • Enable automatic rotation. A 90-day rotation period is a common baseline. Rotation does not re-encrypt old objects, but it limits how much data any single key version protects.
  • Keep keys in the same location as the bucket. A regional bucket needs a regional key in the matching region, and a multi-region bucket needs a key in the matching multi-region. Mismatches cause errors.
  • Lock down KMS IAM tightly. The power of CMEK comes from controlling key access. Grant cryptoKeyEncrypterDecrypter only to the Storage service agent and limit human access to admins.
  • Log key usage. Enable Cloud Audit Logs for Cloud KMS so you have a record of every encrypt and decrypt operation. This is invaluable during an investigation.
  • Plan for key destruction carefully. Treat key deletion as a last resort with documented sign-off, since it destroys access to the underlying data.

CMEK is one of those controls that costs little to set up and pays off exactly when you need it most, during an incident or an audit. Configure it once, enforce it with org policy, and verify it continuously.