IAM deny policies
An IAM deny policy takes permissions away from principals regardless of the roles they hold, Owner included. The GCP Enterprise Baseline uses three: one protects the audit trail from tampering, two keep the auditor groups on configuration and audit logs, away from data.
What it does
Google Cloud evaluates deny policies before allow policies: a denied permission stays denied whatever role grants it. A deny policy is attached to the organization, a folder or a project and applies to everything beneath it. Each deny rule names denied principals, denied permissions and optional exception principals, and can carry a denial condition that matches secure tags on the resource. Only permissions on Google's deny-supported list can be denied.
How BuiltForProd uses it
Audit protection. The organizations unit attaches acme-audit-protection to the organization. Organization policies cannot protect log sinks, so this policy does:
resource "google_iam_deny_policy" "audit_protection" {
parent = urlencode("cloudresourcemanager.googleapis.com/organizations/${var.organization_id}")
name = "${var.namespace}-audit-protection"
display_name = "${var.namespace}: only the platform deployers may delete or alter audit sinks and buckets"
rules {
description = "Protect logging sinks, log buckets and storage buckets in audit-tagged projects"
deny_rule {
denied_principals = ["principalSet://goog/public:all"]
exception_principals = var.deny_exception_principals
denied_permissions = [
"logging.googleapis.com/sinks.delete",
"logging.googleapis.com/sinks.update",
"logging.googleapis.com/buckets.delete",
"logging.googleapis.com/logs.delete",
"storage.googleapis.com/buckets.delete",
]
denial_condition {
title = "audit-tagged"
expression = "resource.matchTag('${var.organization_id}/purpose', 'audit')"
}
}
}
rules {
description = "Protect logging sinks everywhere, including the organization-level sinks"
deny_rule {
denied_principals = ["principalSet://goog/public:all"]
exception_principals = var.deny_exception_principals
denied_permissions = [
"logging.googleapis.com/sinks.delete",
"logging.googleapis.com/sinks.update",
]
}
}
depends_on = [google_tags_tag_value.audit]
}
- Rule 1 applies to resources tagged
purpose=audit, which isacme-core-audit: nobody may delete or update a sink, delete a log bucket or a log, or delete a storage bucket there. The central log bucket and the archive bucket of Cloud Logging live in that project. - Rule 2 has no condition, because a deny condition can only match tags and the organization itself cannot be tagged: updating or deleting any sink, the organization-level sinks included, is denied everywhere.
The exception principals are the only identities that may still change these resources: the Baseline repository's CI service account sa-acme-baseline-ci, which runs the organization-scoped logging unit; the deployers of acme-core-root and acme-core-audit; and the acme-platform-leads@company.com group. Every other principal, project Owners included, is refused.
The deny policy is one layer. The sinks, log buckets and the audit key carry prevent_destroy, so a plan that would delete them fails, and two switches in security.hcl lock the retention of the central log bucket and the archive bucket for 365 days (lock_log_bucket_retention, lock_archive_bucket_retention, both off by default and irreversible once on).
Auditors see configuration, never data. The groups-iam unit attaches two more policies to both auditor groups:
| Policy | Attached to | Denied permissions |
|---|---|---|
acme-auditor-no-data | the organization | Cloud Storage object reads and lists, Secret Manager version access, BigQuery table data and export, Spanner reads, Firestore entity reads |
acme-auditor-no-workload-logs | the plat folder | log entry, private log entry and log view reads |
The auditors read the audit trail of every project, the stage projects included, in the central log bucket of acme-core-audit, where the organization sinks bring it. The stage projects' own log buckets hold application logs, which are data; the second policy takes back the Logs Viewer permissions that roles/viewer carries there.
Terms you will see
| Term | Meaning |
|---|---|
| Deny rule | Denied principals, denied permissions, exceptions and an optional condition. |
| Exception principal | An identity the deny rule does not apply to. |
| Denial condition | A tag match such as resource.matchTag('<org>/purpose', 'audit'). |
purpose=audit | The secure tag bound to acme-core-audit. |
| Retention lock | An irreversible lock that stops a bucket's retention from being shortened. |
Where to read more
- GCP Enterprise Baseline overview for the audit trail and its protection.
- Organization Policy Service for the guardrails a deny policy complements.
- Defense in depth for layering preventive controls.