This check flags Cloud KMS keys whose IAM policy grants access to allUsers or allAuthenticatedUsers. A public KMS key lets anyone encrypt, decrypt, or sign data with your keys, which can defeat encryption entirely. Remove the public bindings with gcloud kms keys remove-iam-policy-binding and lock access down to specific service accounts.
Cloud KMS is the foundation of encryption in Google Cloud. It protects the keys used to encrypt storage buckets, database backups, secrets, and disk volumes. If one of those keys is exposed to the public internet, the encryption sitting on top of it stops meaning much. This check catches the worst case: a KMS key that anyone in the world, or anyone with a Google account, can use.
What this check detects
The kms_public check inspects the IAM policy attached to each Cloud KMS crypto key (and the key rings above them) and looks for two specific principals in any role binding:
allUsers— anyone on the internet, authenticated or not.allAuthenticatedUsers— anyone with a Google account, including accounts outside your organization.
If either of these appears in a binding for roles like roles/cloudkms.cryptoKeyEncrypterDecrypter, roles/cloudkms.signerVerifier, or roles/cloudkms.admin, the check fails. The presence of these principals on a cryptographic key is almost never intentional, and there is no scenario where exposing a production key to the entire internet is the right call.
Note: Cloud KMS IAM can be set at three levels: the project, the key ring, and the individual key. A public binding at the key ring level cascades down to every key inside it, so a single bad binding can expose dozens of keys at once.
Why it matters
A KMS key does not store your data. It controls who can perform cryptographic operations. That distinction is exactly why a public binding is so dangerous. Depending on the role granted, an attacker who can reach a public key can:
- Decrypt data they already stole. If a service account key, a backup file, or an envelope-encrypted secret was protected with this key, the encryption no longer slows an attacker down. They call
decryptand read it in plaintext. - Forge signatures. If the key has signing roles, an attacker can sign artifacts, tokens, or messages as if they came from your systems.
- Run up your bill and trigger key usage limits. KMS operations are billed per request and rate limited. Public access invites abuse that costs money and can disrupt legitimate workloads.
- Pivot deeper. KMS keys often protect other secrets. One exposed key can unravel a chain of dependent credentials.
Consider a realistic chain of events. A backup process encrypts nightly database dumps with a KMS key and writes them to a Cloud Storage bucket. The bucket is locked down correctly, but the KMS key has an allAuthenticatedUsers binding left over from a quick test months ago. An attacker who obtains a copy of one backup, through a leaked URL or a misconfigured replica, can now decrypt every dump that uses that key. The bucket permissions did their job. The key permissions undid all of it.
Warning: Public KMS bindings are frequently invisible in the console because engineers look at the key list, not the IAM policy of each key. Auditing requires explicitly reading each key's policy, which is why this slips through manual reviews.
How to fix it
The fix is to remove the public principal from the key's IAM policy and replace it with explicit bindings for the service accounts that actually need access.
1. Find the offending binding
First, confirm which level the public binding lives at. Check the crypto key directly:
gcloud kms keys get-iam-policy KEY_NAME \
--keyring=KEY_RING \
--location=LOCATION \
--format=json
Look for any binding whose members array contains allUsers or allAuthenticatedUsers. If you do not see it on the key, check the key ring, since bindings there are inherited:
gcloud kms keyrings get-iam-policy KEY_RING \
--location=LOCATION \
--format=json
2. Remove the public binding
Danger: Before removing access, confirm which service accounts legitimately use this key. If a production workload is relying on the public binding to encrypt or decrypt, removing it without adding a scoped replacement will break that workload.
Remove allUsers from the key for the role that was granted. Repeat for each affected role and for allAuthenticatedUsers if present:
gcloud kms keys remove-iam-policy-binding KEY_NAME \
--keyring=KEY_RING \
--location=LOCATION \
--member="allUsers" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
gcloud kms keys remove-iam-policy-binding KEY_NAME \
--keyring=KEY_RING \
--location=LOCATION \
--member="allAuthenticatedUsers" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
If the binding is at the key ring level, target the key ring instead:
gcloud kms keyrings remove-iam-policy-binding KEY_RING \
--location=LOCATION \
--member="allUsers" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
3. Grant scoped access to the right service accounts
Replace the broad grant with the specific identities that need the key. Use the principle of least privilege and grant only the role required for the operation:
gcloud kms keys add-iam-policy-binding KEY_NAME \
--keyring=KEY_RING \
--location=LOCATION \
--member="serviceAccount:backup-runner@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
4. Verify the policy is clean
gcloud kms keys get-iam-policy KEY_NAME \
--keyring=KEY_RING \
--location=LOCATION \
--format="value(bindings.members)"
The output should list only named service accounts and groups, never allUsers or allAuthenticatedUsers.
Tip: If you manage IAM through Terraform, do not run the gcloud commands as a one-off, since the next terraform apply will reintroduce the public binding. Fix it in code instead (see below) and apply from your pipeline.
How to prevent it from happening again
Manage KMS IAM in Terraform with explicit members
Define key access in code so it is reviewable and reproducible. Use google_kms_crypto_key_iam_member for additive, scoped bindings:
resource "google_kms_crypto_key_iam_member" "backup_decrypter" {
crypto_key_id = google_kms_crypto_key.backups.id
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
member = "serviceAccount:backup-runner@${var.project_id}.iam.gserviceaccount.com"
}
Avoid google_kms_crypto_key_iam_policy unless you are certain you want to own the entire policy, and never put allUsers or allAuthenticatedUsers in any member list.
Block public principals with an Organization Policy
GCP provides a constraint that rejects IAM grants to public principals across the whole org or project. This is the strongest guardrail because it stops the misconfiguration at the API level:
gcloud resource-manager org-policies enable-enforce \
iam.allowedPolicyMemberDomains \
--organization=ORGANIZATION_ID
With domain restriction enabled, attempts to add allUsers or members outside your allowed domains are denied. Set the allowed domains in a list policy so only your organization's customer IDs can be granted access.
Note: iam.allowedPolicyMemberDomains applies to all IAM bindings in scope, not just KMS. It is one of the highest-value organization policies you can enable, since it blocks public exposure across storage, KMS, Pub/Sub, and more in a single move.
Gate it in CI/CD with policy-as-code
Catch the problem before it ever reaches GCP. A Conftest or OPA policy run against your Terraform plan will fail the pipeline if a public member shows up in a KMS binding:
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "google_kms_crypto_key_iam_member"
member := resource.change.after.member
public := {"allUsers", "allAuthenticatedUsers"}
public[member]
msg := sprintf("KMS key binding grants public access: %s", [member])
}
Wire this into your merge checks so no one can introduce a public key binding without a human override.
Best practices
- Default to deny. KMS keys should only ever be reachable by named service accounts tied to specific workloads. If you cannot name the identity that needs a key, it does not need access.
- Separate keys by purpose and environment. Use distinct key rings for prod and non-prod, and per-application keys rather than one shared key. This limits the blast radius if a single binding is wrong.
- Use the narrowest role. A workload that only decrypts should not get
roles/cloudkms.admin. Encrypter, decrypter, and signer roles exist for a reason. - Turn on Cloud KMS audit logs. Data access logs for KMS are not enabled by default. Enable them so every
decryptandsigncall is recorded, which is essential for detecting abuse and for incident response. - Review IAM at all three levels. Project, key ring, and key bindings all compound. Audit each one rather than assuming the key policy tells the whole story.
- Scan continuously. A point-in-time fix does not stop the next engineer from reintroducing the issue. Continuous monitoring catches drift the moment it happens.
Encryption is only as strong as the access controls on the key. A perfectly encrypted dataset behind a public key is functionally unencrypted to anyone who finds it.
Lensix runs the kms_public check across your GCP projects automatically and alerts you the moment a key becomes publicly accessible, so you catch these before they turn into an incident rather than during one.

