CoursesGCP securityIAM, conditions & deny policies

IAM, conditions & deny policies

Additive allows, conditions, and enforceable denies.

Advanced35 min · lesson 2 of 15

Someone on the platform team asks for roles/editor on a folder so a launch can ship on Friday. It ships. The grant stays. Six months later that person can still touch every project under that folder, including the one holding your audit logs. Most cloud access trouble looks like this. Nobody picked a lock. Old yeses piled up, and no single screen in the console shows you the total. This lesson walks through the three things Google Cloud gives you to pull that access back in: smaller role grants, conditions that fence a grant in, and deny policies that say no in a way no yes can beat.

Start with the mental model. IAM (Identity and Access Management) is the service that decides who is allowed to do what. A single grant behaves like a keycard you hand someone: the door reads the card, the door opens. In Google Cloud (often shortened to GCP, for Google Cloud Platform) that card is called a binding, and it ties three things together. The principal is who holds the card, meaning a person, a Google Group, or a service account (a robot login that software uses instead of a human). The role is a bundle of permissions, say read objects in Cloud Storage. The resource is the door the card opens: a project, a bucket, a whole folder. One habit pays for itself forever. Grant roles to groups, never to individuals. Then joiners and leavers are handled by whoever runs your identity provider, and you never open IAM for it again.

Leave the basic roles alone. Owner, Editor and Viewer date from a smaller, simpler Google Cloud, and Editor on its own can change almost everything in a project. Reach for a predefined role instead, like roles/storage.objectViewer. Google maintains those and keeps them current as APIs (the interfaces software calls to get work done) grow new permissions. Write a custom role only when nothing predefined fits, and go in knowing you now own it forever, every time the API changes underneath you.

Read the policy before you change it

Before you grant anything, find out what is already granted. IAM allow policies are additive: access accumulates, and nothing adds it up for you. get-iam-policy prints the current bindings on a resource so you can see who already holds a card. Look at the etag line in the output. That is a version stamp on the policy document. If two people edit the same policy at once, the second save gets rejected instead of quietly wiping out the first.

read the current project policy
gcloud projects get-iam-policy payments-prod --format=yaml
bindings:
- members:
role: roles/editor
- members:
role: roles/owner
- members:
role: roles/storage.objectViewer
etag: BwYf3kQvD2c=
version: 1

There it is in black and white. The analytics group can read every object in the project when all it needs is one reports bucket, and platform-admins hold Editor. Neither grant is scoped to anything. Tighten the analytics one first with a condition. Then build a guardrail that stops even an Owner from deleting the audit logs.

A keycard that only works nine to five

Office keycards are often time-boxed. Yours opens the side door on weekdays between nine and five and is a dead lump of plastic at midnight. IAM conditions do that to a grant. A condition is a small yes/no test written in CEL (Common Expression Language, a tiny expression language Google uses for exactly this kind of check). Attach it to a binding and the grant counts only while the test passes. Two attributes carry most of the weight: resource.name, so the grant applies only to resources whose name matches a pattern, and request.time, so it applies only inside a window. The command below gives analytics read access on buckets whose name starts with reports-, and only during business hours.

a conditional binding: reports buckets, business hours only
gcloud projects add-iam-policy-binding payments-prod \
--member="group:[email protected]" \
--role="roles/storage.objectViewer" \
--condition='title=reports-hours,description=reports bucket during business hours,expression=resource.name.startsWith("projects/_/buckets/reports-") && request.time.getHours("UTC") >= 9 && request.time.getHours("UTC") < 17'
Updated IAM policy for project [payments-prod].
bindings:
- condition:
description: reports bucket during business hours
expression: resource.name.startsWith("projects/_/buckets/reports-") && request.time.getHours("UTC")
>= 9 && request.time.getHours("UTC") < 17
title: reports-hours
members:
role: roles/storage.objectViewer
etag: BwYf3kZ1p9A=
version: 3

Two things to notice. The policy moved to version 3, because conditions exist only in version 3 policies and gcloud bumped it for you. Second, the one that catches people over and over: a condition restricts the binding it is attached to and nothing else. If analytics also picks up an unconditional role somewhere higher up the tree, that looser grant still stands, and your business-hours fence buys you nothing. Which makes how grants combine the thing to understand next.

Yeses add up, which is the whole problem

Google Cloud resources sit in a tree. The organization at the top, then folders, then projects, then the buckets and machines inside them. A grant made high up flows downhill to everything beneath it. Your real access at any moment is the union of every binding from the org down to the thing you are touching. Allow policies have no way to say stop here. They only add. One forgotten Editor on a folder therefore reaches every project below it, and no amount of careful, tightly scoped work at the project level takes that back. The blast radius comes from the level you forgot to look at, not the one on your screen.

Deny policies are the missing no. Every building has a sign bolted to the fire exit: this door is never propped open, not for the CEO, not for the cleaners, not for anyone. The number of keycards in your pocket makes no difference. Google Cloud checks deny policies first, before it reads a single allow. If a deny matches your identity and the action you asked for, the request stops right there. No grant outranks it.

How GCP resolves one request
Principal calls an API
e.g. delete the audit-logs bucket
a deny rule matches
Blocked immediately
evaluation stops here; no allow can override it
no deny, allow binding + condition true
Allowed
and only for that one permission
no deny, no matching allow
Denied by default
IAM grants nothing unless a binding says so

Writing a rule that beats every grant

A deny policy is a separate document you attach to a resource. Inside it, a deny rule names three things. deniedPrincipals is who the rule hits, and principalSet://goog/public:all means everybody. exceptionPrincipals is who still gets through, which is where your break-glass group belongs: the small, high-trust group you touch only in a real emergency. deniedPermissions lists the actions to block, written as the service plus the permission, like logging.googleapis.com/buckets.delete. Add a denialCondition and the rule bites only on the resources you name. The policy below stops everyone except break-glass from deleting the audit log bucket or the log sinks that feed it (sinks are the rules that route logs into storage), whatever roles they happen to hold.

deny-audit-logs.yaml
rules:
- denyRule:
deniedPrincipals:
- principalSet://goog/public:all
exceptionPrincipals:
- principalSet://goog/group/[email protected]
deniedPermissions:
- logging.googleapis.com/buckets.delete
- logging.googleapis.com/sinks.delete
denialCondition:
title: only-the-audit-log-bucket
expression: resource.name.endsWith("locations/global/buckets/audit-logs")
displayName: Protect the audit log pipeline from deletion
attach the deny policy to the project
gcloud iam policies create deny-audit-log-deletion \
--attachment-point="cloudresourcemanager.googleapis.com/projects/payments-prod" \
--kind=denypolicies \
--policy-file=deny-audit-logs.yaml
Waiting for operation [cloudresourcemanager.googleapis.com/projects/payments-prod/denypolicies/deny-audit-log-deletion@operations/a1b9...c3] to complete...done.
Created policy [deny-audit-log-deletion].
a project Owner tries to delete the bucket anyway
gcloud logging buckets delete audit-logs \
--location=global --project=payments-prod
ERROR: (gcloud.logging.buckets.delete) PERMISSION_DENIED: Permission 'logging.buckets.delete' denied on resource 'projects/payments-prod/locations/global/buckets/audit-logs'. Access was denied by IAM deny policy 'deny-audit-log-deletion'.

The account running that delete holds roles/owner, the strongest allow the project has, and it still bounces. That is what deny is for. It changes the answer in a way no allow can argue with. Deny policies attach at an organization, a folder or a project, and they flow downhill the same way allows do, so one deny at the org protects every project you will ever create under it. Running them costs nothing. The price is discipline: keep the break-glass exception tiny, require a second login factor beyond the password (multi-factor authentication) for everyone in it, and raise an alert every single time it is used.

A fresh deny is not a kill switch
IAM changes, allows and denies alike, take a few minutes to spread across Google's systems. So you will write a deny, run the blocked command straight away to prove it works, and watch the command succeed. Nothing is broken. The rule has not landed yet. Wait a few minutes and retry. The other side of that delay matters more when you are under attack: do not reach for a deny policy as a real-time way to lock out a compromised account, because whoever holds those credentials may still have a working window. Revoke the active sessions and disable the identity directly instead.

None of this survives without a cleanup habit. Put a date on every grant that exists to unblock something, and treat a grant nobody can explain as an expired one. A quarterly pass over the org and folder levels turns up more real risk than a month of tightening project bindings, because the wide grants live up there and nobody looks. Two questions per binding are enough. Who asked for this, and what breaks if it goes away? If the answer to either one is a shrug, remove it and see who shouts.

Conditions do not replace least privilege. They shrink a grant you cannot avoid making, down to weekdays only, or resources tagged env=dev, or calls coming from one VPC (virtual private cloud, your own private network inside Google Cloud). Deny policies sit above all of it. Deny service account key creation at the folder, the power that roles/iam.serviceAccountKeyAdmin carries, and no allow binding anywhere beneath that folder can mint a key, Owners included. That is a guardrail you can point at in an audit and move on.

Try this

Take one production project. Dump its IAM policy, then ask Policy Analyzer who is able to change that policy. What you want back is a short, boring list with no Owner you did not expect.

terminal
gcloud projects get-iam-policy payments-prod --format=json > policy.json
gcloud policy-intelligence query-activity \
--activity-type=permission-usage \
--project=payments-prod \
--permission=resourcemanager.projects.setIamPolicy 2>/dev/null || true
gcloud asset search-all-iam-policies \
--scope=projects/payments-prod \
--query="policy:roles/owner" \
--format="table(resource,policy.bindings.members)"
output
bindings:
- members:
role: roles/owner
- members:
- serviceAccount:[email protected]
role: roles/editor
etag: BwYh3n2K9pE=
version: 1
# asset search (trimmed)
RESOURCE MEMBERS
//cloudresourcemanager.googleapis.com/projects/802451296328

Takeaway

Every grant is a keycard, and keycards stack. Hand them to groups, fence the ones you cannot avoid with a condition on time or resource name, and write a deny policy when a yes must never be allowed to win.

The next lesson moves from the people holding cards to the machines that hold them: service accounts, and the JSON key files that leak them.

Quick check
01You wrote the conditional Storage viewer binding for analytics on the project: reports- buckets, nine to five only. A folder above that project also grants the same group roles/editor, with no condition on it. It is 2am. Can someone in analytics read the reports bucket?
Incorrect — The condition governs its own binding and nothing else. It cannot reach across and tighten a separate, unconditional grant.
Correct — Allows are a union. The folder's Editor covers storage reads, carries no condition and flows downhill, so it opens the door by itself.
Incorrect — No such rule exists for allows. Effective access is the union of every binding, not the overlap between them.
Incorrect — Deny policies block actions and carve out exceptions. They never grant access, and nothing needs granting here.
02The lesson tells you to reach for a predefined role such as roles/storage.objectViewer rather than one of the three basic roles (Owner, Editor, Viewer). What is the main security problem with handing someone roles/editor?
Incorrect — A condition pushes the whole policy to version 3 whatever roles it holds, and Editor sits there quite happily alongside it.
Incorrect — You can attach Editor at any level. The danger is how much the role covers, not where you hang it.
Correct — The basic roles are blunt. A predefined role gives away only the slice of access the work actually calls for.
Incorrect — Basic roles still work. They are discouraged for being coarse, not scheduled for removal.
03A contractor's credentials are being abused right now, this minute. A teammate wants to write an IAM deny policy on the spot to shut the attacker out instantly. Why is that the wrong first move, and what do you do instead?
Incorrect — Deny rules can name principals and even carve out exceptions, and demoting someone to Viewer does not take away the access they already hold.
Incorrect — exceptionPrincipals keeps break-glass working, so that is not the reason to hold off here.
Incorrect — Deny policies block permissions across many services, not Storage alone.
Correct — A deny policy is a guardrail you set in advance, not a kill switch. Killing sessions and disabling the account take effect immediately.

Every card in this lesson was held by a person or a group. The harder half of Google Cloud identity is the service account, the robot login your software runs as, along with impersonation, where one identity is allowed to become another for a while and borrow its permissions. Conditions and deny policies are what keep that borrowing from turning into a quiet path to everything. That is where you go next.

Related