Cloud KMS & CMEK
Envelope encryption, key IAM, rotation, kill switch.
Every byte you store in Google Cloud is already encrypted on disk. Create a bucket, drop a file in, and Google has quietly scrambled it with a key it generated and looks after for you. The encryption box on your security review is ticked before you touch anything. The question that actually decides the outcome is a different one. Who holds the key? Out of the box, the answer is Google, and only Google. Customer-managed encryption keys (CMEK, meaning the key protecting your data is one you create, control and can switch off) hand that answer over to you.
Your flat came with the lock the building fitted, and the property manager keeps a copy. That is fine for coats and furniture. It stops being fine the day you store something you would rather the manager could not reach, so you add your own padlock and keep the only key. CMEK is that padlock. You create a key inside a Google service called Cloud KMS (Key Management Service, a hosted vault that holds keys and does the sealing and opening on request), then point a bucket, a database or a disk at it. Google still runs the encryption machinery. It cannot open your data without asking your key first, and you decide who is allowed to ask.
There is a trick underneath, and rotation and revocation only make sense once you have seen it. Encrypting a terabyte of data straight from a key that lives inside KMS would mean pushing a terabyte through KMS on every read. Nobody does that. Instead Google generates a fast local key for that one pile of data, the data encryption key (DEK, the key that scrambles the actual bytes), and runs it over the file. Then it takes your KMS key, the key encryption key (KEK, a key whose only job is to encrypt other keys), and wraps the DEK with it. The wrapped DEK is stored right next to the data. Your KEK never leaves KMS. One key sealed inside another is called envelope encryption. To read anything back, the service has to ask KMS to unwrap the DEK with your key. Cut off access to your key and every DEK stays sealed. That is what turns a key into a kill switch.
Bring your own padlock
A key does not float around loose. It lives in a key ring, a named container tied to one region, like a labelled pegboard you hang related keys on. Choose the region with care. A key in europe-west1 can only protect resources in a compatible location, and you cannot move it later. Here is a ring for payments data, and inside it a key that rotates itself every 90 days.
gcloud kms keyrings create payments --location=europe-west1gcloud kms keys create bucket-cmek \--location=europe-west1 --keyring=payments \--purpose=encryption \--rotation-period=90d --next-rotation-time=2026-10-14T00:00:00Zgcloud kms keys describe bucket-cmek \--location=europe-west1 --keyring=payments
name: projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmekpurpose: ENCRYPT_DECRYPTprimary:name: projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmek/cryptoKeyVersions/1state: ENABLEDprotectionLevel: SOFTWAREalgorithm: GOOGLE_SYMMETRIC_ENCRYPTIONrotationPeriod: 7776000snextRotationTime: '2026-10-14T00:00:00Z'createTime: '2026-07-16T09:12:44.021Z'versionTemplate:algorithm: GOOGLE_SYMMETRIC_ENCRYPTIONprotectionLevel: SOFTWARE
Rotation means Cloud KMS mints a fresh version of the key on a schedule and makes that version the primary one for new writes. Old versions stay exactly where they are, still able to decrypt whatever they originally wrapped, so a rotation never forces you to re-encrypt anything. New objects get the new version. Old objects carry on with theirs. The next-rotation-time flag sets when the first rotation fires, and rotation-period sets the gap between every one after that.
One catch trips up nearly everyone the first time. The bucket does not encrypt as you. It encrypts as its own robot account, the Cloud Storage service agent, an identity Google creates in your project so the service can do work on your behalf. It is the building's mailroom clerk who actually turns the lock for you. Give that clerk no permission to use your padlock and every write dies with a permission error. So you grant the service agent the one KMS role that means 'you may encrypt and decrypt with this key', then attach the key to the bucket as its default.
gcloud storage service-agent --project=payments-prodgcloud kms keys add-iam-policy-binding bucket-cmek \--location=europe-west1 --keyring=payments \--member="serviceAccount:[email protected]" \--role=roles/cloudkms.cryptoKeyEncrypterDecryptergcloud storage buckets update gs://payments-data \--default-encryption-key=projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmek
Updated IAM policy for key [bucket-cmek].bindings:- members:- serviceAccount:[email protected]role: roles/cloudkms.cryptoKeyEncrypterDecrypteretag: BwYh3n2K9pE=version: 1Updating gs://payments-data/...Completed 1/1
Prove the padlock is on
Never trust that a control landed because the command exited cleanly. Ask the bucket which key it is using, then check a real object that is already sitting in there.
gsutil kms encryption gs://payments-datagsutil stat gs://payments-data/ledger-2026-07.parquet
Default encryption key for gs://payments-data:projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmekgs://payments-data/ledger-2026-07.parquet:Creation time: Thu, 16 Jul 2026 09:20:31 GMTStorage class: STANDARDContent-Length: 5242880KMS key: projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmek/cryptoKeyVersions/1
Attaching a key changes the default for new objects only. Files already in the bucket keep whatever key wrote them, so retrofitting CMEK onto old data means rewriting those objects to move them onto the new key. The same key covers more than buckets. Point a BigQuery dataset at it and every table created inside inherits it as the default, as long as the dataset lives in the key's region.
bq update \--default_kms_key=projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmek \payments-prod:ledger
Dataset 'payments-prod:ledger' successfully updated.
Who can use the key is the real fence
Here is the part people miss. The key carries its own IAM (Identity and Access Management, Google's record of who is allowed to do what to a resource). That record is a separate fence from the bucket's, and it is the one nobody thinks to read. For Cloud Storage you never hand the key to the person downloading a file. The service agent opens the padlock for them behind the scenes, so a reader only needs permission on the bucket itself. What matters is keeping the list of identities you grant encrypterDecrypter to as short as you can bear: the service agent, maybe one application account, nobody else. Everyone on that list can ask KMS to decrypt with the key, so one careless grant quietly widens who can turn your data back into plaintext, however tight the bucket looks. Split the roles as well. Whoever can rotate, disable or destroy the key (roles/cloudkms.admin) should not be the same identity that can decrypt with it (roles/cloudkms.cryptoKeyEncrypterDecrypter). Treat 'can use this key' as the sensitive permission it is, and audit who holds it the way you would audit who has a master key to the vault.
Blast radius is the reason to stop and think before reusing one key everywhere. A single key can be the default for a dozen buckets, several datasets and a fleet of disks spread across projects. Tidy, right up to the morning you disable it and all of them go dark together. Scope keys to a blast radius you can live with, usually one per workload or per sensitivity tier, rather than one key for the whole organisation. Cost is not what holds you back here. A key version runs a few cents a month plus a tiny charge per crypto operation, so having more keys is cheap.
The kill switch
Mid-incident, the fastest way to make a pile of data unreadable to an attacker who is already inside is to pull the key. You delete nothing. You disable the key version that wrapped the data, and every DEK sealed under it stops unwrapping at once. A read that would have returned plaintext a second earlier now fails at the KMS boundary.
gcloud kms keys versions disable 1 \--location=europe-west1 --keyring=payments --key=bucket-cmekgsutil cp gs://payments-data/ledger-2026-07.parquet .
name: projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmek/cryptoKeyVersions/1state: DISABLEDCopying gs://payments-data/ledger-2026-07.parquet...AccessDeniedException: 403 Cloud KMS error: the key versionprojects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmek/cryptoKeyVersions/1used to decrypt this object is disabled.
Re-enable the version and access comes straight back with no data lost, which is exactly why disabling rather than destroying is the move you make under pressure. Software CMEK like this still has Google running the crypto with key material Google stores. When you need harder separation, up to the point where the provider cannot decrypt at all, you move to keys held in tamper-resistant hardware or kept entirely outside Google. That is where the next lesson goes.
Envelope encryption is why CMEK scales. Your KEK in Cloud KMS never bulk-encrypts terabytes; it wraps short-lived DEKs that sit next to the objects. That same design is what makes disabling a version such a sharp lever, because every read path has to ask KMS to unwrap, so a disabled version fails closed across every object still sealed under it.
The operating habits matter as much as the tick box. Put keys in the region that matches the data, name rings after the workload they serve, keep the admin role away from the crypto-user role, and rehearse a disable and a re-enable in a lab long before you need it at 3 a.m. Attaching a key only changes the default for new writes, so plan the rewrite job for legacy objects rather than assuming history re-keyed itself.
Try this
If you created a CMEK earlier in the lesson, describe it again and confirm the bucket still names that key. If not, create a ring and a key in a lab project and attach it to a throwaway bucket.
gcloud kms keys describe bucket-cmek \--location=europe-west1 --keyring=payments \--format="yaml(name,primary.state,rotationPeriod)"gsutil kms encryption gs://payments-datagcloud kms keys get-iam-policy bucket-cmek \--location=europe-west1 --keyring=payments
name: projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmekprimary:state: ENABLEDrotationPeriod: 7776000sDefault encryption key for gs://payments-data:projects/payments-prod/locations/europe-west1/keyRings/payments/cryptoKeys/bucket-cmekbindings:- members:- serviceAccount:[email protected]role: roles/cloudkms.cryptoKeyEncrypterDecrypter
Takeaway
CMEK settles who holds the kill switch. It does not invent the encryption, because Google already did that part. Scope each key to a blast radius you can survive, hand encrypterDecrypter to the service agents that genuinely need it and to nobody else, and reach for disable rather than destroy when an incident means sealing the room right now.
Next you climb the custody ladder to HSM (hardware security module, tamper-resistant hardware that holds key material and never lets it out) and external keys, for the cases where Google holding even the wrapped key material is still too much trust.