
CMEK with Cloud KMS: Disks, Buckets, BigQuery & 90-Day Rotation via Terraform | 2026
Google Cloud encrypts data at rest by default using Google-Managed Encryption Keys (GMEK). Customer-Managed Encryption Keys (CMEK) via Cloud KMS give your organization cryptographic control, allowing you to manage key rotation, control access via IAM, and instantly revoke decryption rights. This guide provides production Terraform code for KMS Key Rings, automated 90-day key rotation, and service account IAM bindings across Compute Engine persistent disks, Cloud Storage buckets, and BigQuery datasets.
By Mateusz Chmielewski · Aug 22, 2026 · 15 min read
GMEK vs CMEK vs CSEK — Choosing the Right Encryption Model
Customer-Managed Encryption Keys (CMEK) allow GCP customers to protect data at rest using symmetric encryption keys generated and stored in Cloud KMS. While Google-Managed Encryption Keys (GMEK) handle encryption transparently out-of-the-box, CMEK grants organizations control over key lifecycle, rotation schedules, audit logging, and explicit access revocation.
Think of GMEK like a bank vault where the bank holds the master key for all deposit boxes — your items are safe, but the bank opens the box for authorized staff. CMEK is like adding a second custom padlock to your box where only your security team holds the key card (Cloud KMS). If you disable your key card, even bank employees cannot open the box.
| Concept | Explanation | When to use |
|---|---|---|
| GMEK (Google-Managed) | Default AES-256 encryption managed entirely by Google. Zero configuration, zero cost, no key rotation management required. | Standard workloads with no specific compliance or key sovereignty requirements. |
| CMEK (Customer-Managed) | Keys generated in Cloud KMS. Customers control key rotation, location, IAM access, and key destruction. | Regulated enterprise workloads (HIPAA, PCI-DSS, SOC 2) requiring cryptographic control. |
| CSEK (Customer-Supplied) | Customer provides raw AES-256 key material in every API call. Google never stores the key. | Extremely strict key sovereignty — higher operational complexity, unsupported by many GCP services. |
| Key Ring & CryptoKey | Key Ring is a regional grouping container for CryptoKeys. CryptoKeys hold individual key versions (v1, v2) rotated over time. | Group keys by region and risk domain (e.g. app-data-ring in europe-west1). |
| Service Agent IAM Bindings | Each GCP service (Compute Engine, Cloud Storage, BigQuery) uses a dedicated Google service agent service account to encrypt/decrypt using your KMS key. | Required step: grant roles/cloudkms.cryptoKeyEncrypterDecrypter to the service agent email. |
| Automated 90-Day Rotation | Cloud KMS automatically creates a new key version every 90 days for new writes. Old versions remain active for reads. | Security best practice — mitigates key compromise risk without re-encrypting existing data. |
Why CMEK is Mandatory for Enterprise Regulatory Compliance
Relying solely on default GMEK leaves organizations exposed during regulatory audits (SOC 2 Type II, ISO 27001, HIPAA). Auditors require proof of key rotation policies, explicit access logs of cryptographic operations, and the ability to instantly lock down compromised datasets by revoking key access.
Implementing CMEK via automated Terraform modules creates an immutable audit trail for every encryption and decryption request via Cloud KMS Cloud Audit Logs. If a security incident occurs, revoking key access instantly renders all associated persistent disks, buckets, and BigQuery datasets unreadable.
| Feature | thisService | altA | altB |
|---|---|---|---|
| Key Management | CMEK: Cloud KMS managed key lifecycle & rotation | GMEK: Google internal key management | CSEK: Self-hosted key infrastructure |
| Key Rotation | Automated (e.g. 90 days via KMS policy) | Automatic internal rotation | Manual client-side re-encryption |
| Key Revocation | Instant via IAM or disabling CryptoKey version | Not possible (managed by Google) | Instant (stop sending key) |
| Audit Trail | Detailed Cloud Audit Logs for every KMS operation | Standard storage access logs only | Client-side logs only |
| Service Coverage | Supported by almost all core GCP services | 100% of GCP services | Limited to Compute Engine and GCS |
Prerequisites
- GCP Project with Cloud KMS API (`cloudkms.googleapis.com`) enabled
- Terraform CLI v1.6+ installed
- IAM permissions: `roles/cloudkms.admin` and `roles/resourcemanager.projectIamAdmin`
- gcloud CLI configured and authenticated
Step-by-Step Guide
Step 1: Provision Cloud KMS Key Ring and CryptoKey with 90-Day Rotation
Create a Cloud KMS Key Ring in the target region and configure a symmetric CryptoKey with automatic 90-day rotation in Terraform. Key Rings must match the region of the resource being encrypted (e.g., europe-west1 bucket requires a europe-west1 Key Ring). Rotation policies ensure compliance without manual intervention.
# kms.tf — Key Ring & CryptoKey with 90-day rotation
resource "google_kms_key_ring" "prod" {
name = "prod-data-keyring"
location = "europe-west1"
project = var.project_id
}
resource "google_kms_crypto_key" "primary" {
name = "prod-primary-cmek"
key_ring = google_kms_key_ring.prod.id
rotation_period = "7776000s" # 90 days in seconds (90 * 86400)
purpose = "ENCRYPT_DECRYPT"
version_template {
algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION"
protection_level = "SOFTWARE" # Use HSM for Hardware Security Module
}
lifecycle {
prevent_destroy = true # Prevent accidental key deletion
}
}
Step 2: Discover and Grant Service Agent IAM Permissions
Identify the unique Google Service Agent accounts for Compute Engine, GCS, and BigQuery, and grant them `roles/cloudkms.cryptoKeyEncrypterDecrypter` on the CryptoKey. Google services perform encryption operations on your behalf using internal service accounts. Without explicit KMS IAM bindings, resource creation with CMEK will fail with permission denied.
# iam_service_agents.tf — Fetch service identities and grant KMS access
resource "google_project_service_identity" "gcs_service_account" {
provider = google-beta
service = "storage.googleapis.com"
project = var.project_id
}
resource "google_project_service_identity" "bigquery_service_account" {
provider = google-beta
service = "bigquery.googleapis.com"
project = var.project_id
}
# Compute Engine default service agent format: service-{project_number}@compute-system.iam.gserviceaccount.com
data "google_project" "current" {
project_id = var.project_id
}
locals {
compute_service_account = "service-${data.google_project.current.number}@compute-system.iam.gserviceaccount.com"
}
# Grant EncrypterDecrypter role on the CryptoKey
resource "google_kms_crypto_key_iam_binding" "cmek_encrypter_decrypter" {
crypto_key_id = google_kms_crypto_key.primary.id
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
members = [
"serviceAccount:${google_project_service_identity.gcs_service_account.email}",
"serviceAccount:${google_project_service_identity.bigquery_service_account.email}",
"serviceAccount:${local.compute_service_account}",
]
}
Step 3: Attach CMEK to Compute Engine Persistent Disks
Configure Compute Engine persistent disks and VM instances to use the CMEK CryptoKey for disk encryption in Terraform. Standard Compute Engine instances default to GMEK. Specifying `disk_encryption_key` ensures all boot and data disks are encrypted with your managed key.
# compute_disk.tf — CMEK encrypted persistent disk and VM instance
resource "google_compute_disk" "cmek_disk" {
name = "secure-data-disk"
type = "pd-ssd"
zone = "europe-west1-b"
size = 100
project = var.project_id
disk_encryption_key {
kms_key_self_link = google_kms_crypto_key.primary.id
}
depends_on = [google_kms_crypto_key_iam_binding.cmek_encrypter_decrypter]
}
resource "google_compute_instance" "app_vm" {
name = "secure-app-vm"
machine_type = "e2-medium"
zone = "europe-west1-b"
project = var.project_id
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
disk_encryption_key {
kms_key_self_link = google_kms_crypto_key.primary.id
}
}
attached_disk {
source = google_compute_disk.cmek_disk.id
device_name = "data-disk"
}
network_interface {
network = "default"
}
depends_on = [google_kms_crypto_key_iam_binding.cmek_encrypter_decrypter]
}
Step 4: Configure CMEK for Cloud Storage Buckets
Set the default KMS key for a Cloud Storage bucket in Terraform to force CMEK encryption on all uploaded objects. Without `default_event_based_hold` or default KMS key configuration, developers could upload objects without specifying encryption parameters, reverting to GMEK.
# gcs_cmek.tf — Bucket with default CMEK key
resource "google_storage_bucket" "cmek_bucket" {
name = "${var.project_id}-cmek-secure-vault"
location = "EUROPE-WEST1"
uniform_bucket_level_access = true
force_destroy = false
encryption {
default_kms_key_name = google_kms_crypto_key.primary.id
}
depends_on = [google_kms_crypto_key_iam_binding.cmek_encrypter_decrypter]
}
Step 5: Enforce CMEK on BigQuery Datasets and Tables
Apply CMEK encryption configuration to BigQuery datasets and individual tables in Terraform. BigQuery stores columnar data across distributed file systems. Applying CMEK ensures all underlying table files and temporary query partitions are encrypted with your key.
# bigquery_cmek.tf — BigQuery dataset with CMEK encryption
resource "google_bigquery_dataset" "cmek_dataset" {
dataset_id = "analytics_secure_ds"
friendly_name = "Secure Analytics Dataset"
location = "europe-west1"
default_table_expiration_ms = 360000000
default_encryption_configuration {
kms_key_name = google_kms_crypto_key.primary.id
}
depends_on = [google_kms_crypto_key_iam_binding.cmek_encrypter_decrypter]
}
resource "google_bigquery_table" "cmek_table" {
dataset_id = google_bigquery_dataset.cmek_dataset.dataset_id
table_id = "financial_records"
encryption_configuration {
kms_key_name = google_kms_crypto_key.primary.id
}
schema = <<EOF
[
{"name": "record_id", "type": "STRING", "mode": "REQUIRED"},
{"name": "amount", "type": "NUMERIC", "mode": "NULLABLE"},
{"name": "timestamp", "type": "TIMESTAMP", "mode": "REQUIRED"}
]
EOF
}
Step 6: Verify Key Rotation and Test Emergency Key Revocation
Execute gcloud commands to verify automatic rotation status and perform a safe key revocation test. Verifying rotation and emergency revocation procedures ensures your team can execute cryptographic kill-switch procedures during an active security incident.
# Step 1: List key versions to verify rotation configuration
gcloud kms keys describe prod-primary-cmek \
--keyring=prod-data-keyring \
--location=europe-west1 \
--format="yaml(name,rotationPeriod,nextRotationTime,primary)"
# Step 2: Manually trigger key rotation for testing
gcloud kms keys versions create \
--key=prod-primary-cmek \
--keyring=prod-data-keyring \
--location=europe-west1
# Step 3: Test emergency key version disabling (Simulate revocation)
# WARNING: Disabling the primary version makes matching encrypted data unreadable!
gcloud kms keys versions disable 1 \
--key=prod-primary-cmek \
--keyring=prod-data-keyring \
--location=europe-west1
# Step 4: Re-enable key version after test verification
gcloud kms keys versions enable 1 \
--key=prod-primary-cmek \
--keyring=prod-data-keyring \
--location=europe-west1
Step 7: Audit KMS Operations with Cloud Logging & BigQuery
Create a Cloud Logging sink to export Cloud KMS Audit Logs to BigQuery for real-time compliance reporting. Compliance frameworks require continuous monitoring of cryptographic key usage, including who requested decryption and when.
# audit_logging.tf — Export KMS logs to BigQuery
resource "google_logging_project_sink" "kms_audit_sink" {
name = "kms-audit-log-sink"
destination = "bigquery.googleapis.com/projects/${var.project_id}/datasets/${google_bigquery_dataset.cmek_dataset.dataset_id}"
filter = <<EOF
protoPayload.serviceName="cloudkms.googleapis.com"
AND protoPayload.methodName=("Decrypt" OR "Encrypt" OR "AsymmetricSign")
EOF
unique_writer_identity = true
}
resource "google_bigquery_dataset_iam_member" "sink_writer" {
dataset_id = google_bigquery_dataset.cmek_dataset.dataset_id
role = "roles/bigquery.dataEditor"
member = google_logging_project_sink.kms_audit_sink.writer_identity
}
Verification & Health Check
Best Practices
- Set Prevent Destroy Lifecycle Rule on KMS Keys
- Separate KMS Key Admin from Encrypter/Decrypter Roles
Common Mistakes
- {"errorCode":"PERMISSION_DENIED_KMS_SERVICE_AGENT","symptoms":"Error creating disk/bucket/dataset: Access Denied to KMS key.","rootCause":"The service agent account for the resource type was not granted roles/cloudkms.cryptoKeyEncrypterDecrypter on the CryptoKey.","fixCommand":"gcloud kms keys add-iam-policy-binding KEY_NAME --member=serviceAccount:SERVICE_AGENT_EMAIL --role=roles/cloudkms.cryptoKeyEncrypterDecrypter","code":"resource \"google_kms_crypto_key_iam_member\" \"fix_agent\" {\n crypto_key_id = google_kms_crypto_key.primary.id\n role = \"roles/cloudkms.cryptoKeyEncrypterDecrypter\"\n member = \"serviceAccount:${SERVICE_AGENT_EMAIL}\"\n}\n","language":"hcl","filename":"fix_kms_agent.tf","prevention":"Always use module dependencies or explicit depends_on blocks between KMS IAM bindings and resource creation."}
Cost Analysis
| Feature | metric | cost1k | cost10k | cost100k | cost1m |
|---|---|---|---|---|---|
| Active CryptoKey Version ($0.06/version/month) | $0.30/mo | $0.30/mo | $0.30/mo | $3.60/yr | |
| Rotated Historical Key Versions (4 versions/key/yr) | $1.20/mo | $1.20/mo | $1.20/mo | $14.40/yr | |
| KMS API Operations ($0.03 per 10,000 operations) | $0.03/mo | $0.30/mo | $3.00/mo | $36.00/yr | |
| Total Estimated KMS Cost | ~$1.53/mo | ~$1.80/mo | ~$4.50/mo | ~$54.00/yr |