Project Ownership Change Log Alerts Should Be Enabled
More Info:
Ensures that logging and log alerts exist for project ownership assignments and changes. Project Ownership is the highest level of privilege on a project, any changes in project ownership should be heavily monitored to prevent unauthorized changes.
Risk Level
High
Address
Security
Compliance Standards
- CIS GCP
- CIS GCP 2.0.0
- Cloudanix Best Practice
- HIPAA
- HITRUST CSF
- ISO 27001
- Securities and Exchange Board of India (SEBI) - Cloud Security Adoption Framework
Triage and Remediation
- Remediation
Remediation
Using Console
To remediate “Project Ownership Logging” in GCP using the Console, you want to ensure that all changes to IAM (especially project owners) are logged and retained outside the project (per CIS 1.1).
Below are concise step‑by‑step instructions.
1. Confirm Audit Logs Are Enabled for IAM
Admin Activity logs (which include IAM changes) are on by default and cannot be turned off, but verify their presence:
- Go to Logging:
Navigation menu → Logging → Logs Explorer - In the query editor, run something like:
resource.type="project"logName:"cloudaudit.googleapis.com/activity"protoPayload.methodName:"SetIamPolicy"
- Click Run query.
- If you see recent entries, IAM changes (including ownership changes) are being logged.
2. Create a Central Log Bucket / Project (recommended)
If you already have a central logging project/bucket, skip to section 3.
- (Optional but recommended) Create a central logging project (e.g.,
org-logging-prod) and do the rest of these steps there. - In that project, go to
Navigation menu → Logging → Log storage - Click Create Log Bucket:
- Name:
project-ownership-audit-logs - Location: choose region or multi-region as per policy
- Retention: configure per policy (e.g., 365 days or longer)
- Name:
- Click Create.
3. Create a Log Sink for Project Ownership / IAM Changes
Do this in each project where you want to remediate the finding (or at folder/org level if you have the rights).
-
Switch to the source project you want to protect.
-
Go to
Navigation menu → Logging → Log Router -
Click Create sink.
-
Configure:
- Sink name:
project-ownership-logging-sink - Sink description:
Exports IAM and project ownership change logs
- Sink name:
-
Sink destination:
- Choose where to send logs:
- Log bucket: select the central log bucket you created (may be in another project).
- Click Select sink destination → Cloud Logging bucket → choose project and bucket (e.g.,
org-logging-prod / project-ownership-audit-logs).
- Click Select sink destination → Cloud Logging bucket → choose project and bucket (e.g.,
- Alternatively, you can choose BigQuery dataset or Cloud Storage if your policy requires that.
- Log bucket: select the central log bucket you created (may be in another project).
- Choose where to send logs:
-
Choose logs to include (this is key):
In the Build inclusion filter box, use a filter that captures IAM changes, especially role/owner changes, for this project. Example:
resource.type="project"logName=("projects/YOUR_PROJECT_ID/logs/cloudaudit.googleapis.com%2Factivity")protoPayload.serviceName="cloudresourcemanager.googleapis.com"protoPayload.methodName=("SetIamPolicy" OR"projects.setIamPolicy" OR"resourcemanager.projects.setIamPolicy")Replace
YOUR_PROJECT_IDwith your project ID.This ensures all project-level IAM policy changes—including ownership/role assignment changes—are exported.
-
Click Create sink.
-
When prompted to Grant Writer Identity:
- The system shows a service account of the form
cloud-logs@system.gserviceaccount.comorserviceAccount:logging-XXXX@gcp-sa-logging.iam.gserviceaccount.com(depending on target). - Grant that service account the appropriate writer role on the destination:
- If destination is a log bucket:
- On the destination project, go to
IAM & Admin → IAM - Add the sink’s service account with role:
Logging → Logs Bucket Writer(orLogs Writerif using legacy).
- On the destination project, go to
- If BigQuery: grant
BigQuery Data Editoron the dataset. - If Cloud Storage: grant
Storage Object Creatoron the bucket.
- If destination is a log bucket:
- The system shows a service account of the form
4. (Optional) Narrow to Owner‑Level Role Changes Only
If your auditor requires only changes involving owner‑equivalent roles, you can further refine the sink filter:
resource.type="project"
logName=("projects/YOUR_PROJECT_ID/logs/cloudaudit.googleapis.com%2Factivity")
protoPayload.serviceName="cloudresourcemanager.googleapis.com"
protoPayload.methodName:("SetIamPolicy" OR "projects.setIamPolicy" OR "resourcemanager.projects.setIamPolicy")
protoPayload.serviceData.policyDelta.bindingDeltas.member:*
protoPayload.serviceData.policyDelta.bindingDeltas.role:("roles/owner" OR "roles/resourcemanager.projectIamAdmin" OR "roles/resourcemanager.organizationAdmin")
Adjust roles per your environment.
5. Verify Logs Are Reaching the Destination
-
Go to the destination (log bucket / BigQuery / GCS):
- For Log bucket:
Navigation menu → Logging → Logs Explorer- In the project that owns the bucket, filter by:
logName:"cloudaudit.googleapis.com/activity"protoPayload.methodName:"SetIamPolicy"
- For BigQuery:
- Query the table to confirm new rows appear when IAM changes are made.
- For GCS:
- Check objects in the bucket are being created.
- For Log bucket:
-
Make a test IAM change (e.g., add/remove a role) and confirm it appears in the destination within a few minutes.
Once this is in place for all relevant projects (or configured at folder/org level), the “Project Ownership Logging” / “Project ownership logging and monitoring” finding in most security or CIS benchmarks will be considered remediated for GCP IAM.
Using CLI
To remediate “Project Ownership Logging” for GCP IAM using the CLI, you need to enable Cloud Audit Logs (Admin Activity + Data Access) in the project IAM policy.
Below are step‑by‑step gcloud commands.
1. Set your project
PROJECT_ID="your-project-id"
gcloud config set project "${PROJECT_ID}"
2. Export the current IAM policy
gcloud projects get-iam-policy "${PROJECT_ID}" \
--format=json > iam-policy.json
3. Edit IAM policy to add audit logging
Open iam-policy.json in an editor and add (or merge) the auditConfigs block.
If auditConfigs doesn’t exist, add it at the top level:
{
"bindings": [
// ... existing bindings ...
],
"auditConfigs": [
{
"service": "allServices",
"auditLogConfigs": [
{
"logType": "ADMIN_READ"
},
{
"logType": "DATA_READ"
},
{
"logType": "DATA_WRITE"
}
]
}
]
}
If auditConfigs already exists, ensure there is an entry with "service": "allServices" (or "iam.googleapis.com") including the three logType values above.
This configuration ensures IAM (including project owner / role changes) is logged.
4. Apply the updated IAM policy
gcloud projects set-iam-policy "${PROJECT_ID}" iam-policy.json
Verify there are no errors.
5. (Optional) Verify audit configs
gcloud projects get-iam-policy "${PROJECT_ID}" --format=json \
| jq '.auditConfigs'
You should see ADMIN_READ, DATA_READ, and DATA_WRITE under allServices (or specifically iam.googleapis.com).
This enables logging of project ownership and other IAM changes for the project.
Using Python
Below are step‑by‑step instructions and a Python example to remediate “Project Ownership Logging” for GCP IAM (as in CIS GCP Benchmark: log all project owner changes).
Goal
Ensure that any changes to project ownership / IAM policy are logged and exported (e.g., to a log bucket, BigQuery, or Pub/Sub) so they can’t be lost.
This is done by:
- Creating (or verifying) a log sink at the project level.
- Using a filter that matches IAM / ownership changes.
- Ensuring the sink’s destination exists and the sink has permission to write to it.
1. Decide where to export the logs
Common options:
- A log bucket in the same project
- A BigQuery dataset
- A Pub/Sub topic
Example destination (BigQuery):
bigquery.googleapis.com/projects/LOGGING_PROJECT_ID/datasets/audit_logs
Or Pub/Sub:
pubsub.googleapis.com/projects/LOGGING_PROJECT_ID/topics/audit-iam-changes
Or log bucket (same project):
logging.googleapis.com/projects/PROJECT_ID/locations/global/buckets/audit-logs
Create the destination resource beforehand (dataset / topic / log bucket).
2. Use a logging filter that captures IAM / ownership changes
Recommended filter (covers IAM policy changes for the project):
logName:"cloudaudit.googleapis.com/activity"
protoPayload.serviceName="cloudresourcemanager.googleapis.com"
protoPayload.methodName:("SetIamPolicy" OR "SetOrgPolicy" OR "SetIamPolicy" OR "SetBinding" OR "DeleteBinding")
If you specifically care about project “Owner” role changes, you can refine to role roles/owner:
logName="projects/PROJECT_ID/logs/cloudaudit.googleapis.com%2Factivity"
protoPayload.serviceName="cloudresourcemanager.googleapis.com"
protoPayload.methodName="SetIamPolicy"
protoPayload.response.bindings.role="roles/owner"
Replace PROJECT_ID accordingly if you hard‑code it.
3. Python example: create or update a log sink
Prerequisites
pip install google-cloud-logging
gcloud auth application-default login
Code
from google.cloud import logging_v2
from google.api_core.exceptions import AlreadyExists
def ensure_project_ownership_logging_sink(
project_id: str,
sink_name: str,
destination: str,
):
"""
Create or update a log sink for project ownership/IAM changes.
:param project_id: ID of the project whose logs you want to export.
:param sink_name: Name of the sink (e.g., "project-ownership-logging").
:param destination: Sink destination URI (BigQuery, Pub/Sub, or log bucket).
"""
client = logging_v2.ConfigServiceV2Client()
parent = f"projects/{project_id}"
# Filter for ownership / IAM changes (includes project IAM policy changes)
filter_ = (
'logName="projects/{project_id}/logs/cloudaudit.googleapis.com%2Factivity" '
'protoPayload.serviceName="cloudresourcemanager.googleapis.com" '
'protoPayload.methodName="SetIamPolicy" '
'protoPayload.response.bindings.role="roles/owner"'
).format(project_id=project_id)
sink = logging_v2.LogSink(
name=sink_name,
destination=destination,
filter=filter_,
include_children=False, # set True only for org/folder-level sinks
)
sink_path = client.log_sink_path(project_id, sink_name)
try:
# Try to create the sink
response = client.create_sink(
parent=parent,
sink=sink,
unique_writer_identity=True # create a service account for the sink
)
print(f"Created sink: {response.name}")
print(f"Writer identity: {response.writer_identity}")
except AlreadyExists:
# If sink exists, update it
update_mask = {"paths": ["filter", "destination"]}
response = client.update_sink(
sink_name=sink_path,
sink=sink,
update_mask=update_mask,
)
print(f"Updated sink: {response.name}")
print(f"Writer identity: {response.writer_identity}")
# IMPORTANT: Grant the sink's writer_identity permission to write to destination
# This must be done separately depending on the destination type.
if __name__ == "__main__":
# Example usage
PROJECT_ID = "your-project-id"
SINK_NAME = "project-ownership-logging"
# Example: BigQuery dataset destination
DESTINATION = "bigquery.googleapis.com/projects/your-logging-project/datasets/audit_logs"
# Or Pub/Sub: "pubsub.googleapis.com/projects/your-logging-project/topics/audit-iam-changes"
# Or Log bucket: "logging.googleapis.com/projects/your-logging-project/locations/global/buckets/audit-logs"
ensure_project_ownership_logging_sink(PROJECT_ID, SINK_NAME, DESTINATION)
4. Grant sink writer permissions on the destination
After running the script, note the writer_identity printed (something like serviceAccount:cloud-logs@system.gserviceaccount.com or serviceAccount:...gcp-sa-logging.iam.gserviceaccount.com).
Grant it appropriate IAM on the destination:
- BigQuery dataset:
roles/bigquery.dataEditororroles/bigquery.dataOwner - Pub/Sub topic:
roles/pubsub.publisher - Log bucket:
roles/logging.bucketWriteron that bucket
Example (BigQuery) via gcloud:
bq update --dataset \
--access=role:WRITER,group:log-writers@example.com \
your-logging-project:audit_logs
(or use IAM policy binding to the dataset’s service account writer_identity).
If you tell me:
- your chosen destination type (BigQuery / Pub/Sub / log bucket), and
- whether you want only owner role changes or all IAM changes
I can adjust the filter and Python snippet precisely for that setup.
Using Terraform
# Logs-based metric for project ownership changes
resource "google_logging_metric" "project_ownership_change" {
project = "PROJECT_ID" # replace with your GCP project ID
name = "project_ownership_change_count"
description = "Counts changes to project ownership (role roles/owner) via SetIamPolicy on the project."
filter = <<-EOT
resource.type="project"
AND protoPayload.serviceName="cloudresourcemanager.googleapis.com"
AND protoPayload.methodName="SetIamPolicy"
AND protoPayload.authorizationInfo.permission="resourcemanager.projects.setIamPolicy"
AND protoPayload.serviceData.policyDelta.bindingDeltas.role="roles/owner"
EOT
metric_descriptor {
metric_kind = "DELTA"
value_type = "INT64"
}
}
# Notification channel for alerts (email example)
resource "google_monitoring_notification_channel" "project_ownership_email" {
project = "PROJECT_ID" # replace with your GCP project ID
display_name = "Project Ownership Change Email"
type = "email"
labels = {
email_address = "SECURITY_ALERTS_EMAIL@example.com" # replace with your alert email
}
}
# Alert policy using the logs-based metric
resource "google_monitoring_alert_policy" "project_ownership_change_alert" {
project = "PROJECT_ID" # replace with your GCP project ID
display_name = "Project Ownership Change Alert"
combiner = "OR"
conditions {
display_name = "Project ownership changes > 0 in last minute"
condition_threshold {
# Logs-based metrics surface as custom logging metrics under logging.googleapis.com/user/*
filter = "metric.type = \"logging.googleapis.com/user/${google_logging_metric.project_ownership_change.name}\" AND resource.type = \"project\""
comparison = "COMPARISON_GT"
threshold_value = 0
duration = "0s"
aggregations {
alignment_period = "60s"
per_series_aligner = "ALIGN_DELTA"
cross_series_reducer = "REDUCE_SUM"
}
}
}
notification_channels = [
google_monitoring_notification_channel.project_ownership_email.name
]
documentation {
content = "Project ownership (roles/owner) was modified on this project. Review immediately."
mime_type = "text/markdown"
}
}
This change is additive and does not force replacement of existing resources; it creates a new logs-based metric and alert policy.
For verification, terraform plan should show + create for:
google_logging_metric.project_ownership_changegoogle_monitoring_notification_channel.project_ownership_emailgoogle_monitoring_alert_policy.project_ownership_change_alert.