GCP KMS Cross-Project Encryption


Description

Detects when a GCS service account in one project uses a KMS encryption key from a different project. This could indicate potential ransomware activity where an attacker is using their own KMS key to encrypt data in a victim's project, making it inaccessible without the attacker's key.

Query · python

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if (
        event.deep_get("protoPayload", "serviceName") != "cloudkms.googleapis.com"
        or event.deep_get("protoPayload", "methodName") != "Encrypt"
        or "gs-project-accounts.iam.gserviceaccount.com"
        not in event.deep_get("protoPayload", "authenticationInfo", "principalEmail", default="")
    ):
        return False

    # Get the target project from the log name
    # Format: projects/PROJECT/logs/cloudaudit.googleapis.com%2Fdata_access
    source_project = None
    if event.get("logName").startswith("projects/"):
        parts = event.get("logName").split("/")
        if len(parts) >= 2:
            source_project = parts[1]

    kms_project = None
    if event.deep_get("protoPayload", "resourceName").startswith("projects/"):
        parts = event.deep_get("protoPayload", "resourceName").split("/")
        if len(parts) >= 2:
            kms_project = parts[1]

    if source_project and kms_project is not None and source_project != kms_project:
        return True

    return False


def title(event):
    kms_key = event.deep_get("protoPayload", "resourceName", default="<UNKNOWN_KMS_KEY>")
    principal = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN_PRINCIPAL>"
    )
    return f"Cross-project KMS encryption by [{principal}] using key [{kms_key}] detected"


def alert_context(event):
    context = gcp_alert_context(event)
    context["kms_key"] = event.deep_get("protoPayload", "resourceName", default="<UNKNOWN_KMS_KEY>")
    return context

Analyst notes

  1. Query GCP audit logs for all KMS operations by the principal email in the 24 hours before and after this alert to understand the scope of encryption activity
  2. Check if the cross-project KMS key access is documented in approved service integrations or has been used by this service account in the past 90 days
  3. Find all storage operations (GCS object writes, rewrites) by this service account in the 1 hour window around the alert to identify potentially affected data
Raw source GCP KMS Cross-Project Encryption · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: gcp_kms_cross_project_encryption.py
RuleID: "GCP.KMS.CrossProjectEncryption"
DisplayName: "GCP KMS Cross-Project Encryption"
Enabled: true
LogTypes:
  - GCP.AuditLog
Tags:
  - GCP
  - KMS
  - Encryption
  - Ransomware
Severity: High
Description: >
  Detects when a GCS service account in one project uses a KMS encryption key from a different project.
  This could indicate potential ransomware activity where an attacker is using their own KMS key to encrypt
  data in a victim's project, making it inaccessible without the attacker's key.
Runbook: |
  1. Query GCP audit logs for all KMS operations by the principal email in the 24 hours before and after this alert to understand the scope of encryption activity
  2. Check if the cross-project KMS key access is documented in approved service integrations or has been used by this service account in the past 90 days
  3. Find all storage operations (GCS object writes, rewrites) by this service account in the 1 hour window around the alert to identify potentially affected data
Reference: >
  https://cloud.google.com/kms/docs/encrypt-decrypt
  https://cloud.google.com/storage/docs/encryption/customer-managed-keys
SummaryAttributes:
  - protoPayload:authenticationInfo:principalEmail
  - protoPayload:resourceName
  - resource:labels:project_id
Tests:
  - Name: Cross-Project KMS Encryption Detected
    ExpectedResult: true
    Log:
      {
        "p_log_type": "GCP.AuditLog",
        "insertId": "test-insert-id-001",
        "logName": "projects/victim-project/logs/cloudaudit.googleapis.com%2Fdata_access",
        "protoPayload": {
          "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
          "authenticationInfo": {
            "principalEmail": "service-111111111111-gs-project-accounts.iam.gserviceaccount.com"
          },
          "authorizationInfo": [
            {
              "granted": true,
              "permission": "cloudkms.cryptoKeyVersions.useToEncrypt",
              "permissionType": "DATA_READ",
              "resource": "projects/victim-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key"
            }
          ],
          "methodName": "Encrypt",
          "request": {
            "@type": "type.googleapis.com/google.cloud.kms.v1.EncryptRequest",
            "name": "projects/victim-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key"
          },
          "resourceName": "projects/attacker-project/locations/us/keyRings/malicious-keyring/cryptoKeys/ransomware-key",
          "serviceName": "cloudkms.googleapis.com"
        },
        "resource": {
          "labels": {
            "crypto_key_id": "test-key",
            "key_ring_id": "test-keyring",
            "location": "us",
            "project_id": "victim-project"
          },
          "type": "cloudkms_cryptokey"
        },
        "severity": "INFO",
        "timestamp": "2025-12-02 19:41:27.745108384"
      }
  - Name: Same-Project KMS Encryption (No Alert)
    ExpectedResult: false
    Log:
      {
        "p_log_type": "GCP.AuditLog",
        "insertId": "test-insert-id-002",
        "logName": "projects/legitimate-project/logs/cloudaudit.googleapis.com%2Fdata_access",
        "protoPayload": {
          "at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
          "authenticationInfo": {
            "principalEmail": "service-111111111111-gs-project-accounts.iam.gserviceaccount.com"
          },
          "authorizationInfo": [
            {
              "granted": true,
              "permission": "cloudkms.cryptoKeyVersions.useToEncrypt",
              "permissionType": "DATA_READ",
              "resource": "projects/legitimate-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key"
            }
          ],
          "methodName": "Encrypt",
          "request": {
            "@type": "type.googleapis.com/google.cloud.kms.v1.EncryptRequest",
            "name": "projects/legitimate-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key"
          },
          "resourceName": "projects/legitimate-project/locations/us/keyRings/test-keyring/cryptoKeys/test-key",
          "serviceName": "cloudkms.googleapis.com"
        },
        "resource": {
          "labels": {
            "crypto_key_id": "test-key",
            "project_id": "legitimate-project"
          },
          "type": "cloudkms_cryptokey"
        },
        "severity": "INFO",
        "timestamp": "2025-12-02 19:41:27.745108384"
      }

# ------ paired body: gcp_kms_cross_project_encryption.py ------

from panther_gcp_helpers import gcp_alert_context


def rule(event):
    if (
        event.deep_get("protoPayload", "serviceName") != "cloudkms.googleapis.com"
        or event.deep_get("protoPayload", "methodName") != "Encrypt"
        or "gs-project-accounts.iam.gserviceaccount.com"
        not in event.deep_get("protoPayload", "authenticationInfo", "principalEmail", default="")
    ):
        return False

    # Get the target project from the log name
    # Format: projects/PROJECT/logs/cloudaudit.googleapis.com%2Fdata_access
    source_project = None
    if event.get("logName").startswith("projects/"):
        parts = event.get("logName").split("/")
        if len(parts) >= 2:
            source_project = parts[1]

    kms_project = None
    if event.deep_get("protoPayload", "resourceName").startswith("projects/"):
        parts = event.deep_get("protoPayload", "resourceName").split("/")
        if len(parts) >= 2:
            kms_project = parts[1]

    if source_project and kms_project is not None and source_project != kms_project:
        return True

    return False


def title(event):
    kms_key = event.deep_get("protoPayload", "resourceName", default="<UNKNOWN_KMS_KEY>")
    principal = event.deep_get(
        "protoPayload", "authenticationInfo", "principalEmail", default="<UNKNOWN_PRINCIPAL>"
    )
    return f"Cross-project KMS encryption by [{principal}] using key [{kms_key}] detected"


def alert_context(event):
    context = gcp_alert_context(event)
    context["kms_key"] = event.deep_get("protoPayload", "resourceName", default="<UNKNOWN_KMS_KEY>")
    return context

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.