This check flags GCP projects that have no log-based metric alert for Cloud Storage IAM permission changes. Without it, an attacker or misconfigured automation can open a bucket to the public and you find out from a breach report instead of an alert. Fix it by creating a logging metric on SetIamPolicy for GCS and wiring it to a notification channel.
Cloud Storage buckets hold backups, logs, customer uploads, and sometimes things that should never have been put in object storage at all. The permissions on those buckets decide who can read them. When someone changes those permissions, whether through a console click, a Terraform apply, or a compromised service account, you want to know about it quickly. This check confirms that your GCP project has a log-based alert watching for exactly that event.
The catch is that GCP does not alert on storage permission changes by default. Admin Activity audit logs do record them, but a log entry sitting in Cloud Logging is not the same as a page or a Slack message. Someone has to build the bridge between "this event happened" and "a human got notified." This check verifies that bridge exists.
What this check detects
The check looks for a log-based metric and an associated alerting policy in your GCP project that fires when Cloud Storage bucket IAM permissions are modified. Specifically, it watches for audit log entries with a methodName of storage.setIamPermissions (and related SetIamPolicy events on storage resources).
If no metric and policy combination matches that pattern, the check fails. A pass means there is a log-based metric counting these events and at least one alerting policy attached to a working notification channel.
Note: GCP separates "the event was logged" from "someone was told." Admin Activity audit logs are always on and cannot be disabled, so the permission change is recorded regardless. What this check measures is whether you have turned that passive record into an active alert.
Why it matters
Public Cloud Storage buckets are one of the most common and most damaging cloud misconfigurations. The change that makes a bucket public is a single IAM binding: granting allUsers or allAuthenticatedUsers a role like roles/storage.objectViewer. That one line can expose every object in the bucket to the entire internet.
Here is how this plays out in the real world:
- Accidental exposure. An engineer debugging a sharing issue grants
allUsersread access "just to test it," then forgets to revert. The bucket stays open for months. - Compromised credentials. A leaked service account key gets used to grant an attacker-controlled principal access to a sensitive bucket. The attacker now has a quiet, persistent door.
- Insider risk. Someone with legitimate access loosens permissions to exfiltrate data through a path that looks less suspicious than a bulk download.
- Drift from automation. A misconfigured pipeline or a bad Terraform module applies an overly broad binding across many buckets at once.
In every one of these cases, the permission change is logged within seconds. The difference between a non-event and a headline is whether anyone is watching that log. Without an alert, the first signal you get might be a security researcher's email or a customer noticing their data on a public URL. With an alert, your on-call engineer sees the change minutes after it happens and can revert it before anything is scraped.
Warning: Detection is not prevention. An alert tells you a bucket was opened, it does not stop the change. Pair this alert with an organization policy that blocks public access (covered in the prevention section) so you get both a hard guardrail and a notification.
How to fix it
The fix has two parts: create a log-based metric that counts storage IAM changes, then attach an alerting policy with a notification channel. You can do this in the console, with gcloud, or with Terraform.
Option 1: gcloud CLI
First, create the log-based metric. This filter matches IAM policy changes on Cloud Storage resources:
gcloud logging metrics create storage-iam-changes \
--description="Cloud Storage bucket IAM permission changes" \
--project=YOUR_PROJECT_ID \
--log-filter='resource.type="gcs_bucket"
AND protoPayload.methodName="storage.setIamPermissions"'
Next, create a notification channel if you do not already have one. This example uses email:
gcloud beta monitoring channels create \
--display-name="Security On-Call" \
--type=email \
[email protected] \
--project=YOUR_PROJECT_ID
Grab the channel ID from the output, then create the alerting policy. Save this as policy.json:
{
"displayName": "Cloud Storage IAM Permission Changes",
"combiner": "OR",
"conditions": [
{
"displayName": "Storage IAM change detected",
"conditionThreshold": {
"filter": "metric.type=\"logging.googleapis.com/user/storage-iam-changes\" AND resource.type=\"gcs_bucket\"",
"comparison": "COMPARISON_GT",
"thresholdValue": 0,
"duration": "0s",
"aggregations": [
{
"alignmentPeriod": "60s",
"perSeriesAligner": "ALIGN_COUNT"
}
]
}
}
],
"notificationChannels": [
"projects/YOUR_PROJECT_ID/notificationChannels/CHANNEL_ID"
]
}
Apply it:
gcloud alpha monitoring policies create \
--policy-from-file=policy.json \
--project=YOUR_PROJECT_ID
Option 2: Cloud Console
- Go to Logging > Logs-based Metrics and click Create Metric.
- Choose Counter as the metric type and give it a name like
storage-iam-changes. - In the filter box, paste:
resource.type="gcs_bucket" AND protoPayload.methodName="storage.setIamPermissions" - Click Create Metric.
- From the metric, choose Create alert from metric, or go to Monitoring > Alerting > Create Policy.
- Set the condition to fire when the count is above 0, with a rolling window of one minute.
- Attach a notification channel (email, PagerDuty, Slack via webhook, or Pub/Sub) and save.
Option 3: Terraform
resource "google_logging_metric" "storage_iam_changes" {
name = "storage-iam-changes"
project = var.project_id
description = "Cloud Storage bucket IAM permission changes"
filter = "resource.type=\"gcs_bucket\" AND protoPayload.methodName=\"storage.setIamPermissions\""
metric_descriptor {
metric_kind = "DELTA"
value_type = "INT64"
}
}
resource "google_monitoring_notification_channel" "security_oncall" {
display_name = "Security On-Call"
type = "email"
project = var.project_id
labels = {
email_address = "[email protected]"
}
}
resource "google_monitoring_alert_policy" "storage_iam_alert" {
display_name = "Cloud Storage IAM Permission Changes"
project = var.project_id
combiner = "OR"
conditions {
display_name = "Storage IAM change detected"
condition_threshold {
filter = "metric.type=\"logging.googleapis.com/user/${google_logging_metric.storage_iam_changes.name}\" AND resource.type=\"gcs_bucket\""
comparison = "COMPARISON_GT"
threshold_value = 0
duration = "0s"
aggregations {
alignment_period = "60s"
per_series_aligner = "ALIGN_COUNT"
}
}
}
notification_channels = [google_monitoring_notification_channel.security_oncall.id]
}
Tip: Define this metric and policy once in a shared Terraform module and apply it across every project. Permission-change alerting is the kind of control that should be identical everywhere, so codifying it removes the chance that one project gets missed during manual setup.
How to prevent it from happening again
An alert catches the change after it happens. To stop the gap from reopening, and to block dangerous changes outright, layer in a few more controls.
Block public access at the organization level
The single most effective preventive control is the storage.publicAccessPrevention organization policy. It stops anyone from granting allUsers or allAuthenticatedUsers access to a bucket, regardless of their IAM permissions.
gcloud resource-manager org-policies enable-enforce \
storage.publicAccessPrevention \
--organization=YOUR_ORG_ID
Danger: Enforcing this org policy will immediately break any bucket that currently serves public content, such as a static website or a CDN origin. Audit existing public buckets first and add per-project exceptions where genuinely needed before rolling it out org-wide.
Gate IAM changes in CI/CD
If your buckets are managed through Terraform, scan plans before they apply. A policy-as-code tool like Conftest, OPA, or Checkov can reject any plan that introduces a public binding:
# Run Checkov against your Terraform plan in CI
checkov -d . --framework terraform \
--check CKV_GCP_28,CKV_GCP_29
Wire this into your pull request pipeline so a public-access binding fails the build instead of reaching production.
Route alerts somewhere people actually look
An email channel that goes to an unmonitored inbox is barely better than no alert. Send these to a PagerDuty service, a dedicated security Slack channel, or a Pub/Sub topic that feeds your SIEM. Treat a storage permission change like any other security signal that needs triage.
Note: You can also fan these events into an automated response. A Cloud Function subscribed to a Pub/Sub sink can inspect the new binding and automatically revert any grant to allUsers, turning detection into self-healing remediation.
Best practices
- Apply the alert across every project, not just production. Staging and sandbox buckets often hold copies of real data and are watched less closely, which makes them attractive targets.
- Aggregate logs to a central sink. Export Admin Activity logs from all projects to a dedicated logging project so an attacker who compromises one project cannot tamper with the audit trail.
- Alert on more than just public grants. Any change to bucket IAM is worth noticing. A new service account binding can be just as much a sign of compromise as an
allUsersgrant. - Test the alert. Make a benign permission change in a test bucket and confirm the notification actually arrives. An untested alert is a guess, not a control.
- Combine detection with prevention. The strongest posture is a hard guardrail (org policy plus CI gate) backed by an alert that catches anything that slips through.
- Document an expected baseline. Know which buckets are supposed to have which bindings so your on-call can tell a real incident from a routine deployment in seconds.
Storage permission alerting is cheap to set up and pays off the first time a bucket gets opened by accident or by an attacker. Build the metric, wire it to a channel people watch, back it with an org policy, and you turn one of the most common cloud breaches into a non-event.

