Back to blog
Cloud SecurityGCPIdentity & AccessMonitoring & LoggingOperations & Compliance

No Alert for Project Ownership Changes in GCP

Learn why missing alerts for GCP project ownership (IAM) changes are a security risk, and how to detect, fix, and prevent them with log-based metrics.

TL;DR

This check flags GCP projects that lack a log-based metric and alert for IAM policy changes that grant or revoke project ownership. Owner is the most powerful role in a project, so silent changes to it are a prime sign of compromise or insider abuse. Fix it by creating a log-based metric on SetIamPolicy events plus an alerting policy that notifies your team.

Project ownership in Google Cloud is the keys to the kingdom. An account with the roles/owner binding can read every resource, change billing, delete data, modify IAM for everyone else, and disable the very logging you rely on to detect misbehavior. If someone adds themselves as an owner and nobody notices, you have a serious problem and no paper trail pointing at it in real time.

The No Alert for Project Ownership Changes check looks for a missing safety net: a log-based alert that fires whenever the project IAM policy is modified to assign or remove the owner role. Without it, ownership changes sit quietly in Cloud Audit Logs until someone happens to go looking.


What this check detects

Lensix inspects your GCP project for two things working together:

  • A log-based metric that matches IAM policy change events affecting project ownership.
  • An alerting policy in Cloud Monitoring that is wired to that metric and has at least one notification channel attached.

If either piece is missing, the check fails. The relevant events live in the Admin Activity audit log under the SetIamPolicy method, recorded against the Cloud Resource Manager service. These logs are always on and cannot be disabled, which makes them an ideal foundation for detection.

Note: The owner role can be granted to a user, a group, or a service account. A robust alert should catch ownership changes regardless of the principal type, since attackers frequently pivot through service accounts rather than human identities.

This control maps directly to CIS Google Cloud Platform Foundations Benchmark recommendation 2.4, "Ensure log metric filter and alerts exist for Project Ownership assignments/changes." If you are working toward CIS compliance, this is one of the logging metric filters auditors expect to see.


Why it matters

Ownership changes are one of the loudest signals an attacker can produce, but only if someone is listening. Here is what plays out when nobody is.

Privilege escalation that hides in plain sight

An attacker who compromises an account with resourcemanager.projects.setIamPolicy permission can grant ownership to a throwaway identity they control. From there they have full reign. Without an alert, this binding can persist for weeks. By the time it surfaces in a quarterly access review, the damage is done.

Insider risk and offboarding gaps

A departing engineer who still has owner access could grant ownership to a personal account before their corporate identity is deactivated. Ownership changes near the time of an offboarding event are exactly the kind of thing a real-time alert catches and a manual review usually misses.

Loss of containment

Because owners can modify IAM for the whole project, a single rogue owner can grant access to others, creating a fan-out of compromised identities. Catching the first ownership change quickly is the difference between revoking one binding and untangling a dozen.

Danger: An owner can disable Cloud Logging sinks, delete log buckets, and turn off Security Command Center findings. If an attacker reaches owner before you alert on it, they may be able to blind your detection entirely. This is why the alert needs to fire on the ownership change itself, not on downstream activity.


How to fix it

The fix has two stages: create a log-based metric that counts ownership change events, then attach an alerting policy with a notification channel. You can do this in the Console, with gcloud, or with Terraform.

Step 1: Create the log-based metric (gcloud)

This metric counts audit log entries where a SetIamPolicy call adds or removes the owner role.

gcloud logging metrics create project-ownership-changes \
  --project=YOUR_PROJECT_ID \
  --description="Counts IAM changes that add or remove project owners" \
  --log-filter='(protoPayload.serviceName="cloudresourcemanager.googleapis.com")
AND (protoPayload.methodName="SetIamPolicy")
AND (
  protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner"
  AND (
    protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD"
    OR protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE"
  )
)'

Note: The serviceData.policyDelta fields are populated for Cloud Resource Manager IAM changes, which is what gives you the precise owner role match. If you want a broader net, you can drop the bindingDeltas.role clause and alert on all project-level SetIamPolicy calls, then triage in the notification.

Step 2: Create a notification channel

If you do not already have one, create a channel so the alert has somewhere to send. Email is the simplest starting point.

gcloud beta monitoring channels create \
  --project=YOUR_PROJECT_ID \
  --display-name="Security on-call" \
  --type=email \
  [email protected]

Note the channel ID returned, you will reference it in the next step.

Step 3: Create the alerting policy

Write the policy to a file, then apply it. This fires whenever the metric records one or more ownership change events in a rolling window.

{
  "displayName": "Project Ownership Changes",
  "combiner": "OR",
  "conditions": [
    {
      "displayName": "Owner role assignment or removal",
      "conditionThreshold": {
        "filter": "metric.type=\"logging.googleapis.com/user/project-ownership-changes\" AND resource.type=\"global\"",
        "comparison": "COMPARISON_GT",
        "thresholdValue": 0,
        "duration": "0s",
        "aggregations": [
          {
            "alignmentPeriod": "60s",
            "perSeriesAligner": "ALIGN_COUNT"
          }
        ]
      }
    }
  ],
  "notificationChannels": [
    "projects/YOUR_PROJECT_ID/notificationChannels/CHANNEL_ID"
  ],
  "alertStrategy": {
    "autoClose": "1800s"
  }
}
gcloud alpha monitoring policies create \
  --project=YOUR_PROJECT_ID \
  --policy-from-file=ownership-alert-policy.json

Console alternative

If you prefer the UI:

  1. Go to Logging > Logs-based Metrics and click Create Metric.
  2. Choose Counter, paste the log filter from Step 1, and name it project-ownership-changes.
  3. Go to Monitoring > Alerting and click Create Policy.
  4. Select your new log-based metric, set the condition to trigger when the count is above 0, and attach a notification channel.
  5. Save the policy.

Tip: Test the alert before you trust it. Grant and immediately revoke owner on a sandbox service account, then confirm the notification arrives. A silent alert that was never validated is worse than no alert, because it gives a false sense of coverage.


How to prevent it from happening again

Manual Console clicks drift over time. New projects get created without the metric, someone deletes an alert during a noisy incident, and the gap reopens. Bake this into code so every project carries the control by default.

Terraform module

resource "google_logging_metric" "project_ownership_changes" {
  name    = "project-ownership-changes"
  project = var.project_id

  filter = <<-EOT
    protoPayload.serviceName="cloudresourcemanager.googleapis.com"
    AND protoPayload.methodName="SetIamPolicy"
    AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner"
    AND (protoPayload.serviceData.policyDelta.bindingDeltas.action="ADD"
      OR protoPayload.serviceData.policyDelta.bindingDeltas.action="REMOVE")
  EOT

  metric_descriptor {
    metric_kind = "DELTA"
    value_type  = "INT64"
  }
}

resource "google_monitoring_notification_channel" "security_email" {
  display_name = "Security on-call"
  type         = "email"
  project      = var.project_id
  labels = {
    email_address = var.security_email
  }
}

resource "google_monitoring_alert_policy" "ownership_changes" {
  display_name = "Project Ownership Changes"
  project      = var.project_id
  combiner     = "OR"

  conditions {
    display_name = "Owner role assignment or removal"
    condition_threshold {
      filter          = "metric.type=\"logging.googleapis.com/user/${google_logging_metric.project_ownership_changes.name}\" AND resource.type=\"global\""
      comparison      = "COMPARISON_GT"
      threshold_value = 0
      duration        = "0s"
      aggregations {
        alignment_period   = "60s"
        per_series_aligner = "ALIGN_COUNT"
      }
    }
  }

  notification_channels = [google_monitoring_notification_channel.security_email.id]
}

Warning: Log-based metrics and alerting policies are free, but the audit log retention and any sinks routing logs to BigQuery or storage can incur cost. Keep an eye on log volume if you broaden the filter to all SetIamPolicy events across a busy organization.

Apply it at scale with org policy and factories

For organizations with many projects, define this once and roll it out everywhere:

  • Use a project factory (the Terraform Google project factory module, or your own wrapper) so every new project ships with the metric and alert.
  • Centralize audit logs with an aggregated sink at the organization or folder level, then build a single alert against that log bucket rather than one per project.
  • Add a policy-as-code gate in CI. Tools like OPA Conftest or Checkov can scan Terraform plans and fail the build if a project module is missing the ownership alert resources.

Tip: If you run Lensix on a schedule, let it be your backstop. Even with policy-as-code in place, a continuous scan catches drift introduced outside your pipelines, such as someone deleting the alert manually during an outage.


Best practices

  • Alert on the role, not just the API call. Owner is special. Generic IAM change alerts produce noise, so a dedicated owner-focused metric gives you a high-signal trigger you can route straight to on-call.
  • Route to a channel people actually watch. An email to a shared inbox nobody reads is not detection. Send ownership alerts to a paged channel, such as PagerDuty or a monitored Slack channel.
  • Minimize standing owners. The fewer permanent owners you have, the more meaningful every ownership change becomes. Use groups for owner bindings and prefer just-in-time elevation where you can.
  • Pair detection with prevention. An organization policy or IAM Deny policy can block ownership grants to external domains entirely, so the alert becomes a backstop rather than your only line.
  • Review the alert quarterly. Confirm the filter still matches current log schemas and that the notification channel is live. Audit log field names occasionally shift, and a broken filter fails silently.
  • Cover every project. A single unmonitored project is a soft target. Use an org-level aggregated sink so coverage does not depend on remembering to configure each new project.

Ownership changes are rare and consequential, which makes them perfect alert material. Set this up once, validate it, and codify it so it travels with every project you create. The first time it pages you about a change you did not expect, it will have paid for itself.