Back to blog
Best PracticesCloud SecurityGCPIdentity & AccessOperations & Compliance

User Has Admin and KMS CryptoKey Roles: Fixing a Separation of Duties Gap in GCP

Learn why a GCP identity holding both admin and KMS CryptoKey roles breaks separation of duties, the attack risk it creates, and how to remediate and prevent it.

TL;DR

This check flags any GCP identity that holds both an admin-level role and a KMS CryptoKey role. That combination lets one principal manage resources and the keys protecting them, which breaks separation of duties. Split the permissions across different identities so no single account can both encrypt or decrypt data and administer the systems that use it.

Key management is one of the few places in cloud IAM where the principle of least privilege is non-negotiable. The whole point of a Key Management Service is to draw a line between the people who run your infrastructure and the keys that protect your data. When a single user or service account sits on both sides of that line, the line stops meaning anything.

The Lensix check iam_adminandkmsroles looks for exactly this overlap in your GCP projects: an identity that has been granted broad administrative power and the ability to use or manage Cloud KMS CryptoKeys at the same time.


What this check detects

The check inspects IAM policy bindings across your project and surfaces any member (a user, group, or service account) that holds both of the following:

  • An admin-level role, such as roles/owner, roles/editor, a service-specific admin like roles/compute.admin, or roles/iam.securityAdmin.
  • A KMS CryptoKey role, such as roles/cloudkms.admin, roles/cloudkms.cryptoKeyEncrypterDecrypter, roles/cloudkms.cryptoKeyEncrypter, or roles/cloudkms.cryptoKeyDecrypter.

On their own, neither role is a problem. The risk comes from the concentration. An identity with admin rights can change, move, or destroy resources. The same identity holding CryptoKey rights can also unwrap the data those resources depend on. That is enough to read protected data, swap out keys, or quietly disable an audit trail.

Note: Cloud KMS roles come in two flavors. Management roles like roles/cloudkms.admin let a principal create, rotate, and destroy keys. Usage roles like roles/cloudkms.cryptoKeyEncrypterDecrypter let a principal actually encrypt and decrypt data with a key. Either one paired with admin access trips this check, because both undermine separation of duties in different ways.


Why it matters

Separation of duties exists so that compromising one account does not hand an attacker the whole kingdom. KMS is the clearest example. If your data is encrypted with customer-managed encryption keys (CMEK), then the key is the last control standing between an attacker and your plaintext. An admin who also controls that key removes the last control.

Attack scenario: the over-privileged service account

Say you have a CI/CD service account that deploys to Compute Engine. Someone granted it roles/editor to keep things simple, then later added roles/cloudkms.cryptoKeyEncrypterDecrypter so it could decrypt a config file at deploy time. A leaked key for that service account now means an attacker can:

  1. Spin up or modify compute resources anywhere in the project.
  2. Decrypt any data protected by the keys that account can reach.
  3. Exfiltrate plaintext without ever touching the storage layer directly.

Because the same identity is doing both legitimately, the activity blends into normal operations. There is no second account to cross-check against, which makes detection harder and incident response slower.

The insider and the audit problem

With roles/cloudkms.admin plus a project admin role, a single insider can destroy a key version and the resources that referenced it in one sitting. They can also alter logging configuration if their admin role includes that scope. Auditors hate this for a reason: you can no longer prove that no single person had the means to both access and erase sensitive data.

Warning: Compliance frameworks including PCI DSS, SOC 2, and ISO 27001 explicitly call for separation of duties around encryption key management. A finding from this check is the kind of thing that turns into an audit exception if left unresolved.


How to fix it

The fix is to split the overlapping permissions across different identities. Decide which role the flagged identity actually needs, then remove the other and reassign it to a dedicated principal.

Step 1: Find the offending bindings

List the IAM policy and search for the member to confirm both roles are present.

gcloud projects get-iam-policy PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:user:[email protected]" \
  --format="table(bindings.role)"

This returns every role assigned to that member. Look for an admin role and a cloudkms role appearing together.

Step 2: Decide who owns what

Pick one of two clean separations:

  • Keep admin, drop KMS: if this is an infrastructure operator who should not touch encryption, remove the CryptoKey role.
  • Keep KMS, drop admin: if this is a workload that only needs to encrypt and decrypt, strip the broad admin role and grant the narrowest KMS usage role instead.

Danger: Before removing a KMS role from a service account, confirm nothing in production depends on it. If a running workload uses that account to decrypt data, pulling the binding will cause decryption failures and outages. Check Cloud Audit Logs for recent cloudkms.cryptoKeyVersions.useToDecrypt activity first.

Step 3: Remove the overlapping role

To remove the KMS usage role from an admin identity:

gcloud projects remove-iam-policy-binding PROJECT_ID \
  --member="user:[email protected]" \
  --role="roles/cloudkms.cryptoKeyEncrypterDecrypter"

Or remove the broad admin role from a workload identity:

gcloud projects remove-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:deploy@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/editor"

Step 4: Grant the replacement at the right scope

For workloads, bind the KMS role at the key level, not the project level. That limits the blast radius to a single key.

gcloud kms keys add-iam-policy-binding KEY_NAME \
  --keyring=KEYRING_NAME \
  --location=LOCATION \
  --member="serviceAccount:deploy@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/cloudkms.cryptoKeyDecrypter"

If the workload only ever decrypts, grant cryptoKeyDecrypter rather than cryptoKeyEncrypterDecrypter. Match the role to the actual operation.

Tip: Grant KMS roles on the specific CryptoKey or KeyRing instead of the project. A project-level cloudkms.cryptoKeyEncrypterDecrypter binding gives access to every key in the project, which is almost never what you want.


Fixing it in Terraform

If you manage IAM as code, the cleanest pattern is to bind KMS roles at the key resource and keep admin roles separate. Here is a setup that keeps the two responsibilities on different identities:

resource "google_project_iam_member" "infra_admin" {
  project = var.project_id
  role    = "roles/compute.admin"
  member  = "user:[email protected]"
}

resource "google_kms_crypto_key_iam_member" "workload_decrypt" {
  crypto_key_id = google_kms_crypto_key.data_key.id
  role          = "roles/cloudkms.cryptoKeyDecrypter"
  member        = "serviceAccount:deploy@${var.project_id}.iam.gserviceaccount.com"
}

Notice that the admin role and the KMS role go to two different members. Terraform makes this drift visible: if someone adds an overlapping binding by hand, a terraform plan will flag it.


How to prevent it from happening again

Manual cleanup only helps until the next person grants a convenient roles/editor. Push the guardrail upstream.

Policy as code in CI

Add a check to your pipeline that fails when a single member has both an admin role and a KMS role. With Open Policy Agent and Conftest, a rule against your Terraform plan looks like this:

package gcp.iam

admin_roles := {"roles/owner", "roles/editor", "roles/compute.admin"}
kms_roles := {"roles/cloudkms.admin", "roles/cloudkms.cryptoKeyEncrypterDecrypter"}

deny[msg] {
  some member
  member_roles := {r | r := input.bindings[_].members[member]; r != ""}
  count(member_roles & admin_roles) > 0
  count(member_roles & kms_roles) > 0
  msg := sprintf("member %v holds both admin and KMS roles", [member])
}

Organization policy and custom roles

Use IAM custom roles to give people exactly the permissions they need instead of reaching for roles/editor. The broad primitive roles are the most common source of these findings because they quietly include permissions people forget about.

Tip: Lensix runs iam_adminandkmsroles continuously, so a risky binding that slips past CI gets surfaced within a scan cycle rather than waiting for your next audit. Wire the finding into your alerting so it lands in the right channel automatically.


Best practices

  • Separate the key admin from the key user. One identity rotates and manages keys, a different one uses them. Neither should have project admin.
  • Bind KMS roles at the key or keyring level. Project-wide KMS grants defeat the purpose of having per-data keys.
  • Prefer the narrowest role. Use cryptoKeyDecrypter or cryptoKeyEncrypter over the combined role when a workload only does one operation.
  • Avoid primitive roles. roles/owner and roles/editor sweep in permissions you did not intend to grant. Replace them with predefined or custom roles.
  • Audit regularly. Review KMS bindings with Cloud Audit Logs and reconcile them against what each identity actually needs.
  • Keep key destruction behind a second identity. The person who can run your infrastructure should not be able to single-handedly delete the keys protecting it.

Encryption only protects you when the people who manage your systems cannot also reach into the keys at will. Splitting these roles costs a few minutes of IAM cleanup and buys you a real boundary that holds up under both a breach and an audit.