Every object in Cloud Storage is encrypted at rest, whether or not you configure anything. The real question is who controls the key that unlocks it, and what happens when that key is rotated, disabled or lost. Cloud Storage offers four answers: Google default encryption, customer-managed encryption keys (CMEK) in Cloud KMS, customer-supplied encryption keys (CSEK) sent with each request, and client-side encryption before upload. A bucket can also enforce which of the server-side options new objects are allowed to use.

This article explains the envelope model behind all four and compares them on control, cost and blast radius. It sets up CMEK correctly with gcloud, Python and Terraform, explains how objects bind to key versions, and walks through a locked-down training-data bucket. The failure modes and the operational checklist at the end are where most teams get hurt. For the storage basics, see Google Cloud Storage.

The envelope model and the four options

Cloud Storage uses envelope encryption. Object data is encrypted with data encryption keys (DEKs) using AES-256. The DEKs are stored next to the data, wrapped by a key encryption key (KEK). Reading an object means unwrapping the DEK with the KEK, then decrypting the data. The four options differ only in who holds the KEK, or, for client-side encryption, in adding a layer Google never sees.

Envelope encryption in Cloud Storage: the data key is always Google's; the wrapping key is your choiceClient uploadTLS in transitCloud Storageencrypts chunks, AES-256Data encryption keysper object data, stored wrappedDEKsWho holds the key that wraps the DEKs?Google defaultGoogle-managed keysCMEKCloud KMS key you controlCSEKAES-256 key sent per requestClient-sideencrypted before uploadCloud KMS / HSMor Cloud EKM, externalOnly a hash keptlose key, lose dataYour KMS or libraryGoogle sees ciphertextEach step right gives you more control and more ways to lose access to your own data.
The data path is the same for every server-side option. What changes is who controls the wrapping key.
OptionWho holds the keyRevocationMain risk
Google defaultGoogleNot available to youNone operational; may not meet a control requirement
CMEKCloud KMS key in your project (software, HSM or external via Cloud EKM)Disable the key version, or revoke the service agentDisabling or destroying the key makes data unreadable
CSEKYou; Google keeps only a hash to validate requestsStop supplying the keyLose the key and the object is gone for good
Client-sideYou, entirely outside Google CloudYour own KMSServer-side features cannot see plaintext

Encryption in transit is separate. Requests use TLS regardless of these choices. Note also what none of the server-side options protect against: anyone with IAM permission to read the object gets plaintext, because Cloud Storage decrypts on their behalf. CMEK adds a second gate, because the Cloud Storage service agent must still be allowed to use the key, but it does not replace IAM.

Setting up CMEK correctly

CMEK has three moving parts: a key ring and key in Cloud KMS, the project's Cloud Storage service agent, and the bucket or object that names the key. The key ring must be in the same location as the bucket, so a bucket in us-central1 needs a key ring in us-central1, and a multi-region US bucket needs one in us. Only symmetric keys are supported.

PROJECT=my-data-project
KEY=projects/my-kms-project/locations/us-central1/keyRings/storage/cryptoKeys/training-data

gcloud kms keyrings create storage --location=us-central1 --project=my-kms-project
gcloud kms keys create training-data --keyring=storage --location=us-central1 \
    --purpose=encryption --rotation-period=90d \
    --next-rotation-time=2026-11-01T00:00:00Z --project=my-kms-project

# Grant the Cloud Storage service agent of the DATA project encrypt/decrypt on the key
gcloud storage service-agent --project=$PROJECT --authorize-cmek=$KEY

# Make the key the bucket default; new objects use it unless they name another key
gcloud storage buckets update gs://training-data-prod --default-encryption-key=$KEY

# Or name a key for one object
gcloud storage cp shard-0001.tfrecord gs://training-data-prod/ --encryption-key=$KEY

The service agent is a Google-managed identity of the form service-PROJECT_NUMBER@gs-project-accounts.iam.gserviceaccount.com. It needs the Cloud KMS CryptoKey Encrypter/Decrypter role on the key; the --authorize-cmek command grants exactly that. Putting keys in a separate KMS project is a common pattern, because it separates the people who administer keys from the people who administer data. The same setup in Terraform and Python:

data "google_storage_project_service_account" "gcs" {
  project = "my-data-project"
}

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

resource "google_storage_bucket" "training" {
  name     = "training-data-prod"
  location = "US-CENTRAL1"
  uniform_bucket_level_access = true
  encryption {
    default_kms_key_name = google_kms_crypto_key.training_data.id
  }
  depends_on = [google_kms_crypto_key_iam_member.gcs_uses_key]
}
from google.cloud import storage

client = storage.Client()
bucket = client.bucket("training-data-prod")
blob = bucket.blob("manifests/v7.json", kms_key_name=KEY)   # per-object key
blob.upload_from_filename("v7.json")

The depends_on in Terraform is important. Without it, the bucket can be created before the grant exists, and the first write fails with a permission error that looks unrelated to encryption.

How objects bind to key versions

A CMEK key has versions, and each object is encrypted with whichever version was primary when it was written. The object's metadata records the full key name including the version. Four rules follow from that.

  1. Rotation is not re-encryption. When the key rotates, new writes use the new version immediately, and old objects stay on their old versions. Earlier versions are not disabled, so reads keep working.
  2. Re-encryption means rewriting. Changing an object's key or moving it to the current version takes a rewrite, not a metadata update. The command gcloud storage objects update gs://BUCKET/OBJECT --encryption-key=KEY does this in place. In code, use Blob.rewrite in a loop, because large rewrites return a continuation token.
  3. Disabling a version is an outage. Every object on that version becomes unreadable until the version is re-enabled. This is the kill switch CMEK promises, and it works on your own production traffic too.
  4. Destroying a version is permanent. Objects on a destroyed version can never be read again, even objects under a locked retention policy. Cloud KMS schedules destruction after a delay you configure on the key, which is your only window to change your mind.

In practice this means an old version should only be disabled after an inventory confirms no live object still uses it. If you need every object on the newest version, schedule a rewrite job after each rotation and verify it before touching old versions. Remember that object versioning keeps noncurrent copies on their original key versions; see object versioning.

Worked example: a CMEK-only training-data bucket

Suppose a team stores model-training data that a contract says must be encrypted with keys the company controls and can revoke. The goal is a bucket where nothing can land without the approved CMEK, and where access is auditable. Four steps get there.

  1. Create the key ring and key in us-central1 in a dedicated KMS project, with 90-day rotation, and authorize the data project's service agent as shown above.
  2. Create the bucket in us-central1 with the default key, uniform bucket-level access, and an encryption enforcement file that blocks the other two server-side types.
  3. Set organisation policy so new buckets cannot skip CMEK.
  4. Turn on Cloud KMS data-access audit logs in the KMS project, so every encrypt and decrypt is recorded.

The enforcement file, applied with --encryption-enforcement-file on create or update:

{
  "gmekEnforcement": {"restrictionMode": "FullyRestricted"},
  "cmekEnforcement": {"restrictionMode": "NotRestricted"},
  "csekEnforcement": {"restrictionMode": "FullyRestricted"}
}

This blocks Google default encryption and CSEK for any action that creates an object: uploads, copies, compose operations and restores of soft-deleted objects. At least one type must stay allowed, and because the bucket has a default key, CMEK or CSEK must remain allowed. Changes can take up to two minutes to take effect, and they do not touch existing objects, so an older bucket needs a rewrite pass to bring legacy objects into line. In the JSON API the same settings appear on the bucket resource as googleManagedEncryptionEnforcementConfig and its CMEK and CSEK siblings.

For the organisation policy, the list constraint constraints/gcp.restrictNonCmekServices with storage.googleapis.com in its deny list requires CMEK on new resources, and constraints/gcp.restrictCmekCryptoKeyProjects limits which projects may supply the keys. Together they stop someone from creating a fresh bucket with a key from a project nobody audits. Finally, verify one object with gcloud storage objects describe and confirm that its KMS key field names the expected key and a version.

Customer-supplied keys

With CSEK, you generate a 256-bit AES key and send it with every request that writes or reads the object. Cloud Storage uses it to wrap the object's keys, then discards it and keeps only a SHA-256 hash, so it can reject requests that present the wrong key. Over the APIs the key travels in the x-goog-encryption-algorithm, x-goog-encryption-key and x-goog-encryption-key-sha256 headers. The Python client handles them for you:

import os
from google.cloud import storage

key = os.urandom(32)                       # store this in your own secret system first
blob = storage.Client().bucket("vault").blob("ledger.parquet", encryption_key=key)
blob.upload_from_filename("ledger.parquet")
data = blob.download_as_bytes()            # same key required on every read

CSEK gives Google the least standing access of any server-side option, but the operational burden falls entirely on you. Every reader needs the key, including batch jobs and anyone using a signed URL, who must also send the headers; see signed URLs. Rotating means rewriting each object with the old key as the decryption key and the new key as the encryption key. Store the keys in a system with its own backups, such as Secret Manager, because a lost CSEK key cannot be recovered by anyone.

Failure modes

FailureSymptomPrevention
Key ring in the wrong locationBucket update or write rejectedCreate keys per bucket location; check before migrations
Service agent not authorizedWrites fail with permission errorsGrant before setting the default key; order it in IaC
Key version disabled by mistakeReads fail across many objects at onceInventory key usage first; alert on version state changes
Key version destroyedPermanent data lossLong destroy-scheduled duration; restrict destroy permission
CSEK key lostObjects unreadable foreverEscrow keys in a backed-up secret store
Sync tool compares hashesEndless re-copies of CMEK objectsJSON API listings omit MD5 and CRC32C for CMEK objects; fetch per-object metadata
Bulk rewrite hits KMS limitsThrottled or failed rewrite jobsCheck KMS quotas in the key project; rate-limit the job

The common thread is that CMEK makes key availability part of your data's availability. Treat the KMS project with the same care as production databases: limited admins, alerting on key state changes, and a runbook for re-enabling a version. Retention controls do not save you from a destroyed key; see retention policies.

Migrating an existing bucket to CMEK

Moving an existing bucket from Google default encryption to CMEK is a two-phase job, because setting a default key only affects objects written afterwards. Phase one is configuration: create and authorize the key, set it as the bucket default, and confirm that a fresh test upload carries the new key. Phase two is the backfill: rewrite every existing object so it is encrypted with the CMEK. Only after the backfill finishes should you apply an enforcement file that blocks default encryption, or a job that copies old objects inside the bucket could start failing midway.

Plan the backfill like any bulk data job. Large objects can need several rewrite calls each, and every rewrite is a write operation that is billed and that touches Cloud KMS. Run it in batches with checkpoints, track progress by listing objects and checking their key field, and leave noncurrent versions until you decide whether they also need re-encryption. Cloud KMS bills for active key versions and for cryptographic operations, so check current pricing before you add many keys or force frequent rotations.

Choosing an option

Use Google default encryption unless a requirement says otherwise. It costs nothing to operate and has no failure modes of your making. Choose CMEK when a regulation, contract or internal control requires keys you can audit, rotate on your schedule and revoke. It is the right answer for most of those cases, and Cloud KMS Autokey can create keys on demand to reduce setup work. Choose Cloud EKM, the external variant of CMEK, only when keys must physically stay outside Google; you then depend on the external key manager's availability for every read. Choose CSEK rarely: for a small set of objects where you already run solid key management. Use client-side encryption when Google must never see plaintext, and accept that server-side processing of the content becomes impossible.

What to do next

  • List your buckets and record which encryption each uses, including per-object exceptions in mixed buckets.
  • For each CMEK bucket, confirm the key location matches and the service agent grant is managed in code.
  • Add encryption enforcement files to buckets that must only hold CMEK objects, then rewrite legacy objects.
  • Apply restrictNonCmekServices and restrictCmekCryptoKeyProjects where policy requires CMEK.
  • Enable Cloud KMS audit logs and alert on key version disable and destroy events.
  • Lengthen the destroy-scheduled duration on production keys and restrict who can destroy versions.
  • Write and rehearse a runbook for an accidentally disabled key version.
  • If you use CSEK, test key recovery from your secret store before you need it.
Key takeaway: Cloud Storage always encrypts at rest; the choice is who holds the wrapping key. CMEK suits most compliance needs but ties data availability to key availability, so authorize the service agent in code, match key locations, rewrite after rotation when needed, enforce encryption types per bucket, and guard key disable and destroy like a production database.