On-premises, security had a shape you could point at: a building, a firewall, a network edge. Inside was trusted, outside was not. Cloud dissolved that shape. Your storage bucket has a public DNS name. Your database answers on the internet. Your container in Frankfurt talks to an API in Oregon over somebody else's fibre. There is no edge left to defend.
What replaced it is identity. Every single request — a person clicking a console button, a training job reading model weights, one microservice calling another — is now resolved by asking the same three questions, in the same order, on all three clouds:
The triangle, in three dialects the one diagram to keep
This is the whole stage in one picture. Click any element for the translation.
Same concept, three names the identity Rosetta Stone
C1 gave you the hierarchy translation. This is the identity one. Policies attach at the hierarchy levels you already know — that is the join between the two stages.
| Vendor-neutral concept | AWS | Azure | GCP |
|---|---|---|---|
| Human identity store | IAM Identity Center (or an external IdP) | Microsoft Entra ID | Cloud Identity / Google Workspace |
| Long-lived human identity | IAM user discouraged | Entra user | Google Account |
| Assumable identity | IAM role | no true equivalent — use PIM eligibility | service account impersonation |
| Workload identity (VM) | instance profile → IAM role | managed identity (system / user-assigned) | attached service account |
| Workload identity (K8s) | EKS Pod Identity preferred / IRSA | Entra Workload ID for AKS | Workload Identity Federation for GKE |
| Grant of permission | identity-based policy (JSON) | role assignment (principal + role def + scope) | binding in the allow policy |
| Permission attached to the thing | resource-based policy (bucket / key policy) | — mostly absent; RBAC scope does this job | allow policy on the resource node |
| Explicit refusal | "Effect":"Deny" in any policy | deny assignment | deny policy newer |
| Ceiling on a principal | permissions boundary | — approximated by scope + deny assignment | principal access boundary (PAB) newest |
| Org-wide guardrail on principals | service control policy (SCP) | Azure Policy at management-group scope | organization policy constraint |
| Org-wide guardrail on resources | resource control policy (RCP) Nov 2024 | Azure Policy deny effect | org policy + deny policy |
| Attribute-based access | condition keys on tags (ABAC) | ABAC conditions on role assignments | IAM Conditions (CEL) on bindings |
| Short-lived credential broker | AWS STS | Entra token endpoint / IMDS | Security Token Service + metadata server |
| Secret store | Secrets Manager / SSM Parameter Store | Key Vault | Secret Manager |
| Just-in-time elevation | role session + session policy | Privileged Identity Management (PIM) | Privileged Access Manager (PAM) |
| Posture / findings hub | Security Hub re-launched 2025 | Microsoft Defender for Cloud | Security Command Center |
| Threat detection | Amazon GuardDuty | Defender for Cloud plans | Event Threat Detection (SCC Premium) |
| Least-privilege analyser | IAM Access Analyzer | Entra Permissions Management | IAM Recommender / Policy Analyzer |
| Control-plane audit log | AWS CloudTrail | Azure Activity Log + Entra sign-in logs | Cloud Audit Logs (Admin Activity) |
| AI-agent identity | IAM role per agent (no distinct type) | Entra Agent ID GA 2026 | agent identities + pools 2026 |
Two rows to notice now, because they shape everything later. Azure has no IAM role in the AWS sense — nothing you "assume"; you are your Entra identity and permissions arrive by scope. And Azure has almost no resource-based policies — the job AWS gives bucket policies, Azure gives to RBAC scope. Those two absences explain most of the friction people hit moving between the two.
Three habits this stage installs
1 · Identity is the perimeter
Stop asking "is this inside the network?". Ask "which principal, proving what, under which policy, against which resource?". The network still matters — it is a layer, not the boundary — and it gets its own stage.
2 · Least privilege, against creep
Permissions only ever ratchet upward unless something forces them down. The villain has a name: privilege creep. Wildcards, "temporary" admin, and inherited roles nobody reviews. You will learn where it hides in each cloud and the tooling that finds it.
3 · Defense in depth
Identity is one layer of several. A design that is perfect at identity and naive at network, data, or detection is not secure — it just fails differently. Know what each layer adds and which one you are currently reasoning about.
What this stage does not cover scope honesty
Security is layered, so any single stage teaches a slice. CISA's Zero Trust Maturity Model, the most widely used public scoring framework, splits the problem into five pillars plus three cross-cutting capabilities. Here is exactly which parts this stage owns.
CISA, Zero Trust Maturity Model v2.0 (April 2023) — cisa.gov. Architectural definition: NIST SP 800-207 — csrc.nist.gov.
The four sessions
Before we start: four corrections to the reading list
Four things differ, and one of them changes what you should read.
| Assigned | What the material actually contains | What I did |
|---|---|---|
| AWS §3 "IAM … SCPs" |
§3 is correctly titled AWS Identity and Access Management (IAM) and is excellent on users,
groups, roles, policy structure, cross-account access and trust policies — it runs about a hundred pages
and is heavy on SAML and AssumeRole. But SCPs appear only eight times in passing. The real
treatment is §14, "AWS Organizations and Control Tower" — consistent with the C1 correction. |
Read §3 and §14. S2 teaches SCPs properly from §14 plus current docs. |
| AWS §4 "security tools" |
Correct — AWS Security Tools and Compliance: Security Hub, IAM Access Analyzer, CloudTrail, Config, GuardDuty, Inspector. But it predates the June 2025 re-organisation of Security Hub, so its 66 references to "Security Hub" all describe what is now called Security Hub CSPM. | Taught with the current naming, staleness flagged inline. |
| GCP §11 "Cloud IAM & the GCP security model" |
§11 is actually titled "Authentication with Google Cloud IAM" and covers roles, members/policies, service accounts and Cloud Storage access control. It does not cover organization policy constraints, IAM best practices or encryption — those are §12, "Exploring IAM, Project Organization, and Encryption". Your S4 plan explicitly includes org policy constraints, so §11 alone is not enough. | Read §11 and §12. |
| GCP: Security Command Center |
SCC appears nowhere in the material — zero mentions across all 658 pages. It is on your S4 list and it is the single most important GCP security service, so the gap matters. | Taught from Google's own docs; flagged as a source gap, not a source error. |
| Cloud_Patterns | Confirmed: no security section, as you expected. The genuinely relevant material is the External Configuration and Backend Service patterns in §3 — thirty mentions of "secret", fourteen of "vault". Brown poses the bootstrap problem beautifully and stops short of the modern answer. | S1 uses it as the neutral framing for secrets, then goes past it from first principles — and says so. |
| Azure §7–8 | Both correct: Identity and Access Management and Securing Azure Environment. One gap worth naming: managed identities get a single mention in §7, despite being the central Azure workload-identity mechanism. §16 also revisits RBAC, Policy and Defender. | Managed identities taught in full from Microsoft Learn; three-pass treatment in S3. |
Freshness ledger verified July 2026
Identity services get renamed and extended faster than anything else in cloud. Everything below post-dates at least one of the materials; each is carried into the session where it matters.
| Area | What changed | When | Tab |
|---|---|---|---|
| Security Hub | AWS renamed the original service Security Hub CSPM and gave the name "Security Hub" to a new unified service that correlates GuardDuty, Inspector, Macie and CSPM signals. GA at re:Invent 2025; preview users had to opt in by 15 Jan 2026 or be disabled. Both now appear in the console. | Jun 2025 → Dec 2025 | S2 |
| RCPs | Resource control policies — a seventh evaluation layer that caps what can be done to resources. Launched for S3, STS, KMS, SQS and Secrets Manager; since extended to Cognito, CloudWatch Logs and DynamoDB. Quota doubled to 2,000 per organization this month. | Nov 2024 → Jul 2026 | S2 |
| Root user | Centralised root access management: you can now delete root credentials outright in member accounts and use scoped sts assume-root sessions for the rare tasks that need them. New Organizations accounts have no root credentials by default. | Nov 2024 | S2 |
| Access Analyzer | Now three finding types, not one: external, internal (automated reasoning across identity policies, resource policies, SCPs, RCPs and boundaries) and unused access. | 2024–2025 | S2 |
| Inspector Classic | End of support reached 20 May 2026. If the material shows Inspector Classic screens, they are gone. | May 2026 | S2 |
| Azure AD | Renamed Microsoft Entra ID. Your Azure source already uses "Entra" throughout, so it is current here — but 23 residual "Azure AD" references remain in §7. | Aug 2023 | S3 |
| Mandatory MFA | Phase 1 (portal) hit 100% of tenants Mar 2025. Phase 2 extends MFA to every resource-management path — CLI, PowerShell, REST, SDKs, IaC — from Oct 2025. The last postponement window expired 1 July 2026. Workload identities are exempt. | Oct 2025 → Jul 2026 | S3 |
| Key Vault | Azure RBAC is now the default access-control model for new vaults from API version 2026-02-01. Access policies are legacy. Control-plane API versions older than that retire 27 Feb 2027. Existing vaults do not migrate themselves. | Feb 2026 | S3 |
| Entra Agent ID | AI agents became a third identity type alongside users and workload identities, with blueprints, sponsorship, dedicated directory roles and Conditional Access templates. GA 2026. | 2026 | S3 |
| IAM policy types | Google IAM is no longer just allow policies. It is now three: allow, deny, and principal access boundary (PAB) in the IAM v3 API. The material teaches only the first. | 2023–2025 | S4 |
| Workload Identity | "GKE Workload Identity" was renamed Workload Identity Federation for GKE, and gained a direct-principal mode that skips service-account impersonation entirely. | 2024 | S4 |
| SCC Enterprise | The Enterprise tier shuts down 21 May 2027; organizations move to Premium automatically. Standard remains free — the only tier to touch in a lab. | announced 2026 | S4 |
| Agent identities | Google added agent identities and agent identity pools as first-class IAM principals, addressable in allow, deny and PAB policies. | 2026 | S4 |
| Vertex AI | Repeated disclosures that the default Per-Project, Per-Product Service Agent is over-permissioned. Directly relevant to your Vertex work — treated at length in S4. | 2024–2026 | S4 |
Done means
Design it
You can lay out a least-privilege IAM model on any of the three clouds, and justify each grant against a specific need.
Explain it
authN vs authZ, federation, roles vs policies, human vs workload vs agent identity — in plain words, without hand-waving.
Read it
Given a policy document you can enumerate what it actually grants and name the privilege-creep anti-patterns in it.
Translate it
AWS IAM and SCPs ↔ Azure RBAC and Entra ↔ GCP IAM and org policy, fluently and in both directions.
1 · Why this session exists 10%
For thirty years, security had a geography. You bought a firewall, you put your servers behind it, and the firewall's job was to decide what crossed. Being inside was itself a credential.
That model made a bet: that the network edge and the trust boundary were the same line. Cloud broke the bet in three separate ways at once, and understanding why is what makes the rest of this stage feel inevitable rather than arbitrary.
Note what this is not saying. The network did not become unimportant — private endpoints, security groups and segmentation are all still real controls, and C4 is entirely about them. What changed is that the network stopped being sufficient, and identity became the layer that is always present, on every request, with no exceptions.
2 · Core concepts 50%
Authentication vs authorization
Two questions that people constantly merge into one, and that every cloud keeps rigorously separate.
Authentication (authN) is "who are you, and can you prove it?". Showing your passport at the airport check-in desk. The outcome is an established identity, or a rejection.
Authorization (authZ) is "given that I now know who you are, are you allowed to do this particular thing?". The boarding pass that gets you onto this flight, in this seat. The passport proved identity; it said nothing about which plane.
The distinction earns its keep because the two fail differently and are fixed differently. "Access denied" after a successful login is an authZ problem — adding another factor to your login will not help. Conversely, a stolen password is an authN problem, and no amount of careful permission design fixes it.
Every cloud API call runs the same five stages. The vendors differ in the names of the components, not the sequence. Step through it:
A signed request hits the control-plane API endpoint. It carries: the action being attempted
(read this object), the target resource identifier, a set of contextual attributes (source IP,
time, whether MFA was used, TLS version), and a credential.
The credential is almost never a password. For workloads it is a short-lived bearer token or a request signature derived from temporary keys. This matters: the thing on the wire is usually valid for minutes to hours, not forever.
The platform resolves the credential to a principal. It verifies the signature or validates the token — checking the issuer, the audience, the expiry, and the cryptographic signature against the issuer's public keys.
Output: a concrete principal identifier, or a 401-class failure. Note what has not
happened yet — nothing has been decided about permissions. A perfectly authenticated principal with zero
permissions gets exactly as far as this stage and no further.
The platform collects every policy that could bear on this request. This is the stage people underestimate, and it is where the three clouds genuinely diverge.
Policies can be attached to the principal, to the resource, to any ancestor node in the resource hierarchy you learned in C1, or to the session itself. On AWS there are six attachable policy types. On GCP there are three. On Azure, role assignments accumulate down the management-group chain. All of them get gathered before anything is decided.
The policies are evaluated together to produce a single boolean. Two rules are near-universal across all three clouds:
— Default deny. Absence of a grant is a denial. You never have to write "deny everything else";
that is the starting state.
— Explicit deny wins. If any applicable policy says no, the answer is no, regardless of how many
other policies say yes. Denials are not outvoted.
The result is a decision, and increasingly also a reason — all three clouds now ship troubleshooting tools that replay this evaluation and tell you which policy produced the outcome.
The decision is written to an audit log — allowed or denied. This is a control, not a formality. The audit log is what turns "we think we are least-privilege" into "here is which permissions were actually used in the last ninety days", which is the raw material every least-privilege tool consumes.
One caution that will recur: administrative actions are logged by default everywhere, but data-plane reads often are not, because the volume is enormous and the logging is billable. Reading an object is frequently invisible unless you turned it on.
- Some controls straddle both. Azure Conditional Access evaluates at authentication time but decides using authorization-shaped inputs (device compliance, risk score, location). It is best read as "authN with policy", and it is why Azure's identity story feels different from the other two.
- Successful authN is not nothing. Even with zero permissions, an authenticated principal has proven a credential is valid, can often enumerate error messages, and exists in your audit trail. Attackers value a valid low-privilege identity precisely because it is a foothold to escalate from.
- Authorization is stateless per request. There is no session that "stays" authorized. Every call is re-evaluated against current policy. Revoke a role binding and the next call fails — though a token already issued may remain valid until it expires, typically minutes to an hour. That gap is real and worth knowing during an incident.
- The third question. Mature systems ask accounting too — what did the principal actually do? AuthN, authZ and audit together are sometimes called AAA. Skipping the third makes the first two unverifiable.
Principals: three kinds, and only one of them is a person
"User" is the most misleading word in cloud security, because the overwhelming majority of principals in any real environment are not users. They are programs.
Think of a hospital. Some badges belong to people — a doctor swipes in, and the badge is tied to a human who can be trained, fired, and asked to prove who they are with a second factor. Other badges belong to machines — the pharmacy robot, the lab analyser. Nobody phones the robot to check it is really the robot; it authenticates constantly, silently, and nobody watches. And recently a third kind appeared: the badge for something that acts on behalf of a person but makes its own decisions about what to do next.
Human identity
A person. Authenticates interactively, usually against a corporate identity provider, ideally with a phishing-resistant second factor.
Lifecycle: joiner–mover–leaver, driven by HR. Sessions are hours. Controls that work: MFA, Conditional Access, just-in-time elevation, access reviews.
Workload identity
A program: a VM, a container, a function, a CI job. Authenticates non-interactively, continuously, at machine speed.
Lifecycle: tied to deployment, not to HR — which is precisely why nobody deprovisions them. MFA is meaningless here. Controls that work: platform-attested identity, scoped permissions, short-lived tokens.
Agent identity new
An AI agent. Authenticates like a workload but chooses its own actions, so its permission needs are unpredictable at design time.
Both Microsoft and Google shipped dedicated constructs for this in 2026 rather than reusing service principals unchanged — a strong signal the category is real.
The single most useful habit in this stage: whenever you read "identity", immediately ask which kind. Half of all bad cloud security advice comes from applying human-identity controls to workloads, or vice versa. Telling a service account to enable MFA is not a hardening step; it is a category error.
Workload identities outnumber human ones by a wide margin in any container or data platform, and they behave worse: they are created by pipelines, rarely reviewed, and frequently granted broad permissions "temporarily" so a deployment will go green. Two current figures put numbers on it:
- Orca's 2025 cloud security research found 78% of organizations have at least one IAM role that has not been used in over 90 days — standing, unused, still grantable.
- A 2026 infrastructure identity survey reported that 70% of organizations grant AI systems more access than they would give a human employee doing the same job. That is the agent-identity problem in one sentence, and it is the reason the vendors built a distinct construct for it.
Orca Security research, cited in orca.security · 2026 Infrastructure Identity Survey, cited by NHI Management Group.
How permissions get expressed: RBAC, ABAC, and policy-based access
Once you know who is asking, you need a way to write down what they may do. There are three broad grammars, and every cloud uses some blend of them.
RBAC — role-based. "Nurses may read charts." You define a job, attach permissions to it, and assign people to the job. Like a job title in an org chart: what you may do follows from what you are.
ABAC — attribute-based. "Anyone whose department matches the record's
department may read it." No named role — a rule compares properties of the principal, the
resource and the context. Like a rule that says you may enter any room whose colour matches your badge.
Policy-based. The general case, and what the clouds actually implement: a document that states allow-or-deny over (principals, actions, resources) with optional conditions. RBAC and ABAC both fall out of it — a role is a named bundle of policy, a condition block is where attributes go.
Every cloud's grant, stripped to its skeleton, is the same shape. Here is that shape in pseudo-syntax, with each part labelled by the triangle vertex it belongs to:
# --- the universal grant ---
effect: allow | deny # the verdict
principal: who this applies to # WHO (often implied by attachment)
action: what may be done # the verb
resource: on which thing # WHAT (often implied by attachment)
condition: ...only when this holds # ABAC lives here
The clouds differ mainly in which fields are implied by where you attach the document.
Attach it to a principal and the principal field is redundant. Attach it to a resource and the
resource field is redundant. Azure goes furthest — a role assignment names the principal, the
role definition and the scope, and derives everything else. That single insight makes all three dialects
readable.
| RBAC | ABAC | |
|---|---|---|
| Scales with | number of distinct job functions | number of distinct rules |
| Fails by | role explosion — one role per team × environment × region, then thousands of near-duplicates nobody dares delete | opacity — you cannot answer "who can read this?" by reading a list; you must evaluate rules against every principal |
| Auditability | excellent: enumerate assignments | hard: requires a solver, which is why all three clouds now ship automated-reasoning analysers |
| Onboarding a new team | create and assign new roles | often zero changes — tag the resources correctly and access follows |
| Depends on | disciplined role design | disciplined tagging — and an untagged resource silently gets no access, or worse, matches a broad rule |
The practical answer everywhere is a hybrid: RBAC for the coarse grain, ABAC for the fine grain. Roles decide what kind of thing you may do; conditions decide which instances. The failure mode to avoid is using ABAC as a patch over a role you were afraid to scope down — the condition becomes the only thing standing between a broad grant and a broad breach, and conditions are much easier to get subtly wrong than role assignments.
Least privilege, and the villain: privilege creep
Least privilege is a one-line idea: every principal gets exactly the permissions it needs for its task, and nothing more, for no longer than it needs them.
It is trivially agreeable and almost never achieved, for a reason worth stating plainly: permissions are asymmetric. Granting one takes seconds and unblocks somebody who is currently blocked. Removing one takes research, risks breaking something at 3am, and unblocks nobody. So the ratchet only turns one way.
The accumulated result has a name. Privilege creep: the slow, undirected growth of what an identity can do, well past what it does. It arrives through three doors — the wildcard, the inherited role, and the temporary grant that was never temporary.
The reason least privilege is a security control and not just hygiene is blast radius: the set of things an attacker reaches once they hold a credential. Least privilege does not reduce the chance of compromise. It reduces what compromise costs.
The wildcard
* in the action or resource field.
Usually written during debugging, when the real permission was unknown and the deadline was real. It works
immediately, which is exactly the problem: nothing ever comes back to narrow it.
The managed-policy shortcut
Vendor-supplied "FullAccess"
policies. They are designed to make the happy path work on the first try, not to be least privilege.
AmazonSageMakerFullAccess, for instance, also grants selected access to S3, ECR and CloudWatch
Logs — entirely reasonable as a starting point, and routinely still attached in production two years
later.
Inheritance
A grant made at an org or folder node applies to every descendant, including projects created afterwards by people who never saw the grant. On GCP this is sharpened by allow policies being purely additive — a child cannot reduce what a parent granted.
The permanent temporary
Elevated access granted for an incident, a migration, a demo. The incident ends; the grant does not. This is the case just-in-time elevation exists to solve, by making the grant expire on its own rather than depending on someone remembering.
The countermeasure is measurement, not intention. "We will be careful" fails predictably. What works is a feedback loop: log every authorization decision, compare granted permissions to used permissions over a window, and remove the difference. Every cloud now ships this — AWS IAM Access Analyzer's unused-access findings, Entra Permissions Management, GCP's IAM Recommender. The tooling finds the gap; closing it is still a human decision, and that remains the bottleneck.
Federation and trust: how one system believes another
Federation is a system agreeing to accept identity claims issued by a system it does not control.
The everyday analogy is a passport. A hotel in Tokyo does not maintain a database of Irish citizens. It accepts a passport because it trusts the Irish government's issuing process — the tamper-evident document, the known format, the verifiable signature. The hotel authenticates the document, and thereby, at one remove, the person.
Two roles matter. The identity provider (IdP) issues claims — the passport office. The relying party or service provider consumes them — the hotel. The trust is configured once, deliberately, and then every subsequent login rides on it.
Single sign-on (SSO) is the user-visible consequence: authenticate once to the IdP, and every relying party that trusts it lets you in without a separate password. SSO is the feature; federation is the plumbing.
This exact five-step dance underlies corporate SSO, GitHub Actions deploying to a cloud, and a Kubernetes pod getting cloud credentials. Learn it once here and you will recognise it in all three clouds:
Once, by an administrator. The cloud is told: "trust tokens issued by this issuer, for this audience, and only when the subject claim matches this pattern."
Those three constraints — issuer, audience, subject — are the whole security model, and getting the subject pattern wrong is the classic federation mistake. A trust that accepts any repository from a CI provider, rather than your specific repository on your specific branch, hands your cloud account to anyone who can open a free account with that provider.
The workload or user proves itself locally, to its own platform. A person types a password and taps a security key at the corporate IdP. A pod does nothing at all — its orchestrator projects a signed token into the container's filesystem because it knows which pod it started.
This is the step that makes federation powerful: the local platform already has strong knowledge of identity. Federation stops that knowledge from being thrown away and re-established with a password.
The resulting token is presented to the cloud's token service. It is a signed assertion — a SAML assertion for classic enterprise SSO, a JSON Web Token for OIDC, which is what nearly all machine-to-machine federation now uses.
The token contains the claims: who the subject is, who issued it, who it is for, when it expires. It is signed, not encrypted — anyone can read it, nobody can forge it without the issuer's private key.
The cloud validates the signature and maps the subject to a cloud principal. It fetches the issuer's public keys from a well-known endpoint, checks the signature, checks that issuer, audience and expiry all match the configured trust, then applies the mapping rule.
The mapping is where federation meets authorization: "the subject
repo:acme/api:ref:refs/heads/main may act as this cloud role". Note it is still just a
rule — and a sloppy one is a wildcard by another name.
The cloud issues its own short-lived credentials. Typically valid for fifteen minutes to an hour, scoped to the mapped principal's permissions, and automatically refreshed by the SDK.
This is the payoff. No long-lived secret was created, stored, rotated or leaked at any point. The only durable artefact is the trust configuration itself — a public, auditable statement that lives in your infrastructure code rather than a password in a vault that somebody has to remember to rotate.
- The subject pattern is the whole boundary. Trusting an issuer without constraining the subject is the single most dangerous federation misconfiguration, and it is easy to make because the broad version works perfectly in testing.
- Trust is transitive whether you like it or not. If you federate with an IdP, then whoever administers that IdP can mint an identity that your cloud accepts. Federating with a partner means inheriting their offboarding discipline.
- Token lifetime is a real trade-off. Short tokens shrink the window a stolen one is useful, and increase load on the token service and the blast radius of an outage in it. Fifteen minutes to one hour is the common range for workloads.
- Revocation is weak by design. A signed token is valid until it expires; there is no central "cancel". You revoke the underlying grant so the next exchange fails. Plan incident response around that lag rather than being surprised by it.
- Clock skew breaks things silently. Tokens carry not-before and expiry claims. A host whose clock drifts by minutes produces authentication failures that look like permission problems and waste hours.
Secrets versus identities — and the bootstrap problem
the patterns catalog's External Configuration pattern makes the case against hardcoded credentials precisely: settings baked into an application force you to give developers the production credentials, and committing them to source control spreads them to everyone with repository access — while secret credentials, he argues, should be the most closely guarded settings of all. His recommended shape is environment variables populated from a values-management service such as HashiCorp Vault or a cloud parameter store, deliberately keeping the application from depending on any one vendor's secrets API.
He then names the trap in the approach, and it is the important part: the application needs settings in order to reach the store that holds its settings. A chicken-and-egg problem, stated in a patterns catalog, left open.
That open problem has a name in practice: secret zero. You can move every credential into a vault, and you are still left with one credential — the one that opens the vault. Move that into a second vault and you have moved the problem, not solved it.
The modern answer is not a better hiding place. It is to stop having a secret zero at all.
Here is the shift, in one line. A secret is something you know. An identity is something you are. A password can be copied by anyone who sees it, and remains valid wherever it is used. But the platform that started your container already knows, with certainty, which container it started — it allocated the process. Workload identity is the platform attesting to that fact, cryptographically, so the container never needs to know a password in the first place.
The analogy: a door code is a secret — whoever overhears it gets in, forever. Your face is an identity — it cannot be handed to a colleague, and it is verified fresh each time.
This move is not in the patterns catalog, so I am teaching it from first principles rather than attributing it. Brown's framing is still exactly right about why configuration must be external; the platforms simply grew a better answer to how after the pattern was written.
Every cloud implements the same three-part trick, and it is worth seeing the shape before you see the vendor names in the next three tabs:
- The platform attests. The hypervisor, orchestrator or runtime knows which workload it launched and will vouch for it.
- The workload asks locally. It calls a link-local metadata endpoint — unroutable, reachable only from inside that instance — and asks for a token. No credential is presented, because the ability to make the call from that network position is the credential.
- A short-lived token comes back, scoped to whatever identity was attached at deployment time, and the SDK refreshes it automatically before it expires.
Secrets do not disappear; they get demoted. Vaults remain the right home for the things that are irreducibly secrets — third-party API keys, database passwords for engines that only speak passwords, signing keys. But the credential your workload uses to open the vault becomes a platform identity, and secret zero stops existing.
That metadata endpoint is reachable from any process in the workload — including code you did not write. A compromised dependency, a prompt-injected agent, or an attacker with code execution can simply ask for the token, and the platform will hand it over, because from the platform's point of view the request is legitimate. Workload identity eliminates credential theft-at-rest, not credential abuse-at-runtime. This is precisely the mechanism behind the Vertex AI service-agent findings we work through in Session 4.
- Crossing a trust boundary the platform cannot attest to — a third-party SaaS API that has never heard of your cloud. Unless it supports OIDC federation, you hold a key.
- Legacy protocols. Database engines, SMTP, message brokers that only understand a username and password. Cloud databases increasingly accept platform identity; older engines do not.
- Signing material. A private key for signing artefacts is a secret by definition. It belongs in a key-management service that performs the signing internally so the key never leaves.
- The escalating hierarchy. Preference order, best first: platform-attested identity → federated token from a trusted issuer → short-lived secret from a vault, fetched with a platform identity → long-lived secret in a vault → long-lived secret in an environment variable → secret in source control. Every step down multiplies both the time a leak stays useful and the number of people who can cause one.
Decision tree: how should this workload authenticate?
The most common design question in this stage. Follow "no" downward; the first "yes" exits right. The bottom-left box is the default when nothing above matched.
The three clouds, at concept level
Deliberately shallow — each row is unpacked properly in its own session. Read it as a map of where you are going.
| Neutral concept | AWS | Azure | GCP |
|---|---|---|---|
| Grammar of a grant | JSON policy document; action and resource both explicit | role assignment: principal + role definition + scope; resource implied by scope | binding: role + principals, attached at a hierarchy node |
| Primary style | policy-based, with RBAC layered on top via roles | RBAC-first, ABAC added as conditions | RBAC-first (roles are bundles of permissions), conditions via CEL |
| Where "deny" lives | any policy can carry Effect: Deny | separate deny assignments — rare and mostly system-created | separate deny policies, distinct from allow policies |
| Workload identity | role assumed via instance profile, IRSA or Pod Identity | managed identity, attached to the resource | service account, attached to the resource |
| Number of policy types in the evaluation | six attachable + a global explicit-deny check | role assignments, deny assignments, ABAC conditions, plus Azure Policy on the side | three: allow, deny, principal access boundary |
| Inheritance direction | SCPs and RCPs restrict downward; identity policies do not inherit | role assignments inherit downward and accumulate | allow policies inherit downward and are purely additive |
| Short-lived credential service | STS — explicit and user-visible; you call AssumeRole | largely invisible; the SDK and IMDS handle it | STS + metadata server; visible for federation, invisible for attached accounts |
| Biggest single trap | the interaction between six policy types — things fail for reasons two layers away | Entra roles vs Azure RBAC roles are different systems with similar names | allow policies cannot subtract — a broad grant high in the tree cannot be narrowed below it |
3 · Reality check 25%
Worked example: counting a blast radius on paper
"Least privilege is important" is a slogan until you count. So let us count. No console, no tools — arithmetic.
The scenario. A modest GenAI project. One inference service pulls model weights from object storage and serves an endpoint. Its credential leaks — a debug log, a compromised dependency, an over-shared container image. Same leak, two different policies, written by two different engineers on two different days.
The inventory (deliberately small — a real production account is ten to a hundred times this):
| Resource class | Count | Notes |
|---|---|---|
| Object storage buckets | 40 | 1 holds model weights; 12 application data; 6 logs and audit; 4 backups; 17 other teams' |
| Managed databases | 8 | 2 production, 6 dev and analytics |
| Stored secrets | 25 | third-party API keys, DB passwords, signing keys |
| Compute instances | 60 | mixed CPU and GPU nodes |
| Container registries | 3 | including the one CI pushes to |
| Identity surface | 1 | the ability to create roles, users and keys |
Design A — the wildcard
Written during a Friday-afternoon debugging session, when the exact permission was unknown and the
demo was Monday. Effect: allow, action *, resource *.
| Step | Reachable | Running total |
|---|---|---|
| Read the weights bucket (the intended use) | 1 bucket | 1 |
| Read every other bucket — app data, logs, backups, other teams' | +39 buckets | 40 |
| Read the 25 stored secrets — which contain credentials to further systems | +25 secrets | 65 |
| Reach 8 databases directly, plus any reachable via those secrets | +8 databases | 73 |
| Read, stop, start, snapshot or delete 60 instances | +60 instances | 133 |
| Pull and push to 3 registries — supply-chain write access | +3 registries | 136 |
| Delete the 6 log and audit buckets — destroying the evidence | (already counted) | 136 |
| Create a new admin identity with its own keys | persistence | unbounded |
The last row is the one that changes the character of the incident rather than its size. Everything above it is damage. That row is persistence: the attacker no longer needs the leaked credential. Revoking it does not end the incident, and you now cannot be sure the incident has ended at all. Note also that the audit trail is deletable by the same credential, so your ability to answer "what did they take?" was itself in the blast radius.
Design B — scoped
Effect allow, action read-object, resource
weights-bucket/gemma-2b/*, condition: request originates from the service's own network.
| Step | Reachable | Running total |
|---|---|---|
| Read one prefix in one bucket | 1 prefix | 1 |
| Everything else in that bucket | denied | 1 |
| All other buckets, secrets, databases, instances, registries | denied | 1 |
| Create an identity | denied — no persistence | 1 |
| Use the credential from outside the service network | denied by the condition | 0 |
The arithmetic that matters
136 → 1
Distinct resources reachable. A 136× reduction in blast radius from one policy decision that cost about ten minutes to make correctly.
25 → 0
Secrets exposed. Each one is a credential to another system, so this is the term that makes the blast radius compound beyond your own account.
∞ → 0
Persistence paths. Design A survives credential revocation. Design B does not. This is the difference between an incident and a breach.
≈ 10 min
The cost of Design B, once. Everything above is what that ten minutes bought — assuming it was spent before the leak, not after.
Now multiply. The example assumed the role was attached to one workload. Orca's 2025 research found that in 12% of organizations, a single over-permissive role is attached to more than 50 instances. Under that condition, a compromise of any one of those 50 instances yields the full 136-resource blast radius — so the probability of the credential leaking has been multiplied by fifty while the consequence stayed constant. That is the actual shape of the risk: not one bad policy on one machine, but one bad policy fanned out across a fleet, where the weakest instance sets the security of the whole thing.
Orca Security, Top 5 Cloud Security Risks of 2025 — orca.security.
Three grounded examples
Palo Alto's Unit 42 responded to more than 750 major cyber incidents in 2025. Their 2026 incident response report finds that in over 90% of breaches, preventable gaps materially enabled the intrusion — specifically limited visibility, inconsistently applied controls, and excessive identity trust. Their initial-access breakdown is worth internalising: credential misuse and brute force account for 21% (13% previously-compromised credentials, 8% brute force), and identity policy and insider risk another 11%, of which IAM misconfiguration such as overly permissive policies is 3% and insider abuse of legitimate credentials is 8%.
Read those numbers carefully, because the naive reading is wrong. IAM misconfiguration being "only 3%" of initial access does not make it minor — it is rarely how attackers get in, and very often how a small intrusion becomes a large one. Their own recommendation is framed exactly that way: reduce the area of impact by tightening IAM to remove excessive trust and limit lateral movement. Least privilege is a blast-radius control, not an entry-prevention control, and measuring it as the latter will make you undervalue it.
Unit 42, 2026 Global Incident Response Report — paloaltonetworks.com.
Google's Cloud Threat Horizons Report H1 2026 documents a campaign the Google Threat Intelligence Group attributes with moderate confidence to the North Korean state-sponsored group UNC4899, targeting a cryptocurrency organization in 2025. The chain ran from an initial developer endpoint compromise to actions on the attacker's objectives inside the cloud environment, and Google's own summary of the lesson is that it is a classic case of overly permissive trust in automated pipelines creating a direct path into a cloud environment.
Every link in that chain is a topic in this stage. The developer endpoint is a human identity. The pipeline that trusted it is a federation trust with a subject pattern that was too broad. The lateral movement afterwards is blast radius. Google's recommended controls map one-to-one onto later sessions: IAM Recommender to reduce excessive permissions, organization policies to prevent external identities and public access, and Security Command Center to detect the drift.
Google Cloud, Cloud Threat Horizons Report H1 2026 — cloud.google.com.
If you are ever asked "are we zero trust?", the honest answer references two documents. NIST SP 800-207 (August 2020) is the architectural definition — seven tenets, and a logical split between a policy engine that decides, a policy administrator that issues, and policy enforcement points that gate. Its core claim is that the enterprise network is no more trustworthy than any other network, so trust is never granted implicitly and must be continually evaluated. NIST later added SP 800-207A specifically for cloud-native applications in multi-location environments, whose central move is exactly the one this session opened with: shifting access control from network parameters to identity.
CISA's Zero Trust Maturity Model v2.0 (April 2023) is the scoring instrument — five pillars, four maturity stages from Traditional through Initial and Advanced to Optimal. Two things to know about it. First, each pillar can progress at its own pace, so "we are Advanced on Identity and Traditional on Data" is a coherent and common position. Second, the honest industry read is that most enterprises score Traditional or Initial across the board — so if a vendor tells you a product makes you zero trust, they are describing a pillar, at best.
NIST SP 800-207 — csrc.nist.gov · NIST SP 800-207A — nvlpubs.nist.gov · CISA ZTMM v2.0 — cisa.gov.
4 · Apply it 10%
Your deployments, re-read as identity
You have already built four things that each answer the "how should this workload authenticate?" question differently. You probably did not think of them that way at the time. Here they are on the tree you just learned:
| What you built | Human identity in play | Workload identity in play | Where it lands on the tree |
|---|---|---|---|
| Gemma on vLLM, via Modal |
Your Modal account, and whatever cloud account it federates from. | Modal's own execution sandbox. Anything the function reaches outside Modal — a Hugging Face token, an S3 bucket — needed a credential you supplied. | Branch 4: third-party platform. Those tokens are real secrets. Worth asking what their scope and expiry actually are. |
| Vertex AI deployment script, T4 + spot |
Your gcloud credentials running the script — almost certainly broadly privileged. |
The Vertex service agent the endpoint runs as, which you very likely never chose. | Branch 2: managed compute, so platform identity is available — but the default identity is the issue, not the mechanism. Session 4 takes this apart line by line. |
| vLLM tutorial stack on a GCP L4 |
You, over SSH. | The VM's attached service account — by default a broad one, reachable from any process on that box via the metadata endpoint. | Branch 2. The correct instinct: give the VM its own narrow service account rather than the default. |
| Kubernetes / KServe concepts |
Your kubeconfig, which is a cluster credential and often a long-lived one. | The pod's Kubernetes service account — and separately, whatever cloud identity that maps to. | Branch 2, with the twist that there are two identity systems stacked. Getting them to line up is exactly what IRSA, Entra Workload ID and Workload Identity Federation for GKE exist to do. |
The one question to carry into the next three sessions. For your inference service, the model weights sit in object storage and the container must read them at start-up. That single read is the cleanest possible test of everything in this session: which principal makes it, what proves that principal's identity, which policy permits it, and what else does that same policy permit? If you can answer those four for a workload you actually built, the vendor tabs become naming exercises.
Optional lab — the paper audit cost: $0.00
No console, no CLI, nothing billable — deliberately, because the concept is the deliverable here and nothing in this session needs a cloud to practise.
- Pick one workload you have deployed. The Vertex endpoint is the best choice because Session 4 revisits it.
- Draw the triangle: principal (name it exactly — not "the app", the actual identity), policy (what grants it access, and where that grant is attached), resource (the specific thing).
- Answer three questions in writing. What proves this principal is who it claims to be? — a key file, a platform attestation, a federated token? What else does this grant permit? — not what it is used for; what it permits. If this credential leaked right now, what is the count? Do the arithmetic above against your own inventory.
- Walk the decision tree for that workload. Note where you actually landed versus where the tree says you should have.
- Write one sentence: the smallest change that would most reduce the blast radius.
Billing note: nothing here touches a cloud, so there is nothing to tear down. From Session 2 onward the labs do touch real accounts — IAM itself is free on all three clouds, but the security services layered on top are emphatically not, and each lab flags exactly which ones to leave switched off.
AWS takes the triangle and makes every edge explicit in JSON — then stacks six policy types and a global deny check on top, so a request can fail for reasons two layers away from where you are looking.
Next: policy anatomy, roles versus users, permission boundaries, SCPs and RCPs, and the evaluation order that explains every "but I granted that!" moment on AWS.
1 · Why this session exists 10%
AWS is the most explicit of the three clouds about access control, and also the most layered. Every edge of the triangle is written out in JSON, nothing is implied — and then six separate kinds of document, attached at six different places, all get a vote on every request.
This is a real design choice with real consequences. The upside is that anything is expressible and everything is auditable. The downside is the single most common AWS experience: "I granted that permission and it still says access denied." Almost always the answer is that a different layer — one you were not looking at, possibly one owned by a different team — said no. You cannot debug that by reading harder at the layer you already know about. You have to know the stack.
Each of those layers was added to solve a real problem, roughly in that order over fifteen years. Nothing here was designed as a whole; it accreted. Knowing that makes the oddities easier to accept.
2 · Core concepts 50%
The identities: users, groups, roles
Three nouns, and the interesting one is the third.
An IAM user is a permanent identity with permanent credentials — a password, or an access key pair that works until someone deletes it. It is a named employee badge that never expires.
An IAM group is a bag of users that carries policies. It is purely an administrative convenience: groups are not principals, nothing "runs as" a group, and you cannot grant a group access to anything the way you would grant a role.
An IAM role is the one that does not exist in ordinary life. A role is an identity nobody owns. It has permissions but no permanent credentials. Instead, it has a second document — a trust policy — saying who is permitted to become it. When someone qualifies, they receive temporary credentials that expire.
The analogy that works: a role is a hi-vis vest hanging by the door of a construction site. The vest is not anybody's. It confers the right to be on site. The rule on the wall — "site staff and their contractors may wear this" — is the trust policy. You put it on, do the job, and hang it back up.
Why this matters more than it sounds: because a role's credentials expire, an attacker who steals them holds something with a clock on it. Because a user's access key does not expire, an attacker who steals that holds something permanent. This single property is why current AWS guidance is to create essentially no IAM users at all.
Every role carries two policy documents doing completely different jobs, and conflating them is the most common beginner error on AWS.
Trust policy — who may assume
Technically a resource-based policy attached to the role itself. Its
Principal field names who may call AssumeRole. It says nothing about what the role
can do.
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
Reads as: "EC2 may become me."
Permissions policy — what it may do
An ordinary identity-based policy. No Principal field — the principal is
implied by attachment.
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource":
"arn:aws:s3:::weights/gemma-2b/*"
}
Reads as: "whoever is me may read those objects."
Access denied on AssumeRole is a trust-policy problem. Access denied
after assuming is a permissions-policy problem. That one sentence resolves a large fraction of AWS
IAM confusion, and it is worth memorising before you touch a console.
- Default quotas per account (several are adjustable): 5,000 IAM users, 300 groups, 1,000 roles, 1,500 customer-managed policies. A user may belong to 10 groups and carry 10 attached managed policies. A managed policy document is capped at 6,144 characters — small enough that real least-privilege policies routinely hit it, which is itself an argument for ABAC conditions over enumerating resources.
- Two access keys per user, maximum. That limit exists to make rotation possible: create the second, migrate, delete the first. If you have users at all, this is the drill.
- The current answer to "how do humans log in?" is not IAM users. It is IAM Identity Center — AWS's SSO service, formerly AWS SSO — or federation from your own identity provider. Both issue short-lived role sessions instead of permanent keys. A greenfield AWS environment in 2026 should contain zero IAM users with console passwords.
- Roles are cheap. Create more of them. The instinct to reuse one role across several workloads is exactly the mechanism that produced the "one permissive role on fifty instances" figure from Session 1. A role per workload costs nothing and bounds each blast radius separately.
Policy anatomy
An AWS policy is a list of sentences. Each sentence says: allow or deny — these verbs — on these nouns — optionally only when some fact about the request holds. That is the entire grammar. Everything else is vocabulary.
{
"Version": "2012-10-17", // always this date; it is a language version, not a timestamp
"Statement": [{
"Sid": "ReadGemmaWeightsOnly", // your label. Show up in denials. Name them well.
"Effect": "Allow", // Allow | Deny
"Action": "s3:GetObject", // service:Operation. Wildcards legal, hence dangerous.
"Resource": "arn:aws:s3:::weights/gemma-2b/*",
"Condition": { // where ABAC lives
"StringEquals": { "aws:PrincipalTag/team": "inference" }
}
}]
}
Three details that repay attention:
- The ARN is the resource half of the triangle, and it is structured:
arn:partition:service:region:account-id:resource. Empty segments are meaningful — S3 ARNs carry no region or account because bucket names are globally unique. Getting an ARN subtly wrong produces a silent denial, never an error. Resourceis absent from trust policies and present in identity policies;Principalis the reverse. If you find yourself writing both in one statement, you are probably writing a resource-based policy — which is fine, but know which one you are writing.- The condition block is where least privilege becomes affordable. Enumerating a thousand ARNs blows the 6,144-character limit; one condition on a tag does not. This is why ABAC is not a nicety on AWS but a scaling necessity.
The skill worth building is not writing policies but reading them for what they permit beyond their stated purpose. A checklist:
- Wildcards in
Action.s3:*includesDeleteBucketandPutBucketPolicy— the second lets the holder grant access to anyone.iam:*is account takeover written in six characters. - The escalation actions specifically.
iam:PassRole,iam:CreatePolicyVersion,iam:AttachUserPolicy,sts:AssumeRolewith a loose resource, andlambda:CreateFunctioncombined withPassRole. Each is a path from limited permissions to unlimited ones, and none of them looks alarming in isolation. NotActionandNotResource. Legal, occasionally necessary, and inverted logic that humans reliably misread. Treat their presence as a prompt to slow down.- Conditions that do not constrain what you think.
aws:SourceIpdoes nothing against a request routed through a VPC endpoint; you wantaws:SourceVpce. A condition that never matches fails closed and is merely annoying; one that always matches fails open and is a vulnerability.
Identity policies, resource policies, and how they combine
Two attachment points, and the rules for combining them are not symmetric — which surprises people.
Identity-based
Attached to a user, group or role. Answers "what may
this principal do?". No Principal field. The default place to put permissions.
Resource-based
Attached to the resource: bucket policies, KMS key
policies, SQS queue policies, Lambda resource policies, Secrets Manager resource policies, and role trust
policies. Answers "who may touch this thing?". Has a Principal field.
- Same account: either one can grant. An allow in the identity policy is enough; an allow in the resource policy is enough. You do not need both.
- Cross-account: you need both. The resource's account must allow your principal, and your account must allow the principal to make the call. Two teams have to agree, deliberately. This is a feature, and it is the mechanism behind almost all safe cross-account sharing.
- The short-circuit worth knowing: if a resource-based policy grants access directly to an IAM user or a role session principal, an implicit deny in a permissions boundary or session policy does not block it. That asymmetry exists for good reasons and it is exactly the kind of thing that makes a boundary less absolute than its name suggests.
- KMS is the one to watch. A KMS key policy is unusual in that it must explicitly delegate to IAM before identity policies have any effect on the key at all. Encrypt something with a key whose policy you then break and you can lock yourself out of your own data permanently — one of the few genuinely unrecoverable mistakes on AWS.
STS and the assume-role exchange
AWS Security Token Service (STS) is the component that mints temporary credentials. The material covers it heavily in §3 and it is worth the attention — STS is the beating heart of AWS identity, and nearly every modern AWS credential passes through it.
The caller already holds some identity: an IAM user's keys, an EC2 instance profile, a federated
SAML assertion, or an OIDC token from GitHub Actions or a Kubernetes cluster. Note that even a federated
caller with no AWS identity at all can start here — that is what
AssumeRoleWithWebIdentity and AssumeRoleWithSAML are for.
It calls sts:AssumeRole naming a target role ARN, a session name, and optionally: a
session policy (a further restriction applied to this session only), a duration, and session tags for
ABAC.
The session name is not cosmetic — it lands in CloudTrail and is often the only way to attribute a role session back to a human or a workload afterwards. Set it to something meaningful.
STS performs two separate authorization checks, and both must pass:
— Does the target role's trust policy permit this caller to assume it?
— Does the caller's own identity policy permit sts:AssumeRole on that role ARN?
The double check is deliberate: the role owner consents to being assumed, and the caller's administrator consents to the caller assuming it. Neither side can unilaterally create the link.
STS returns three values — an access key ID, a secret access key, and a session token. The third is what distinguishes temporary credentials from permanent ones; requests carry it in a header.
Default lifetime is one hour. It can be extended to twelve for role chaining scenarios, and the role's own maximum session duration caps it. Shorter is better; the SDKs refresh automatically, so short durations are usually free.
The credentials work until they expire, then stop. Nothing needs to be revoked, cleaned up, or rotated.
The nuance for incident response: you cannot un-issue a session. Detaching the role's policies
affects the next call, and attaching a deny policy conditioned on
aws:TokenIssueTime is the documented way to cut existing sessions off. Knowing that in advance
is worth more than discovering it at 2am.
Permission boundaries: a ceiling, not a grant
A permissions boundary is a policy that defines the maximum a principal can ever have, regardless of what anyone grants it later. It grants nothing by itself.
The analogy: a spending limit on a corporate card. Your manager can approve any expense they like; the card still declines above the limit. The limit does not authorise a single purchase — it only caps them.
The reason it exists is safe delegation. You want to let a team lead create roles for their own team without being able to create a role with administrator access. Give the lead permission to create roles, but require that every role they create carries a specific boundary. They get autonomy; the ceiling holds.
Effective permissions are the intersection of the identity policy and the boundary:
Identity policy grants: s3:* ec2:* iam:*
Boundary permits: s3:* ec2:Describe*
──────── ────────── ───
Effective: s3:* ec2:Describe* (nothing)
Both must allow. Neither alone suffices. Adding a boundary to an existing principal reduces what it can do; removing one increases it — which is why boundaries are usually enforced by an SCP requiring that they be attached, rather than by trusting people to attach them.
| Permissions boundary | Service control policy | |
|---|---|---|
| Attached to | one IAM user or role | an account, OU, or the org root |
| Managed by | an IAM admin in the account | the organization's management account |
| Applies to | exactly that principal | every principal in every affected account |
| Escapable? | yes, by anyone who can detach it | no — not even by the account's own administrator |
| Use it for | delegating role creation safely | guardrails nobody in the account may cross |
Both are ceilings; the difference is who holds the ladder. A boundary is a local ceiling an account admin controls. An SCP is a ceiling imposed from outside the account, which is precisely why it is the right tool for "no one in the sandbox OU may leave the EU region" and the wrong tool for "this one role should not grow".
Organization-level guardrails: SCPs, RCPs, and declarative policies
§3 mentions SCPs eight times in passing. The substantive treatment is §14, "AWS Organizations and Control Tower" — consistent with the C1 finding that this material's Organizations coverage lives late. RCPs and declarative policies do not appear in the material at all: RCPs launched November 2024, after it was written. Everything on RCPs below comes from AWS documentation.
SCPs and RCPs are both org-wide ceilings, and the difference between them is which vertex of the triangle they cap.
SCP caps the principal. "Nobody in this organization may do X." It follows the actor.
RCP caps the resource. "Nothing may be done to my resources by anyone outside this organization, whatever their own permissions say." It follows the thing.
Why the second one had to exist: before RCPs, an org administrator could constrain their own people perfectly, and still have a bucket policy in some member account granting a stranger read access. The SCP never saw that request, because the requester was not their principal. RCPs close that hole from the resource side — which is exactly what AWS means by a data perimeter.
- Both attach to the org root, an OU, or an account — the C1 hierarchy, reused. Both inherit downward and cannot be escaped by the account's own administrator.
- Both are filters, not grants. An SCP that allows an action grants nothing; it merely declines to remove it. You still need an identity policy. This is the number-one SCP misconception.
- Both ship with a default full-access managed policy attached everywhere —
FullAWSAccessfor SCPs andRCPFullAWSAccessfor RCPs — which cannot be detached and exists so that enabling the feature changes nothing until you write your first real policy. - The management account is exempt from SCPs. Every guardrail you write has a hole in the shape of that account, which is why the standing advice is to run no workloads in it.
- RCPs currently apply to a specific and growing service list — S3, STS, KMS, SQS and Secrets Manager at launch, with Cognito and CloudWatch Logs added in January 2026 and DynamoDB in February 2026. Check the current list before assuming coverage; a data perimeter with a gap is not one.
A third type exists that is neither: declarative policies enforce a desired configuration at the service control plane rather than gating an API call. Attach one and the configuration is maintained even as the service adds new features and APIs later — the "block public access on EC2 AMIs, forever, including for features that do not exist yet" case. Also in the family: backup policies, tag policies, chat-application policies, AI services opt-out policies, and Security Hub policies.
- The RCP quota doubled to 2,000 per organization in July 2026, from 1,000, at no extra cost and with no action required. If you read anything citing the old limit, it is out of date.
- SCPs gained the full IAM policy language in 2025, including condition keys and
NotAction. Older guidance describing SCPs as a restricted subset is stale. - Test SCPs on an OU containing one throwaway account. An SCP applied at the root that denies something load-bearing takes effect immediately across every account, and the people best placed to fix it may be the ones you just locked out.
- Deny-lists scale, allow-lists do not. An SCP that lists permitted services must be updated every time anyone adopts a new one, and that update becomes a bottleneck the whole org routes around. Denying specific dangerous things ages far better.
AWS Organizations policy types — docs.aws.amazon.com · RCP launch — aws.amazon.com · quota increase — aws.amazon.com.
The evaluation order, in full
Click any layer for what it does and how it fails.
One consequence is worth stating on its own. With Organizations in play and no resource-based policy, effective permissions are the intersection of the identity policy, the SCP and the RCP — the action must be allowed by all three. An explicit deny in any one of them overrides every allow.
AWS, Policy evaluation logic — docs.aws.amazon.com.
The root user — and why the material's advice is now outdated
Classic guidance, which the material follows, is: secure the root user with hardware MFA, lock the credentials in a safe, and never use them. That was correct advice and it is no longer the best advice.
Since November 2024, AWS supports centralised root access management. From the management account or a delegated administrator you can now delete root credentials outright in member accounts — password, access keys, signing certificates, MFA devices — and new accounts created in Organizations have no root credentials at all by default. For the handful of tasks that genuinely require root, you take a short-lived, task-scoped session instead:
aws sts assume-root \
--target-principal 987654321098 \
--task-policy-arn arn:aws:iam::aws:policy/root-task/IAMDeleteRootUserCredentials \
--duration-seconds 900
The security argument is clean: a credential that does not exist cannot be phished, leaked or forgotten in a
safe. It also removes an MFA device per account from your inventory, which at a hundred accounts is a real
operational saving. Enable it with
aws iam enable-organizations-root-credentials-management and
aws iam enable-organizations-root-sessions.
Two caveats. Removing credentials is currently a per-account console action, so at scale you will want to script it. And separately, AWS discontinued its free MFA security key programme in November 2025 — existing devices keep working, but you now buy your own.
AWS, Centrally manage root access — docs.aws.amazon.com AWS Security Blog.
Workload identity on AWS: instance profiles, IRSA, Pod Identity
Three mechanisms, all producing the same outcome — short-lived STS credentials with no stored secret — and differing in how the trust is established.
Instance profile (EC2)
A container for a role, attached to an instance. The workload calls the link-local metadata
endpoint and gets credentials. Trust policy names ec2.amazonaws.com.
Insist on IMDSv2. Version 1 answered an unauthenticated GET, which made every server-side request forgery bug in your app a credential-theft bug. v2 requires a PUT to obtain a session token first and defaults the hop limit to 1, which stops containers on the host from reaching it casually.
IRSA (EKS, since 2019)
IAM Roles for Service Accounts. The cluster gets an OIDC issuer; each IAM role's trust
policy names that issuer and a specific system:serviceaccount:namespace:name subject. The pod
exchanges its projected token via AssumeRoleWithWebIdentity.
Works everywhere Kubernetes runs, including EKS Anywhere, ROSA and self-managed clusters, because it depends only on standard OIDC.
EKS Pod Identity preferred
Since re:Invent 2023. An agent DaemonSet plus an association API. The role's trust policy
just names pods.eks.amazonaws.com — no OIDC provider, no per-cluster trust editing.
Free add-on; you pay only standard EKS cluster pricing and the DaemonSet's few tens of megabytes per node.
Why Pod Identity exists, concretely. With IRSA, every IAM role a pod can assume must carry the cluster's OIDC issuer URL in its trust policy. Five clusters means five trust statements per role; adding a cluster means editing every relevant role; and at scale you hit both the per-account OIDC provider limit and the trust-policy size limit, which forces people to duplicate roles. Cluster admins also often lack the IAM permissions to create OIDC providers, so every change becomes a cross-team ticket.
Pod Identity also does something IRSA cannot: it injects session tags automatically
— cluster ARN, cluster name, namespace, service account, pod name — which are usable in policy
conditions via aws:PrincipalTag. That makes real ABAC on Kubernetes workloads practical for the
first time: one role, conditioned on namespace, instead of a role per namespace.
Use Pod Identity for new workloads on EC2 node groups. Keep IRSA for Fargate (Pod Identity does not support it) and for existing clusters. Both are fully supported, both can coexist in one cluster, and there is no deprecation timeline for IRSA — so there is no big-bang migration to plan. Each new workload on Pod Identity is one fewer OIDC trust policy to maintain.
The security service catalogue — with one large naming correction
Your §4 refers to "Security Hub" 66 times. In June 2025 AWS took that name away from the existing service and gave it to a different, new one:
- Security Hub CSPM is the original service, renamed. Findings aggregation, security-standard checks, compliance scoring. Everything the material describes is this. It is fully supported with no announced deprecation.
- Security Hub (unqualified) is now a new unified service that correlates signals across
GuardDuty, Inspector, Macie and Security Hub CSPM to surface prioritised risks. It went GA at re:Invent on
2 December 2025 with near-real-time risk analytics, and uses
V2APIs (EnableSecurityHubV2) distinct from the classic ones.
Both now appear in the console side by side, which is as confusing as it sounds. When reading any AWS security material, check whether it predates June 2025 — if so, "Security Hub" means CSPM.
| Service | What it is | What it answers | Billable? |
|---|---|---|---|
| IAM Access Analyzer | least-privilege analyser | Three finding types now: external (who outside my trust zone can reach this?), internal (who inside my org can reach this critical resource?), and unused (which roles, keys, passwords and permissions have gone unused?). Internal findings use automated reasoning across identity policies, resource policies, SCPs, RCPs and boundaries. | external findings free; unused and internal analysers charged per resource |
| CloudTrail | control-plane audit log | Who called which API, when, from where. The evidentiary basis for everything else. Management events are recorded by default; data events are not and are billed separately. | first management-event trail free |
| AWS Config | configuration recorder | What did this resource's configuration look like at 14:00 last Tuesday, and does it comply with my rules? Config rules can auto-remediate. | yes, per configuration item |
| GuardDuty | threat detection | Is something anomalous happening? Consumes CloudTrail, VPC flow logs and DNS logs. Detects credential exfiltration, cryptomining, and IAM anomalies without you writing rules. | yes — 30-day trial |
| Inspector | vulnerability scanning | Do my EC2 instances, container images and Lambda functions contain known CVEs? Inspector Classic reached end of support on 20 May 2026 — if the material shows Classic, it is gone. | yes |
| Macie | sensitive-data discovery | Which of my S3 objects contain personal or sensitive data? Feeds the unified Security Hub. | yes — can be expensive at scale |
| Security Hub CSPM | posture & compliance | How do I score against CIS, PCI DSS, and the AWS Foundational Security Best Practices standard? | yes, per check |
| Security Hub v2 | unified risk correlation | Given everything the above found, what are the few things that actually matter right now? | yes |
| IAM Identity Center | workforce SSO | How do humans get in, without IAM users? Permission sets become roles in each account; sessions are short-lived. | free |
Cost warning for later labs IAM, Organizations, SCPs, RCPs and IAM Identity Center are free. GuardDuty, Inspector, Macie, Config, both Security Hubs, and Access Analyzer's unused/internal analysers are not. Enabling GuardDuty organization-wide "just to look" is a classic way to generate a surprising bill from an account you then forget about.
Decision tree: which AWS control fits this requirement?
AssumeRole time. Nothing persists afterwards, and nothing needs cleaning up.The three clouds, from the AWS side
| AWS concept | Azure equivalent | GCP equivalent | Key quirk when translating |
|---|---|---|---|
| IAM role + trust policy | no true equivalent | service account + impersonation | Nothing in Azure is "assumed". People arriving from AWS look for it for a week before accepting it is absent. |
| Identity-based policy | role definition + assignment | role in an allow-policy binding | AWS writes actions and resources in one document; the other two split "which permissions" (role) from "where" (scope or attachment point). |
| Resource-based policy | largely absent | allow policy on the resource node | Azure's scope inheritance does this job. The AWS habit of "just add a bucket policy" has no Azure translation. |
| Permissions boundary | — | principal access boundary | GCP's PAB is the closest analogue and arrived much later. Azure approximates it with scope discipline and deny assignments. |
| SCP | Azure Policy at management group | org policy constraint | Azure Policy also remediates, not just denies — it can modify or deploy resources. SCPs only ever subtract. |
| RCP | Azure Policy deny effect | org policy + deny policy | Neither has an exact match. The AWS data-perimeter pattern is genuinely more developed here. |
STS AssumeRole | Entra token acquisition | STSgenerateAccessToken | AWS makes the exchange explicit and user-visible; the other two mostly hide it in the SDK. |
| Instance profile | managed identity | attached service account | Same idea, three names. All three read from a link-local metadata endpoint. |
| IRSA / Pod Identity | Entra Workload ID for AKS | Workload Identity Federation for GKE | All three map a Kubernetes service account to a cloud identity. All three had a clumsy v1 and a cleaner v2. |
| IAM Access Analyzer | Entra Permissions Management | IAM Recommender / Policy Analyzer | All three find unused permissions. None of them removes anything — detection is solved, safe remediation is not. |
| CloudTrail | Activity Log + Entra sign-in logs | Cloud Audit Logs | All three log admin actions by default and data reads only if you pay. Assume data-plane access is invisible until proven otherwise. |
| GuardDuty | Defender for Cloud plans | Event Threat Detection (SCC Premium) | Azure bundles detection into one product with per-plan pricing; AWS sells it as separate services; GCP gates it behind an SCC tier. |
3 · Reality check 25%
Worked example: what does this role actually grant?
Reading an identity policy tells you what someone intended to grant. Computing the intersection across the layers tells you what is actually true. Here is that computation, by hand, for a realistic GPU inference role.
Layer 6 — the identity policy, the document attached to inference-svc-role and the only
one most people look at:
| Statement grants | Approx. actions | Why it is there |
|---|---|---|
s3:* | ~130 | "the service reads weights from S3" — written broad during setup |
bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream | 2 | fallback to a hosted model |
logs:CreateLogStream, logs:PutLogEvents | 2 | emit logs |
secretsmanager:GetSecretValue | 1 | a third-party API key |
iam:PassRole | 1 | added once so a job could launch with a role, never removed |
| Nominal total | ~136 | what a reviewer reading only this document would report |
Layer 4 — the permissions boundary the platform team attaches to every workload role:
Permits s3:Get* and s3:List* (~40 actions), bedrock:*
(~40), logs:* (~30). Says nothing about Secrets Manager or IAM — and in a boundary, silence is
denial.
Layer 2 — the SCP on the workloads OU: deny every action outside eu-west-1, and deny
iam:* outright for workload roles.
Layer 1 — the RCP at the org root: deny all S3 access unless the calling principal's
aws:PrincipalOrgID matches the organization.
The intersection, computed
| Service | Identity (L6) | ∩ Boundary (L4) | ∩ SCP (L2) | ∩ RCP (L1) | Effective |
|---|---|---|---|---|---|
| s3 | ~130 | 40 (Get*/List* only) | 40, region-gated | 40, in-org callers only | 40 |
| bedrock | 2 | 2 (boundary is wider; identity is the limit) | 2, region-gated | 2 | 2 |
| logs | 2 | 2 | 2, region-gated | 2 | 2 |
| secretsmanager | 1 | 0 — absent from boundary | 0 | 0 | 0 |
| iam | 1 (PassRole) | 0 — absent from boundary | 0 — explicitly denied | 0 | 0 |
| Total | ~136 | 44 | 44 | 44 | 44 |
Four readings of that number
136 → 44
Two layers the application team never wrote removed 92 of 136 actions. Reviewing the identity policy alone would have overstated the grant by 3×.
Silently reversible
Detach the boundary and the role jumps back to ~136 with no error, no alert, and no change to the document anyone is reading. This is why boundaries are enforced by an SCP, not by convention.
The two zeros
Secrets Manager and PassRole are the
most dangerous grants in the list and both are blocked — by accident of the boundary's design, not by
anyone deciding. Working, but not by intent.
Still 40 too many
The service reads one prefix. Forty S3 read and list actions across every in-region bucket is not least privilege — it is a much better ceiling. Fixing that is a Layer 6 job, and nobody else can do it for you.
The debugging skill this builds. When a request fails, the question is never "what does my policy say?". It is "which of the seven layers said no?". They fail distinguishably:
| Symptom | Likely layer | How to confirm |
|---|---|---|
| Fails in one region, works in another | SCP with a region condition | Compare the same call across regions; check OU policies |
| Fails only for external principals; internal works | RCP | The error is deliberately less verbose for out-of-org callers |
| Worked yesterday, same policy today | a boundary or SCP was attached | CloudTrail on PutRolePermissionsBoundary / AttachPolicy |
| Works from the console, fails from a script | session policy, or a different role entirely | aws sts get-caller-identity in both places — do this first, always |
AssumeRole itself fails | trust policy | Read the target role's trust policy, not its permissions |
| KMS-encrypted object unreadable despite S3 access | the KMS key policy | S3 permission and key permission are separate grants |
The tool that does this for you is the IAM Policy Simulator, and the tool that finds the problem before it happens is IAM Access Analyzer. Both are in the lab below.
Three grounded examples
AWS added a Generative AI Lens to the Well-Architected Framework, and its first security best practice is GENSEC01-BP01, "Grant least privilege access to foundation model endpoints". Its recommendation is notably specific: limit access to foundation-model endpoints to IAM roles, and use session durations and permissions boundaries to further constrain them — plus PrivateLink so the traffic never traverses the internet.
Two things worth extracting. First, AWS is explicitly recommending the boundary mechanism you just computed through, for AI workloads specifically — which is unusual; boundaries are normally pitched as a delegation tool. Second, "session durations" is doing real work: a model endpoint invoked by a long-running service is a standing capability unless the session is short.
AWS Well-Architected Generative AI Lens, GENSEC01-BP01 — docs.aws.amazon.com.
IAM Access Analyzer's newer internal access findings are genuinely impressive engineering: they use automated reasoning to evaluate identity policies, resource policies, SCPs, RCPs and permissions boundaries together, and answer "who inside my organization can reach this S3 bucket, DynamoDB table or RDS snapshot?" — the exact question the intersection above required by hand. There is a scale limit worth knowing: internal access findings are not generated for organizations containing more than 70,000 principals.
The honest caveat, and the reason least-privilege programmes stall: Access Analyzer finds unused access; it does not remove it. Industry analysis through 2026 keeps landing on the same point — the bottleneck is not visibility but safe, scalable action, because a role that looks dormant may support something rare, undocumented or quarterly. Set against the finding that 78% of organizations have a role unused for 90+ days, the picture is clear: everyone can see the problem and almost nobody can safely act on it at scale.
The practical response is to make removal cheap rather than heroic: scope analyzers to a subset of accounts and roles so the finding list is reviewable, use the guided-revocation and policy-generation features rather than hand-editing, and treat "this role has been unused for 90 days" as a ticket with an owner, not a dashboard number.
AWS, IAM Access Analyzer findings — docs.aws.amazon.com.
Within twelve months AWS did two things that quietly invalidate a large amount of published material, and both bite exactly the kind of study you are doing now.
Security Hub. In June 2025 the existing service was renamed Security Hub CSPM and a new unified service took the bare name, going GA on 2 December 2025. Preview users had to opt into the GA experience by 15 January 2026 or have it disabled organization-wide, with configurations removed. So there are now three eras of "Security Hub" documentation in circulation, and the console shows two entries.
Inspector Classic reached end of support on 20 May 2026. Any walkthrough using the Classic console cannot be followed.
The transferable habit: check the date on any AWS security document before trusting a service name. For a certification in particular, the exam tracks the current naming and the material does not, so where the two disagree, the docs win.
4 · Apply it 10%
Your GPU inference service, as an AWS identity design
Everything you have built runs on GCP or Modal, so the useful exercise is translation: what would the same architecture look like here, and which decisions would AWS force you to make explicitly that GCP made for you?
| Your component | AWS shape | The identity decision AWS makes you make |
|---|---|---|
| vLLM serving Gemma on a GPU node | EC2 (G, P4d/P5) or EKS pod | Which role, and how attached? Instance profile on EC2; Pod Identity association on EKS. Not the node role — giving pods the node's role is the standard EKS mistake and hands every pod the kubelet's permissions. |
| Reading model weights from object storage | s3:GetObject on one prefix | Exactly which ARN? arn:aws:s3:::weights/gemma-2b/*, not weights/* and certainly not *. If the bucket is KMS-encrypted you need kms:Decrypt on the key as a separate grant — the single most common "but I gave it S3 access" failure. |
| Autoscaling 1–3 on spot capacity | ASG or Karpenter with spot | Who may launch instances, and pass which role? iam:PassRole is the escalation primitive here: whoever can pass a role can run code as it. Constrain it to specific role ARNs, always. |
Your gcloud-equivalentdeploy script | a CI pipeline | Federation, not keys. GitHub Actions to AWS via OIDC, with the subject pattern pinned to your repository and branch. Long-lived deploy keys in CI secrets are the credential most likely to leak in your whole setup. |
| KServe / Kubernetes familiarity | EKS | Two identity systems, deliberately aligned. Kubernetes RBAC governs the cluster API; IAM governs AWS. Pod Identity is the bridge, and its automatic session tags let one IAM role serve many namespaces via ABAC instead of one role per namespace. |
| Hugging Face token for gated weights | Secrets Manager | The one legitimate secret. Third-party, cannot consume AWS identity — branch 4 of the Session 1 tree. Store it, fetch it at runtime with the pod's own identity, rotate it. Never bake it into the image. |
The open-source angle. Your preference for self-hosting is a security preference too, and worth naming: self-hosted vLLM on EC2 or EKS puts the whole model-serving path inside identities you control, whereas a hosted endpoint moves part of the trust to the provider's service principal. Neither is better in general. But they place the shared-responsibility line from C1 in different places, and the AWS Generative AI Lens is written mostly for the second case — worth remembering when applying its guidance to a self-hosted stack.
Optional lab — read-only IAM archaeology cost: $0.00
Everything here is free. IAM API calls cost nothing, the Policy Simulator costs nothing, and the lab creates no resources. It is read-only by design: you will not enable a single billable service.
Set a budget alert anyway. Billing → Budgets → create a zero-spend or $1 monthly budget with an email alert. Costs nothing, takes two minutes, and it is the habit that saves you later when a lab does touch something billable. Do not enable GuardDuty, Inspector, Macie, Config, either Security Hub, or Access Analyzer's unused-access analyzer — all are billable and easy to switch on by accident while exploring. The external-access analyzer is free and is the only one this lab uses.
- Establish who you are.
aws sts get-caller-identity. This should become reflex — it answers "which principal am I actually?" and resolves a surprising share of permission mysteries on its own. - Inventory the identities.
aws iam list-usersandaws iam list-roles. Note the ratio. If users outnumber roles in a modern account, that is itself a finding. - Find the wildcards. Pick one role and run
aws iam list-attached-role-policies --role-name NAME, thenaws iam get-policy-versionon each. Search the JSON for"*"inActionandResource. Write down what you find; do not change anything yet. - Check for a boundary.
aws iam get-role --role-name NAMEand look forPermissionsBoundary. If it is absent, the role's nominal permissions are its effective permissions — there is no ceiling. - See what is actually used. In the console, open the role's Access Advisor tab. It shows service-level last-accessed data. The gap between "granted" and "last accessed" is your privilege creep, measured.
- Simulate. Open the IAM Policy Simulator, pick the role, and test a deliberately over-broad action such
as
iam:CreateUserors3:DeleteBucket. Note that the simulator reports which policy type produced the decision — that is the seven-layer diagram, live. - Optional, free: create an external-access analyzer in IAM Access Analyzer with your account as the zone of trust. It costs nothing and will list every resource shared outside it. Expect at least one surprise.
Teardown
- Nothing billable was created, so there is nothing to delete for cost reasons.
- If you created the external-access analyzer and want a clean account:
aws accessanalyzer list-analyzersthenaws accessanalyzer delete-analyzer --analyzer-name NAME. It is free to leave running and genuinely useful, so leaving it is a reasonable choice. - Verify nothing billable got switched on:
aws guardduty list-detectors,aws securityhub describe-hub, andaws configservice describe-configuration-recordersshould each return empty or an error. Run these in every region you clicked into — these services are regional, and the console makes it easy to enable one in a region you then never revisit. That is the classic silent AWS bill. - Keep the budget alert. It costs nothing and it is the cheapest insurance in cloud.
Azure splits what AWS unified: identity lives in Entra, resources live in ARM, and the two have separate role systems with confusingly similar names.
Next: Entra roles versus Azure RBAC, scope inheritance, managed identities, Conditional Access as a policy engine, and a Key Vault default that changed this year.
1 · Why this session exists 10%
Azure has two access-control systems. They have similar names, overlapping vocabulary, separate administration, and they govern completely different things. Almost every Azure identity mistake — and every hour lost to one — traces back to confusing them.
Microsoft Entra ID is the directory: it holds identities and controls the directory itself. Azure RBAC is the resource authorization system: it controls what you may do to subscriptions, resource groups and resources. Both call their units "roles". Neither can grant the other's permissions.
A Global Administrator can toggle a setting called "Access management for Azure resources", which grants themselves the User Access Administrator role at the tenant root scope — and from there, access to every subscription. So the two planes are separable in normal operation and joinable by one switch. That switch is the single most privileged action in an Azure tenant, it is logged, and it should be alerted on.
2 · Core concepts 50%
The principals Entra holds
Entra's object model is unusual, and it is worth getting straight before anything else because the names are genuinely counter-intuitive.
An application registration is the definition of an application — its blueprint. It lives in the tenant where the application was authored, and it says what the app is, what permissions it wants, and how it authenticates.
A service principal is the local instance of that application in a particular tenant — the thing that actually gets permissions and signs in. One registration can have service principals in many tenants; that is how multi-tenant SaaS works.
The analogy: the registration is the recipe, the service principal is the meal on your table. You grant permissions to the meal, not the recipe.
This matters because every non-human identity in Azure is ultimately a service principal. Managed identities are service principals with the credential handling removed. And as of 2026, the new agent identities are also built on the service principal object — Microsoft extended the most elastic object in the directory rather than inventing a new primitive.
Where AWS writes a JSON document, Azure composes a role assignment from three parts. That is the whole model:
# role assignment = WHO + WHAT + WHERE
az role assignment create \
--assignee <object-id of user, group, SP or managed identity> # WHO
--role "Storage Blob Data Reader" # WHAT (role definition)
--scope "/subscriptions/<sub>/resourceGroups/rg-inference/providers/Microsoft.Storage/storageAccounts/weights"
Compare with AWS. The action lives in the role definition (a named, reusable bundle). The resource is not written at all — it is implied by the scope. That is why Azure has almost no resource-based policies: the scope of the assignment already answers "on what".
Scope is a path down the C1 hierarchy, and assignments inherit downward and accumulate. An assignment at a management group applies to every subscription beneath it. Effective permissions are the union of every assignment at every level above the resource — which is why an unexpected permission usually turns out to have been granted three levels up by someone else.
- Additive by default. Like GCP and unlike AWS, ordinary Azure RBAC has no "deny" you can write. More assignments only ever mean more access. You reduce access by removing assignments or by assigning at a narrower scope — not by adding a restriction.
- Deny assignments exist but you mostly cannot create them. They are primarily generated by Azure Blueprints and Managed Applications. Treat them as something you will encounter and be confused by, not something you will sources. When a subscription Owner cannot delete a resource, a deny assignment is usually why.
- ABAC conditions are the supported way to narrow a broad role — for example, granting Storage Blob Data Reader but only for blobs carrying a particular index tag. Support is real but not universal across services; check per service before designing around it.
- Quotas that bite at scale: roughly 4,000 role assignments per subscription and 5,000 per management group, and 5,000 custom role definitions per tenant. Groups are the escape hatch — assign to a group once rather than to two hundred users — and the sooner you adopt that habit the less painful the migration.
- Control plane and data plane are separate grants. Contributor on a storage account lets you manage the account and, notably, read its access keys — but the clean way to read a blob is the Storage Blob Data Reader data-plane role. Any role name containing "Data" is a data-plane role, and that naming convention is one of the more useful things to memorise.
Managed identities — the mechanism the material barely mentions
§7 is strong on users, groups, Conditional Access, MFA, PIM and B2B. Managed identities get a single passing mention across the whole section — despite being the central answer to "how does an Azure workload authenticate?". This section is taught from Microsoft Learn rather than the material.
A managed identity is a service principal that Azure creates, credentials, rotates and deletes on your behalf. You never see a secret because there is never a secret for you to see. It is the direct answer to Session 1's secret-zero problem, and it is the single most important thing in this session for your work.
Two flavours, and the choice is more consequential than it looks:
System-assigned
Created with the resource, tied to it one-to-one, deleted with it. Lifecycle is automatic and you cannot get it wrong.
Use for: a single workload with its own permissions. The default choice.
Trap: delete and recreate the VM and you get a new object ID, so every role assignment must be redone. This breaks infrastructure-as-code redeploys in a way that is deeply confusing the first time.
User-assigned
A standalone resource with its own lifecycle. Attach it to many resources; it survives them all.
Use for: a fleet sharing one permission set, or anywhere the identity must outlive the compute — which is most real infrastructure-as-code.
Trap: it survives everything, including the workload it was created for. Orphaned user-assigned identities with live role assignments are a genuine finding class.
Identical in shape to the AWS instance profile and the GCP attached service account, which is the point:
- You enable a managed identity on a resource — VM, App Service, Function, AKS pod, Container App. Azure creates the service principal in Entra.
- You give that principal role assignments at the scopes it needs.
- At runtime the code calls the Instance Metadata Service at
169.254.169.254(or an injected local endpoint) and receives an access token for the target resource. The Azure SDK'sDefaultAzureCredentialdoes this automatically, which is why correctly written Azure code often contains no credential handling at all. - Tokens are short-lived and refreshed transparently.
Managed identities are exempt from MFA and from most Conditional Access. They are workload identities; interactive proof is meaningless for them. This is correct design, and it means human-identity controls give you no protection here — the controls that do are scope discipline, Conditional Access for workload identities (a separate, licensed capability), and short token lifetimes.
The metadata endpoint is reachable from any process on the host. Same caveat as every cloud: managed identity removes the stored secret, not the runtime abuse path.
Managed identities only work for compute inside Azure. For everything else — GitHub Actions, a workload on GCP or AWS, an on-prem Kubernetes cluster — Azure offers workload identity federation: configure a federated credential on a user-assigned managed identity or app registration, naming the external issuer, subject and audience, and the external token is exchanged for an Entra token. Same three-constraint model as Session 1; same warning about subject patterns.
For AKS specifically this is Microsoft Entra Workload ID: the cluster acts as an OIDC issuer, a Kubernetes service account is annotated with a client ID, and the pod's projected token is exchanged for an Entra token. It replaced the older pod-identity approach, which worked by intercepting metadata traffic — a design that had the same drawbacks as AWS's pre-IRSA world. If you know IRSA, you already know this.
Microsoft Learn, Workload identities — learn.microsoft.com.
Agent identities: a third category 2026
Microsoft Entra Agent ID is now generally available and makes AI agents a first-class identity type alongside human users and workload identities. Given your work, this is worth knowing about even if you never touch Azure in production — it is the clearest statement yet of how a major vendor thinks agent identity should work.
Agent identity
A specialised service principal representing one agent. Notably, it holds no credentials of its own — an agent identity blueprint acquires tokens on its behalf, and the agent identity is the subject of the resulting token.
Blueprints
Reusable templates defining permissions and governance for a class of agents, in a parent–child relationship. Approve once; every agent created from it inherits the settings. This is aimed squarely at the "every agent gets admin because nobody wants to configure permissions" failure.
Human sponsorship
Agents have owners, sponsors and managers as distinct administrative relationships, plus dedicated directory roles (Agent ID Administrator, Agent ID Developer, Agent Registry Administrator). Every agent traces back to an accountable human.
The distinction Microsoft draws is worth repeating because it is the substantive argument, not marketing: application identities were designed for services with long-term stability, known ownership and a managed lifecycle. Agents are created and destroyed quickly, act autonomously, and make dynamic decisions about which tools to call — so their permission needs cannot be fully known at design time. There are also Conditional Access templates specifically for blocking high-risk agent identities and for autonomous agents operating without user context.
Two practical notes. It supports OAuth 2.0, MCP and A2A, and works with agents built elsewhere — including on AWS Bedrock — via an auth SDK sidecar or workload identity federation. And the full governance product around it, Microsoft Agent 365, is separately licensed, so "Agent ID" and "the whole agent governance suite" are not the same purchase.
Microsoft Learn, What is Microsoft Entra Agent ID? — learn.microsoft.com.
Conditional Access: the policy engine humans meet
Conditional Access is Azure's answer to "authentication should not be a single yes/no". It is an if-then engine evaluated at sign-in: if these signals hold, then require these controls or block.
It is the piece with no clean AWS equivalent, and the reason Azure's identity story feels different. AWS decides authorization from policy documents about actions and resources; Azure additionally decides authentication strength from context — who, from where, on what device, how risky does this look.
Every policy is signals → decision:
Signals in
User or group membership · target application · device state and compliance · location or named IP range · client app type · real-time and aggregate risk from Identity Protection.
Decisions out
Block · grant with MFA · grant only from a compliant or hybrid-joined device · require an approved client app · require a specific authentication strength such as phishing-resistant · force a limited or short session.
The break-glass rule. Always exclude at least two emergency-access accounts from every Conditional Access policy, store their credentials offline, and monitor their use. A policy requiring a compliant device, authored on a non-compliant device, locks the material out of the tenant that would let them fix it. This is the single most common self-inflicted Azure outage and it is entirely avoidable.
Microsoft has made MFA mandatory for Azure, in phases, and as of this month the transition is complete.
- Phase 1 — MFA to sign in to the Azure portal, Entra admin center and Intune admin center. Reached 100% of tenants in March 2025.
- Phase 2 — MFA for resource-management operations through any client: Azure CLI, PowerShell, REST APIs, SDKs, the mobile app, and IaC tools including Terraform and Bicep, for any create, update or delete. Began 1 October 2025, rolled out via Azure Policy.
- Postponement was available for complex environments and expired 1 July 2026. There are no further extensions.
The teaching point: workload identities — managed identities and service principals — are exempt. This is not an oversight; it is the human/workload distinction from Session 1 showing up as product policy. If your CI pipeline authenticates as a service principal, nothing changed for it. If your pipeline authenticates as a human's account, it broke, and the fix is to give it a workload identity rather than to seek an exemption. Relatedly, the OAuth 2.0 resource-owner-password-credentials flow is incompatible with the requirement and is deprecated.
Microsoft Learn, Plan for mandatory MFA — learn.microsoft.com Azure blog.
Privileged Identity Management (PIM) is the companion control and the one the material does cover. It makes privileged roles eligible rather than active: you hold no standing permission, you activate for a bounded window with justification, approval and MFA, and it expires on its own. This is the structural fix for the "permanent temporary" privilege-creep door from Session 1 — the grant expires without anyone remembering to remove it. It works for both Entra roles and Azure RBAC roles, and it requires Entra ID P2 licensing.
Key Vault, and a default that changed this year
Azure Key Vault stores three kinds of thing, and the distinction matters: secrets (arbitrary strings — passwords, connection strings, API keys), keys (cryptographic keys, where the vault performs sign, verify, wrap and unwrap operations internally so the key never leaves), and certificates.
The intended pattern is the one you now recognise: the application holds no secrets, authenticates with its managed identity, and fetches what it needs at runtime. Key Vault plus managed identity is the Azure answer to secret zero.
Key Vault has always had two ways to authorize data-plane access, and Microsoft has now changed which one you get by default.
- Access policies — the legacy model, native to Key Vault. Permissions are assigned per principal at vault scope, in a list on the vault itself.
- Azure RBAC — the recommended model, and the default for new vaults from API version
2026-02-01. Uses ordinary role assignments: Key Vault Secrets User, Key Vault Crypto Officer, and so on.
Two consequences you will hit. Existing vaults do not migrate themselves — they keep
whatever enableRbacAuthorization setting they have. So a subscription can easily contain new
IaC-deployed vaults on RBAC and older vaults on access policies, behaving differently, with no visual
indication. And control-plane API versions older than 2026-02-01 retire on 27 February
2027, so ARM templates, Bicep, Terraform providers and SDK calls that create or manage vaults need
updating regardless of which authorization model you choose.
Microsoft Learn, Azure RBAC vs access policies — learn.microsoft.com Prepare for API version 2026-02-01.
The security argument for the change is not cosmetic, and it is the subject of this session's
worked example below. Under the access-policy model, anyone holding a role with
Microsoft.KeyVault/vaults/write — which includes plain Contributor — can edit the
vault's access policy list and thereby grant themselves data-plane access to every secret in it. Under
RBAC, permission management is restricted to Owner and User Access Administrator, so managing the vault and
reading its contents are genuinely separate privileges.
Azure Policy is not RBAC
The last big Azure distinction, and the one that maps least cleanly onto AWS.
Azure RBAC
Governs who may perform an operation. If you lack the role, the call is refused.
Azure Policy
Governs what resources may look like. You may have every permission and still be refused because the resource you are creating violates a rule — wrong region, missing tag, public network access enabled.
The capability that has no AWS analogue is that Azure Policy does not only refuse. Its effects
include deny, audit, modify (silently fix the request as it passes) and
deployIfNotExists (create a missing companion resource, such as attaching diagnostic settings to
every new resource automatically). An SCP can only subtract; Azure Policy can subtract, correct and add. That
makes it the natural home for "every storage account must have secure transfer on" — and, aptly, it is the
mechanism Microsoft used to roll out mandatory MFA enforcement.
Where they meet: both attach at management group, subscription or resource group scope and inherit downward. A policy at a management group is Azure's closest structural equivalent to an AWS SCP, with the important caveat that a subscription Owner can usually create an exemption, whereas an SCP cannot be escaped from inside the account.
Detection and posture: Defender for Cloud and Sentinel
| Service | What it is | What it answers | Billable? |
|---|---|---|---|
| Defender for Cloud — free tier | CSPM baseline | Secure Score, recommendations, regulatory compliance view. Turned on by default on subscriptions. | free |
| Defender for Cloud — plans | workload protection | Per-workload threat detection: Defender for Servers, for Storage, for Containers, for Key Vault (anomalous access patterns), for Databases. Each plan is priced separately. | yes, per plan per resource |
| Microsoft Sentinel | cloud-native SIEM | Correlate across Azure, Entra, Microsoft 365 and third-party sources; hunt, and automate response. The material covers this well. | yes, per GB ingested |
| Entra ID Protection | identity risk engine | Is this sign-in or this user risky? Feeds risk signals into Conditional Access. | P2 licence |
| Entra Permissions Management | CIEM | Which permissions are granted but unused, across Azure, AWS and GCP? Azure's answer to Access Analyzer. | yes |
| Activity Log / sign-in logs | audit | Who did what to resources; who signed in and how. Retention beyond the default window means shipping to Log Analytics, which is billable. | basic retention free |
Cost warning for the lab Entra ID, Azure RBAC, managed identities and Azure Policy are free. Every Defender for Cloud plan, Sentinel ingestion, and Log Analytics retention are not, and Defender plans are enabled per-subscription with a single toggle that is easy to hit while exploring. The lab below stays entirely in the free surface.
Decision tree: which Azure control fits this requirement?
deny to prevent, audit to measure first, and modify or
deployIfNotExists when you want it corrected rather than blocked.The three clouds, from the Azure side
| Azure concept | AWS equivalent | GCP equivalent | Key quirk when translating |
|---|---|---|---|
| Entra role | IAM permissions on iam:*, roughly | Cloud Identity admin roles | AWS and GCP do not split the directory from resources this sharply. The Azure split is real and constant. |
| Azure RBAC role assignment | identity policy attached to a role | binding in an allow policy | Azure names the resource once, in the scope. AWS names it in every statement. |
| Scope | the Resource ARN | the node the binding is attached to | Azure and GCP both attach at a hierarchy node and inherit down. AWS identity policies do not inherit at all. |
| Service principal | IAM role (for services) | service account | All three are "an identity for a program". Only AWS makes assumption an explicit, visible act. |
| Managed identity | instance profile / Pod Identity | attached service account | Same mechanism, same metadata endpoint. Azure's system- vs user-assigned split has no exact counterpart. |
| Conditional Access | no equivalent | Context-Aware Access / Access Context Manager | Google's BeyondCorp lineage is the nearest thing. AWS has nothing comparable at authentication time. |
| PIM | role session + approval workflow you build | Privileged Access Manager | Azure's is the most mature and the most licence-gated. On AWS you assemble it yourself. |
| Azure Policy | SCP + Config rules, combined | org policy constraints + custom constraints | Only Azure Policy remediates. Its modify and deployIfNotExists effects have no AWS or GCP equivalent. |
| Deny assignment | Effect: Deny anywhere | deny policy | On AWS anyone can write a deny; on Azure almost nobody can. Do not design around authoring them. |
| Key Vault | Secrets Manager + KMS | Secret Manager + Cloud KMS | Azure merges secrets and keys into one service; the other two keep them apart. |
| Defender for Cloud | Security Hub + GuardDuty + Inspector | Security Command Center | Azure and GCP each sell one product with tiers; AWS sells four services you assemble. |
| Entra Agent ID | an IAM role per agent | agent identities and pools | Microsoft and Google both shipped dedicated constructs in 2026. AWS has not, so far. |
3 · Reality check 25%
Worked example: counting a privilege-escalation path
This is not a hypothetical. It is the concrete security argument behind Microsoft flipping the Key Vault default in February 2026, and it is worth walking because it shows how privilege creep produces escalation without anyone granting anything obviously wrong.
Setup. A perfectly ordinary arrangement. One resource group, rg-inference, containing a
Key Vault and the workloads that use it. A platform engineer holds Contributor at resource-group scope,
because they need to deploy and manage the infrastructure. The vault uses the access policy model,
because it was created in 2024 and nobody has revisited it.
What Contributor is supposed to mean. Manage resources; do not manage access. Contributor explicitly
excludes Microsoft.Authorization/*/write, so the engineer cannot create role assignments. That is
the intended ceiling, and on paper it holds.
What Contributor actually permits here:
| # | Step | Permission used | Blocked? |
|---|---|---|---|
| 1 | Read the vault's configuration | Microsoft.KeyVault/vaults/read | no — Contributor has it |
| 2 | Edit the vault's access policy list, adding themselves with Get and List on secrets | Microsoft.KeyVault/vaults/write | no — and this is the hinge. The access policy list is part of the vault resource, so editing it is a resource write, not an authorization write. |
| 3 | Read every secret in the vault | data plane, now granted by step 2 | no |
| 4 | Use those secrets — database passwords, third-party API keys, signing material | whatever the secrets unlock | no, and now outside Azure's control entirely |
Counting it
1 step
From "manage infrastructure" to "read all secrets". No exploit, no misconfiguration, no vulnerability — just the documented behaviour of two features combining.
0 alerts
By default. Editing an access policy is an ordinary resource write on the Activity Log. Without Defender for Key Vault or a targeted alert rule, nothing fires.
× every vault
The engineer holds Contributor at resource-group scope, so this applies to every access-policy vault in the group. Held at subscription scope, it is every such vault in the subscription.
Blocked under RBAC
Under the RBAC model, granting data-plane
access requires Microsoft.Authorization/roleAssignments/write — Owner or User Access
Administrator only. Contributor cannot self-grant. Step 2 fails.
The generalisable lesson, which is bigger than Key Vault. A permission boundary only holds if the two sides of it cannot be bridged by a third permission. Here, "manage the resource" and "access the resource's data" were meant to be separate, and one field inside the resource object joined them. When you evaluate any access model, the question to ask is not "what does this role grant?" but "what can this role grant itself?" — and that question is exactly what the AWS escalation-action checklist in Session 2 was drilling too. Same idea, different vocabulary.
What to do about it, in order: migrate vaults to the RBAC model; if you cannot yet, limit
who holds Contributor on resource groups containing access-policy vaults; enable purge protection and soft delete
so destructive actions are recoverable; and alert on SecretPurge, KeyPurge and
VaultDelete unconditionally, since purge operations bypass soft-delete recovery.
Three grounded examples
Microsoft's guidance is unambiguous: use RBAC rather than access policies, because RBAC restricts permission
management to Owner and User Access Administrator and thereby separates security from administration —
whereas under access policies, anyone with Microsoft.KeyVault/vaults/write can grant themselves
data-plane access.
The operational trap is in the rollout. From API version 2026-02-01, new vaults default
to RBAC — but existing vaults keep their current model until you explicitly change
enableRbacAuthorization. So the realistic state of a mature subscription right now is a mix:
recently deployed vaults on RBAC, older ones on access policies, identical in the portal, behaving differently
under audit. Separately, control-plane API versions before 2026-02-01 retire on 27 February
2027, which forces every Bicep, ARM and Terraform definition to be touched anyway. That is the moment to
migrate the model too, rather than doing two migrations.
Microsoft Learn — Azure RBAC vs access policies.
Azure's mandatory MFA rollout is the clearest real-world illustration of Session 1's central distinction. MFA is a human-identity control, so Microsoft applied it to humans and exempted workload identities entirely — managed identities and service principals are unaffected.
The practical fallout was predictable and instructive. Pipelines that authenticated as a service principal sailed through Phase 2. Pipelines that authenticated as a person's account — because someone once put their own credentials in a CI secret and it worked — broke on 1 October 2025. Microsoft's answer was not an exemption process; it was that those pipelines had the wrong identity type in the first place. Note also that Microsoft states MFA is required for break-glass accounts too, which is why those accounts should use phishing-resistant methods such as FIDO2 rather than being treated as an exception.
If you are asked in an interview why service accounts do not get MFA, this is the answer with a citation attached: the vendor mandating MFA across an entire cloud deliberately carved workload identities out of it.
Microsoft Learn, Plan for mandatory MFA — learn.microsoft.com.
Entra Agent ID is worth studying as a design artefact even if you never deploy on Azure, because it is the most complete public answer to a question your field is about to face everywhere: what does governance look like when identities are created and destroyed at the speed of a deployment loop and act autonomously?
Microsoft's answers are specific. Agent identities carry no credentials of their own — a blueprint acquires tokens on their behalf, which means there is no agent secret to steal. Every agent has an accountable human as owner, sponsor or manager. Blueprints let permissions be approved once per class of agent rather than negotiated per instance. And existing Entra machinery — Conditional Access, entitlement management, access reviews, lifecycle workflows — extends to agents without a new policy engine, which is the genuinely clever part: the governance you already run applies unchanged.
Set against the Session 1 finding that 70% of organizations give AI systems more access than an equivalent human employee, the blueprint idea is aimed precisely at the cause — permissions granted broadly because configuring them per agent was too much work. Google shipped agent identities and agent identity pools in the same period; Session 4 covers those. Two of the three hyperscalers converging on the same construct within a year is a strong signal about where this is going.
Microsoft Learn, What are agent identities? — learn.microsoft.com.
4 · Apply it 10%
Your stack, translated to Azure
Your goal is working Azure literacy rather than production depth, so the useful thing here is a reliable translation table plus the two or three places where the Azure model would genuinely change your design.
| What you know | Azure equivalent | What changes in the identity design |
|---|---|---|
| GKE + KServe | AKS + KServe or Azure ML endpoints | Entra Workload ID. Annotate the Kubernetes service account with a managed identity's client ID; the cluster issues OIDC tokens exchanged for Entra tokens. Structurally identical to Workload Identity Federation for GKE, so your existing mental model transfers directly. |
| Vertex AI endpoint | Azure ML online endpoint or Azure AI Foundry | Endpoints support system- or user-assigned managed identity. Choose user-assigned if you redeploy from IaC — a system-assigned identity gets a new object ID on recreate and silently invalidates every role assignment. |
| Weights in GCS | Blob Storage | Grant Storage Blob Data Reader at container scope, not Contributor at account scope. Contributor on a storage account lets the holder read the account keys, which is a full data-plane bypass wearing a control-plane name. |
| Spot GPU nodes, autoscale 1–3 | NC / ND-series spot VMs in an AKS node pool | Identity is per-pod, not per-node, so scaling changes nothing about permissions — which is the point. Avoid granting the node pool's kubelet identity anything your pods need; that is the AKS version of the EKS node-role mistake. |
| Hugging Face token | Key Vault secret | Fetch at runtime with the pod's managed identity. Check whether the vault is on RBAC or access policies before designing the grant — the answer changes which role you assign and whether Contributor is a back door. |
Your gcloud script,run locally | az CLI | Phase 2 MFA applies to you. Any create, update or delete from the CLI now requires MFA on the interactive account. If you script deployments, give the script a service principal or federated workload identity rather than reusing your own login — the change that broke other people's pipelines is the change that fixes your design. |
Optional lab — map the two planes cost: $0.00
Read-only, on the free surface of Entra and RBAC. It creates nothing and enables nothing billable.
Set a budget alert first: Cost Management → Budgets → a $1 monthly budget with an email alert. Then, explicitly: do not enable any Microsoft Defender for Cloud plan, do not connect a Log Analytics workspace, and do not enable Sentinel. The Defender overview page invites you to enable plans with one click and they bill per protected resource per hour. The free Defender CSPM tier and Secure Score are already on and cost nothing — those you may look at freely.
A free trial subscription is fine for this. Note from C1 that the Azure trial credit is $200 for 30 days, and that GPU SKUs are typically blocked on trial — irrelevant here, since this lab deploys no compute at all.
- Confirm which plane you are in.
az account showgives you the subscription and tenant. Note both IDs — conflating them is a common source of confusion in scripts. - List your resource-plane permissions.
az role assignment list --assignee <your-upn> --all --output table. Look at thescopecolumn: how many are at subscription scope or higher? Every one of those is inherited by everything beneath it. - Now look at the other plane. In the portal, Microsoft Entra ID → Roles and administrators, and check what directory roles you hold. Compare the two lists. They will overlap very little, and that gap is the two-plane model.
- Inspect a built-in role's actual contents.
az role definition list --name "Contributor" --output json. Read thenotActionsarray — that is whereMicrosoft.Authorization/*/writeis excluded, and seeing the exclusion written down makes the worked example above concrete. - Find the data-plane roles.
az role definition list --query "[?contains(roleName,'Data')].roleName" -o tsv. These are the ones to reach for instead of Contributor. - Check any Key Vault's model.
az keyvault list --query "[].{name:name, rbac:properties.enableRbacAuthorization}" -o table. Anything showingfalseor empty is on the legacy access-policy model and carries the escalation path above. - Look at inheritance. In the portal, open a resource group → Access control (IAM) → the Role assignments tab, and note the "Scope" column showing which assignments are inherited rather than direct. This is the union rule from the concepts section, rendered as a list.
Teardown
- Nothing was created, so nothing needs deleting.
- Verify no Defender plan got enabled:
az security pricing list --query "value[?pricingTier=='Standard'].name" -o tsv. Empty output is what you want. Anything listed is billing right now — disable withaz security pricing create --name <NAME> --tier Free. - Check for a stray Log Analytics workspace:
az monitor log-analytics workspace list -o table. Ingestion is the usual source of unexpected Azure security spend. - If you deployed anything at all in an exploratory moment:
az group delete --name RG --yes. Two C1 gotchas still apply — deleting a resource group does not remove role assignments scoped elsewhere, and a soft-deleted Key Vault continues to occupy its name until purged withaz keyvault purge. - Keep the budget alert.
Google inverts the emphasis again: roles are bundles you bind to principals at a hierarchy node, inheritance is purely additive, and for years there was no way to subtract at all.
Next: allow, deny and principal access boundary policies; service accounts and the impersonation chain; three confusingly similar federations; and the Vertex AI service agent that has been quietly over-permissioned in your own project.
1 · Why this session exists 10%
Google Cloud's access model is the simplest of the three to describe and the easiest to get quietly, permanently wrong — because for most of its life it could only add.
An allow policy binds roles to principals at a node in the C1 hierarchy, and every descendant inherits it. There is no subtraction. A grant made at the organization node cannot be narrowed at a folder, cannot be narrowed at a project, and cannot be narrowed at the resource. Effective access is the union of every binding on every ancestor, all the way up.
That property is elegant when the tree is clean and unforgiving when it is not. Someone grants
roles/editor at the organization node in year one because it unblocks a migration. Four years later
every project ever created inherits it, nobody remembers, and no policy written lower down can take it away.
2 · Core concepts 50%
Principals, roles, bindings
Three nouns, and Google's vocabulary is the most consistent of the three clouds once you accept one historical wrinkle: what the docs now call a principal was called a member until recently, and your source uses both.
A principal is anything that can be named in a policy: a Google Account, a Google group, a whole
domain, a service account, an agent identity, or one of two special values —
allAuthenticatedUsers and allUsers. That last pair deserves a moment of alarm:
allUsers means literally the entire internet, unauthenticated, and it is one dropdown selection
away in the console.
A role is a named bundle of permissions. You never grant a permission directly on GCP — you grant
a role that contains it. Permissions are named service.resource.verb, as in
storage.objects.get.
A binding joins them: this role, to these principals, attached at this node. The set of bindings on a node is that node's allow policy.
Basic roles avoid
Owner, Editor, Viewer. They predate IAM itself and span thousands of permissions across every service, including services released long after the grant was made.
Editor can modify nearly everything in a project. Owner adds the ability to manage access and billing. Google's own guidance is to avoid them in production — and a free-trial project hands you Owner on day one, which is where most people's habits form.
Predefined roles
Google-maintained, service-specific, and the right default:
roles/storage.objectViewer, roles/aiplatform.user,
roles/secretmanager.secretAccessor.
Google updates them as services add permissions, which is convenient and means the grant you audited last year is not necessarily the grant you have today.
Custom roles
Your own permission list, defined at organization or project level.
The real cost is maintenance: Google will not add new permissions to your custom role when a service gains features, so custom roles silently rot. Reach for them when a predefined role is genuinely too broad and a condition cannot fix it — not by reflex.
# a binding, in the shape gcloud and Terraform both use
gcloud projects add-iam-policy-binding ml-serving \
--member="serviceAccount:vllm-inference@ml-serving.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer" \
--condition='expression=resource.name.startsWith("projects/_/buckets/weights/objects/gemma-2b/"),title=gemma-only'
Note where the resource lives. There is no resource field — the binding is attached to the project, so every bucket in it is in scope. Narrowing to one prefix requires the condition, written in Common Expression Language. On GCP, conditions are not an optimisation; they are frequently the only way to express least privilege at all.
- Roles are additive within a principal too. Holding both
roles/viewerandroles/storage.admingives you the union. There is no "most specific wins". - Groups count more than once. For the allow-policy principal limit, each appearance of a group in a binding counts separately — so a heavily used group consumes quota faster than expected.
- Conditions do not work everywhere. Not all services support all condition attributes, and an unsupported condition can behave differently from how it reads. Test rather than assume.
- Predefined roles drift. Because Google maintains them, a role can gain permissions between audits. This is a genuine argument for periodic re-review rather than one-time design.
- Two roles are escalation primitives and deserve the same suspicion as
iam:PassRoleon AWS:roles/iam.serviceAccountUser(attach a service account to a resource, i.e. run code as it) androles/iam.serviceAccountTokenCreator(mint tokens for it directly). Either one turns "can deploy" into "can act as anything that service account can act as".
Three policy types, not one
§11 teaches allow policies — members, roles, bindings — and that was the whole model when it was written. Google IAM now evaluates three policy types together. The two additions are precisely the subtraction mechanisms the model lacked.
Think of access as three gates in series, and a request must pass all three.
Allow policy — "is this principal granted the permission?" The gate that opens.
Deny policy — "is this principal explicitly refused it?" A veto that beats any allow, anywhere.
Principal access boundary (PAB) — "is this principal even eligible to touch resources in
this part of the estate?" A containment fence drawn around the principal, independent of what it has been
granted.
The PAB is the interesting one and the newest. It grants nothing. It answers a different question from "what may you do?" — namely "where are you allowed to be at all?". If your deployment service account is compromised, allow policies still describe what it can do; a PAB is what stops it from roaming across every folder in the organization while doing it.
Click each gate.
- It is fail-closed. If IAM cannot evaluate a PAB policy, access is denied. Google notes this commonly happens when newly created principals are still propagating — a security-positive default that makes identity lifecycle timing matter during rollouts and, awkwardly, during incident response.
- Multiple PABs can widen eligibility, not narrow it. This is the counter-intuitive one. A principal set may have up to 10 PAB policies bound to it, and binding a second one does not intersect with the first. Assuming "one more boundary means less access" is the classic PAB mistake.
- Enforcement versions matter. Each PAB declares which version of the permission set it enforces.
Google warns against blindly using
latest, since future changes could unexpectedly alter what is constrained. - It is not a substitute for least privilege. PAB contains access; it does not design it. A wildcard allow policy inside a PAB is still a wildcard.
- Structurally: PAB policies are always children of the organization, and are available in the IAM v3 API. Bindings attach to a principal set — all identities in a project, folder or workload identity pool — rather than to individuals, which is what makes them practical at scale.
The mental model that makes all three clouds line up: PAB is to GCP what a permissions boundary is to AWS, arriving roughly seven years later and operating on principal sets rather than single principals. Azure still has no real equivalent.
Google Cloud, IAM policy types — cloud.google.com Troubleshoot policies — docs.cloud.google.com.
Service accounts: identity and resource at the same time
A GCP service account is the strangest object in cloud identity, because it is simultaneously two things: it is a principal that can be granted roles, and it is a resource that other principals can be granted roles on.
That duality is the whole story. Granting someone a role on a service account is granting them the ability to become it — which means a service account's own permissions are only half of its risk. The other half is who is allowed to wear it.
Attached service account best
Attach it to a VM, Cloud Run service, Cloud Function or GKE node pool. The workload reads a token from the metadata server. No key exists.
Impersonation
A principal holding roles/iam.serviceAccountTokenCreator calls
generateAccessToken and receives a short-lived token for the service account. Excellent for
humans who need occasional elevated access — the action is logged and attributable to the
human, not just the service account.
Workload identity federation
An external OIDC or SAML token is exchanged for a GCP token. For CI and other clouds. No key.
Service account key file avoid
A downloadable JSON file containing a private key that never expires. It is the single most leaked credential type in Google Cloud, and it is precisely the thing every other option on this list exists to avoid. There is an organization policy constraint to forbid creating them; use it.
Google has named three separate features with nearly identical words. Getting these straight saves real time:
| Name | For | Mechanism |
|---|---|---|
| Workload Identity Federation | Workloads outside GCP: GitHub Actions, GitLab, AWS, Azure, on-prem | External token → workload identity pool → GCP token, optionally via service account impersonation |
| Workload Identity Federation for GKE | Pods inside GKE | Cluster metadata server; Kubernetes ServiceAccount maps to a GCP identity. Renamed in 2024 — it used to be "GKE Workload Identity" |
| Workforce Identity Federation | Human users from an external IdP | Okta, Entra ID or similar federating people into Google Cloud without Cloud Identity accounts |
For GKE specifically there are now two configuration styles, and the newer one is simpler.
Direct principal identifiers grant IAM roles straight to the Kubernetes ServiceAccount using a special
principal syntax — no GCP service account in the middle at all. Service account impersonation is the
older path: bind roles/iam.workloadIdentityUser with the member
serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]. Start with the direct style; fall back to
impersonation only when a service has limitations with federated tokens.
Two operational notes for your cluster work: it is always enabled on Autopilot, and on
Standard the node pool needs --workload-metadata=GKE_METADATA, which blocks the raw
169.254.169.254 endpoint and routes through the GKE metadata server instead. Enabling it on an
existing node pool requires recreating the nodes, so plan a maintenance window. If you know AWS IRSA, this
is the same idea with different nouns.
Google Cloud, Authenticate to Google Cloud APIs from GKE workloads — docs.cloud.google.com.
Organization policy constraints — not IAM at all
This is §12, not §11 — Exploring IAM, Project Organization, and Encryption, which is where the Organization Policy Service actually lives. If you read only §11 you would not meet it.
The distinction mirrors Azure's RBAC-versus-Policy split exactly, and it is worth holding in the same shape:
IAM
Governs who may do what. A principal question.
Organization policy
Governs what configurations are permitted, regardless of who is asking. A resource question. Set at organization, folder or project, and inherited downward.
The constraints that matter most for a GenAI estate, and which you should consider setting on day one:
iam.disableServiceAccountKeyCreation— forbids downloadable key files outright. The single highest-value constraint on this list.iam.allowedPolicyMemberDomains— domain-restricted sharing; prevents anyone granting access to an identity outside your organization. This is Google's answer to the AWS data-perimeter problem.storage.publicAccessPrevention— blocksallUsersandallAuthenticatedUserson buckets, closing the classic public-bucket failure at the platform level rather than by vigilance.compute.vmExternalIpAccess— restricts public IPs on VMs. Not identity, but it caps the exposure of anything that does hold an identity.- Custom constraints let you write your own rules against resource fields in CEL, which is the closest GCP gets to Azure Policy's expressiveness — though still without a remediation effect.
Security Command Center — the service the material never mentions
Security Command Center appears zero times across all 658 pages, yet it is GCP's central security product and on your session plan. Taught here from Google's documentation.
| Tier | What you get | Pricing |
|---|---|---|
| Standard | Asset inventory, basic vulnerability findings, Security Health Analytics baseline. Enough to answer "what do I have and what is obviously wrong?" | free — the only tier to touch in a lab |
| Premium | Event Threat Detection (cryptomining, anomalous IAM activity, suspicious API access), Container Threat Detection, extended detectors, compliance reports against CIS, PCI DSS, ISO 27001, NIST 800-53. | fixed subscription or pay-as-you-go per vCPU-hour; from 1 Jan 2026 PAYG also bills Cloud Run and AlloyDB |
| Enterprise ending | Premium plus the Google SecOps SIEM and Mandiant threat intelligence, with multi-cloud coverage of AWS and Azure. | subscription — shuts down 21 May 2027; organizations move to Premium automatically |
Cost warning IAM, service accounts, organization policies, Policy Troubleshooter and Cloud Audit Logs' Admin Activity stream are all free. SCC Premium and Enterprise are not, and activating a paid tier is an organization-level action that is awkward to reverse. Stay on Standard. Also note that Data Access audit logs are disabled by default and are billable when enabled — which matters more than it sounds, as the next section shows.
Vertex AI service agents — where your own work meets this stage
When you ran your Vertex AI deployment script, you chose a machine type, a GPU, a scaling range and a spot policy. You almost certainly did not choose an identity — and one was chosen for you.
Google provisions service agents: Google-managed service accounts, created automatically per project per product, that platform services act as on your behalf. The Vertex family's is often written as a P4SA — Per-Project, Per-Product Service Agent. It exists so that Vertex can read your training data, write your model artefacts and pull your containers without you wiring anything up.
That convenience is the problem. A service agent's permissions are sized for everything the product might need to do in any project, not for what your specific job does. Google's own documentation says it plainly: the default Vertex AI service agent has access to BigQuery and Cloud Storage, and if you want fewer permissions than that, you must supply your own service account.
The fix is bring your own service account: create a narrow user-managed service account and tell Vertex to run as it. Configurable for custom training jobs, hyperparameter tuning jobs, training pipelines, deployed models serving online predictions, and Ray on Vertex AI clusters.
# the identity your endpoint actually runs as — set it explicitly
gcloud iam service-accounts create vllm-serving --project=ml-serving
# exactly one grant: read one prefix of one bucket
gcloud storage buckets add-iam-policy-binding gs://model-weights \
--member="serviceAccount:vllm-serving@ml-serving.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer" \
--condition='expression=resource.name.startsWith("projects/_/buckets/model-weights/objects/gemma-2b/"),title=gemma-only'
# then pass --service-account=vllm-serving@... when deploying the model
Four caveats that are easy to trip over, all from Google's documentation:
- The default prediction-container service account has permission to read model artefacts at the URI in
AIP_STORAGE_URI— and Google explicitly says not to rely on it having any other permissions, because you cannot customise them. - You cannot use a custom service account to pull images from Artifact Registry. The default service agent is still used for image pulls. So BYOSA narrows the runtime identity, not every identity in the flow.
- For batch inference, the service agent is still used to reach BigQuery and Cloud Storage even when a custom service account is configured.
- There is a cap of 20 custom service accounts per project, per region, per service.
Google Cloud, Use a custom service account — docs.cloud.google.com.
Multiple independent research teams have published attack paths that turn over-permissioned Vertex service agents into project-wide access. The pattern is consistent enough to be worth treating as a class rather than a series of bugs:
- ModeLeak (Unit 42, November 2024). Two escalation paths in earlier Vertex components. One via a custom-code service agent that could list all service accounts and read and write all storage buckets; one via model exfiltration, where deploying a malicious model gave access to all fine-tuned models in the project's Cloud Storage. The full chain ran from a poisoned public model upload to exfiltration of proprietary fine-tuned adapter weights — LLM intellectual-property theft without ever touching the training infrastructure.
- XM Cyber's disclosures (January 2026) on Vertex AI Agent Engine and Ray on Vertex AI: users holding read-only Viewer permissions could extract service agent credentials from the instance metadata service and obtain project-wide access to Cloud Storage, BigQuery, Pub/Sub and Artifact Registry. Google classified both as "working as intended", with no remediation planned at the time.
- Unit 42's "Double Agents" (disclosed 31 March 2026): the P4SA provisioned by default for Vertex AI Agent Engine deployments carries permissions letting a compromised or misconfigured agent read every Cloud Storage bucket in the project, reach private container registries, and pivot laterally. The researchers note the chain requires no novel exploit — it consists entirely of querying the standard metadata service for credentials that were already there.
That last sentence is the one to carry. This is the Session 1 warning about metadata endpoints arriving in your exact domain: workload identity removes credential theft-at-rest and does nothing about credential abuse-at-runtime. Any code executing in the agent runtime — a compromised dependency, an injected prompt that reaches a tool, your own bug — can ask the metadata server for the token, and the platform will hand it over, because from the platform's point of view the request is entirely legitimate.
Cloud Security Alliance research notes on Vertex AI service agent privilege escalation and AI agents as cloud escalation vectors.
Decision tree: which GCP control fits this requirement?
iam.disableServiceAccountKeyCreation.The full translation table
Everything from all four sessions, in one place. This is the artefact to keep.
| Concept | AWS | Azure | GCP |
|---|---|---|---|
| Directory / identity store | IAM Identity Center or external IdP | Microsoft Entra ID | Cloud Identity / Workspace |
| Human identity | Identity Center user → permission set | Entra user | Google Account |
| Group | IAM group (users only) / Identity Center group | Entra group | Google group |
| Program identity | IAM role | service principal | service account |
| Auto-managed program identity | instance profile / Pod Identity | managed identity | attached service account |
| K8s workload identity | EKS Pod Identity / IRSA | Entra Workload ID for AKS | Workload Identity Federation for GKE |
| External federation | AssumeRoleWithWebIdentity + OIDC provider | federated credential on a managed identity | Workload Identity Federation + pool |
| Human federation | Identity Center + external IdP | native — Entra is the IdP | Workforce Identity Federation |
| Permission bundle | managed policy | role definition | predefined or custom role |
| The grant itself | policy attachment | role assignment (principal + role + scope) | binding in the allow policy |
| "Where it applies" | the Resource ARN in the document | the scope of the assignment | the node the policy is attached to |
| Policy on the resource | resource-based policy | — largely absent | allow policy on the resource node |
| Explicit refusal | Effect: Deny, writable anywhere | deny assignment — rarely sources-able | deny policy |
| Ceiling on a principal | permissions boundary | — approximate with scope discipline | principal access boundary |
| Org guardrail on principals | SCP | Azure Policy at management group | org policy constraint |
| Org guardrail on resources | RCP | Azure Policy deny | org policy + deny policy |
| Config enforcement / remediation | Config rules + declarative policies | Azure Policy — the only one that remediates | org policy + custom constraints |
| Attribute-based conditions | condition keys, aws:PrincipalTag | ABAC conditions on assignments | IAM Conditions in CEL |
| Short-lived token service | STS | Entra token endpoint | STSgenerateAccessToken |
| Metadata endpoint | IMDSv2 at 169.254.169.254 | IMDS at 169.254.169.254 | metadata server at metadata.google.internal |
| Just-in-time elevation | role session + your own approval flow | Privileged Identity Management | Privileged Access Manager |
| Secret store | Secrets Manager / Parameter Store | Key Vault (secrets + keys + certs) | Secret Manager |
| Key management | KMS | Key Vault / Managed HSM | Cloud KMS |
| Least-privilege analyser | IAM Access Analyzer | Entra Permissions Management | IAM Recommender / Policy Analyzer |
| Access debugger | IAM Policy Simulator | Check access / effective permissions | Policy Troubleshooter |
| Admin audit log | CloudTrail management events | Activity Log + sign-in logs | Cloud Audit Logs — Admin Activity |
| Data-plane audit log | CloudTrail data events off by default | diagnostic settings off by default | Data Access logs off by default |
| Posture / findings hub | Security Hub CSPM → Security Hub v2 | Defender for Cloud | Security Command Center |
| Threat detection | GuardDuty | Defender plans | Event Threat Detection (Premium) |
| SIEM | Security Lake + partner | Microsoft Sentinel | Google SecOps |
| AI agent identity | — an IAM role per agent | Entra Agent ID + blueprints | agent identities + pools |
| The escalation primitive to watch | iam:PassRole | Microsoft.KeyVault/vaults/write on access-policy vaults; roleAssignments/write | iam.serviceAccountUser and serviceAccountTokenCreator |
3 · Reality check 25%
Worked example: what your Vertex endpoint can actually reach
Same arithmetic as Session 1, applied to a project shaped like yours. Assume a Vertex AI endpoint serving a
model, deployed with defaults — which is to say, running as the Vertex service agent because nobody passed
--service-account.
The project inventory, modest by production standards:
| Resource class | Count | Contains |
|---|---|---|
| Cloud Storage buckets | 14 | 1 model weights, 3 fine-tuned adapters, 4 training datasets, 2 eval outputs, 4 miscellaneous |
| BigQuery datasets | 6 | training data, eval results, inference logs including user prompts |
| Artifact Registry repositories | 2 | your serving images, including any embedded configuration |
| Service accounts in project | 9 | each one a potential impersonation target |
| Secret Manager secrets | 7 | Hugging Face token, third-party API keys |
Default deployment — running as the service agent
| Reachable via the metadata server | Count | Why |
|---|---|---|
| Model weights bucket (the intended access) | 1 | the job it was deployed to do |
| Every other Cloud Storage bucket in the project | +13 | the default Vertex service agent has Cloud Storage access project-wide |
| — of which: fine-tuned adapter buckets | (3) | this is the ModeLeak exfiltration target. Your proprietary fine-tunes. |
| Every BigQuery dataset | +6 | default service agent has BigQuery access — including inference logs containing user prompts |
| Artifact Registry repositories | +2 | image pulls run as the service agent regardless of BYOSA |
| Total reachable | 22 | from one compromised inference container |
| Visible in audit logs? | no | Data Access logs are off by default — the reads leave no record |
BYOSA deployment — a dedicated service account with one conditioned binding
| Reachable | Count | Why |
|---|---|---|
| One prefix of one bucket | 1 | the single conditioned objectViewer binding |
| Other buckets, BigQuery, secrets | 0 | no binding grants them |
| Artifact Registry | — | still pulled by the service agent, but the runtime container cannot reach it |
| Total reachable at runtime | 1 |
22 → 1
A 22× reduction from one flag on the deploy call plus one conditioned binding. Perhaps fifteen minutes of work, once.
3 adapter buckets
Your fine-tuned weights are the asset with the highest ratio of value to protection in most ML projects. Under the default they are reachable from any code running in the serving container.
0 log entries
With Data Access logs off, you would have no record that any of it happened. Turn them on for Cloud Storage and BigQuery in projects holding model artefacts — and budget for the volume.
Not a bug
Every step above is documented, intended behaviour. Google classified two of the disclosed variants as "working as intended". You cannot wait for a patch; this is a configuration you own.
The habit this should install. On every managed AI platform — Vertex, SageMaker, Azure ML, Bedrock — ask one question before you deploy: what identity does this run as, and did I choose it? If the answer is "the default one", you have inherited the platform's permission set rather than designing your own, and the platform's set is sized for the product's full capability surface, not your job. This is the managed-service version of the shared-responsibility line from C1: the provider secures the platform, and the identity your workload runs as is unambiguously on your side of it.
Three grounded examples
The XM Cyber disclosures on Vertex AI Agent Engine and Ray on Vertex AI showed that a user with read-only Viewer permissions could extract service agent credentials from the instance metadata service and reach Cloud Storage, BigQuery, Pub/Sub and Artifact Registry across the project. Google's response was to classify both as working as intended, with no remediation planned.
That is a defensible position — the metadata server is supposed to hand credentials to code running on the instance; that is its entire purpose. But it has a direct consequence for how you plan. There is no patch coming. The controls that actually address it are all yours: pass a custom service account rather than accepting the default; scope its bindings with conditions; use a principal access boundary to contain the whole class of service accounts in the project; and enable Data Access logs so that if it does happen you can see it.
The transferable lesson for a certification and for interviews: "the vendor says it is intended" and "it is safe in your configuration" are different claims, and the shared-responsibility line is exactly where they separate.
Cloud Security Alliance, Vertex AI Service Agents as Lateral Movement Vehicles — labs.cloudsecurityalliance.org.
Google's Cloud Threat Horizons Report H1 2026, which Session 1 used for the UNC4899 pipeline compromise, ends with a specific set of recommendations. Worth reading as a checklist because it is the platform vendor telling you which of its own controls it expects you to be running: IAM Recommender to reduce excessive permissions, organization policies to prevent external identities and public access, and Security Command Center to detect drift such as the broadening of access and permissions over time.
Notice that all three are about privilege creep rather than intrusion. Recommender measures the gap between granted and used; org policy prevents the two highest-severity misconfigurations structurally; SCC watches for the gap widening. That is the feedback loop from Session 1 — measurement, not intention — assembled from GCP parts.
Google Cloud, Cloud Threat Horizons Report H1 2026 — cloud.google.com.
The Enterprise tier of Security Command Center shuts down on 21 May 2027. Organizations using it move to Premium automatically. If you are designing a multi-cloud security posture on GCP right now, or reading anything written in 2024–2025 recommending SCC Enterprise for its bundled SIEM and Mandiant intelligence, that recommendation has an expiry date attached.
A free-trial GCP account has no organization resource. This one bites in labs: without an organization, there are no folders, no organization policy constraints, no deny policies and no principal access boundaries, because all of those are children of the organization node. A trial project gives you exactly one thing — project-level allow policies — and it gives you Owner on it. So the environment where most people form their GCP IAM habits is precisely the one that cannot express any of the controls in this session. Plan lab work accordingly, and be suspicious of habits formed in a trial project.
Google Cloud, Security Command Center service tiers — docs.cloud.google.com.
4 · Apply it 10%
Your Vertex AI script, re-read as identity decisions
Your deployment script made a set of infrastructure choices. Every one of them also made an identity choice, silently. Here they are, made explicit:
| What your script sets | The identity decision hiding inside it | What to change |
|---|---|---|
| machine type T4 accelerator | None directly — but GPU quota is granted per project per region, and quota requests are an IAM-adjacent surface most people never review. | Nothing. Noted so you know where the boundary is not. |
(no --service-account) | The big one. Absence of this flag means the endpoint runs as the default Vertex service agent, with project-wide Cloud Storage and BigQuery access. This is the 22-versus-1 from the worked example. | Create vllm-serving@, grant one conditioned objectViewer binding, pass it explicitly. |
| model artefact URI in Cloud Storage | Which bucket, and therefore which blast radius. If weights and fine-tuned adapters share a bucket, one grant covers both. | Separate buckets for base weights and fine-tunes. The adapters are the higher-value asset and deserve their own boundary. |
| autoscaling 1–3 | min_replica_count=1 means a GPU-backed replica holds an identity continuously, not just during requests. A standing credential in a standing container. | Nothing for identity, but from C1: this is also the setting that quietly bills a GPU around the clock. |
| spot instances | Preemption recreates replicas frequently. With an attached service account this is a non-event — tokens are refreshed automatically. With a key file it would be a distribution problem at every restart. | Nothing. This is workload identity earning its keep invisibly. |
| the credentials running the script | Almost certainly your own gcloud login with Owner or Editor. Every resource is created by, and initially owned by, a broadly privileged human. | For anything recurring, move to a deployment service account with only the roles the deploy needs, invoked from CI via Workload Identity Federation. |
Three concrete things to do, in order of value:
- Pass an explicit service account to every Vertex deployment. Highest return of anything in this
stage for your setup: fifteen minutes, and it removes an entire class of lateral movement. Verify with
gcloud ai endpoints describethat the deployed model shows your service account, not the default. - Turn on Data Access audit logs for Cloud Storage and BigQuery in any project holding model artefacts. They are off by default and they are the difference between "we believe nothing was read" and knowing. Budget for the log volume — this is billable, and it is the one billable thing in this session worth paying for.
- Set
iam.disableServiceAccountKeyCreationif you have an organization resource. Your GKE and Vertex work never needs a key file, and the constraint makes the worst option structurally unavailable rather than merely discouraged.
For the GKE side of your work: if you move the vLLM stack from a bare L4 VM to GKE, use
Workload Identity Federation for GKE with direct principal identifiers — grant the IAM role straight to the
Kubernetes ServiceAccount and skip the intermediate GCP service account entirely. It is fewer objects and one
fewer thing to get wrong. On Autopilot it is already enabled; on Standard, remember that switching an existing node
pool to GKE_METADATA requires recreating the nodes.
Optional lab — audit your own project's identity surface cost: $0.00
Read-only, entirely on free APIs. It creates nothing and enables no billable service.
Set a budget alert: Billing → Budgets & alerts → a $1 monthly budget with an email threshold. Then, explicitly: do not activate Security Command Center Premium or Enterprise — the activation flow is organization-level and awkward to reverse. Standard is free and is all you need. Do not enable Data Access logs during the lab itself; note them as a production recommendation instead.
- Who are you?
gcloud auth listandgcloud config list. Confirm the active account and project before anything else. - Read the project's allow policy.
gcloud projects get-iam-policy $(gcloud config get-value project) --format=json. Search the output forroles/owner,roles/editorandallUsers. Each is a finding. - See the whole ancestor chain — the union rule made visible:
gcloud beta projects get-ancestors-iam-policy $(gcloud config get-value project) --include-deny. This shows every allow and deny policy inherited from folders and the organization. If you are in a trial project with no organization, this will be short, and that absence is itself the lesson. - Inventory the service accounts.
gcloud iam service-accounts list. For each, check for key files:gcloud iam service-accounts keys list --iam-account=EMAIL --managed-by=user. Any user-managed key is a long-lived credential; note when it was created. - Find the escalation grants. In the policy JSON from step 2, search for
iam.serviceAccountUserandiam.serviceAccountTokenCreator. Anyone holding these can act as the service accounts they cover. - Check what your Vertex endpoints run as.
gcloud ai endpoints list --region=REGION, thengcloud ai endpoints describe ENDPOINT_ID --region=REGIONand look forserviceAccounton the deployed model. An empty field means the default service agent — the 22-resource case. - Use Policy Troubleshooter. In the console, IAM & Admin → Policy Troubleshooter. Enter a principal, a resource and a permission. It reports allow, deny and PAB policies and explains which one decided — the three-gate diagram, live and against your own project.
- Optional, free: IAM & Admin → Recommendations. IAM Recommender shows over-granted roles based on 90 days of usage. This is your privilege creep, measured, at no cost.
Teardown
- Nothing was created, so nothing needs deleting for this lab.
- Confirm SCC is still on Standard: in the console, Security → Security Command Center → Settings → Tier detail. If a Billing status row appears at all, a paid tier is active.
- The C1 GPU check still applies, and matters more here than anything else on this list:
gcloud ai endpoints list --region=REGIONfor every region you have ever deployed into. A deployed model withmin_replica_count=1holds a GPU replica indefinitely and is invisible on the Compute Engine instance list. Undeploy withgcloud ai endpoints undeploy-model ENDPOINT_ID --deployed-model-id=ID --region=REGION, thengcloud ai endpoints delete ENDPOINT_ID --region=REGION. - If you created a throwaway service account while exploring:
gcloud iam service-accounts delete EMAIL. Remember its bindings persist as orphaned entries in the allow policy until removed —gcloud projects remove-iam-policy-bindingfor each. - Keep the budget alert.
You can now draw the principal→policy→resource triangle on any of the three clouds, read a policy for what it actually permits, and translate a control from any dialect into the other two.
Identity is one pillar. C4 adds the network layer — segmentation, private endpoints and the perimeter that is still worth having even though it is no longer the boundary.