The second day-0 decision is not what to build. It is who each thing gets to be. Identity comes before infrastructure, because the moment a resource can act, the only question that matters is what it is allowed to do, and the honest default answer is almost nothing.
Most breaches are not clever. They are an identity that could do more than it needed to, holding a credential that lived longer than it should have. So the two rules I want on day 0 are simple to state and annoying to actually keep: every workload runs as its own minimal identity, and nothing carries a long-lived key. The annoyance is the point. If least privilege is convenient, you are probably not doing it.
Least privilege from resource one
The tempting shortcut on GCP is the default compute service account. It exists automatically, it works, and it comes with broad project-wide reach that nobody chose on purpose. Using it is the single most common way a small project ends up with an identity that can touch far more than the one job in front of it.
So the rule is: never the default identity. Every workload gets a dedicated service account created alongside it, and that account starts from zero and receives only the specific grants its job requires. In this infrastructure a Cloud Run service, or the VM that replaced it, gets exactly three or four things and no more: permission to pull its own container image from one specific repository, permission to read the one secret it actually needs, and permission to write its own logs and metrics. That is the entire surface.
Two details make this real rather than aspirational. First, scope. The image-pull grant is bound to a single repository, not to the project. A second repository would require its own explicit grant, which is exactly the friction you want, because it means the surface never grows silently. The secret grant is bound to one named secret, not to "secrets" as a category. If the workload needs a second secret later, that is a new visible line in a plan, reviewed on its own merits. Least privilege is not a posture you set once; it is a property you preserve grant by grant.
Second, placement. The baseline that every service shares, its own identity plus log and metric writing, lives in the shared module. But anything sharper, reading a specific secret or a specific bucket, lives in the application's own configuration, right next to the resource it concerns. Keeping the grant beside the thing it grants access to means a reviewer can see the access and the reason in the same glance. Permissions that drift off into a central IAM file are permissions nobody remembers the reason for.
There is one identity that breaks the least-privilege pattern on purpose, and honesty demands naming it. The thing that applies Terraform is broadly powerful by nature, because it creates and destroys resources across the project. You cannot give a general-purpose applier a tiny permission set and still have it build arbitrary infrastructure. What you can do is refuse to hand it the very top. In this setup the intended applier identity is scoped to editor plus a few specific admin roles it genuinely needs, and deliberately not owner. The reasoning is written down: owner adds project-level IAM escalation surface that editor lacks, so if that identity is ever compromised the blast radius is resource changes and scoped IAM, not the ability to make itself god. A powerful applier is unavoidable. An omnipotent one is a choice, and it is the wrong one.
No long-lived keys, anywhere
The second rule is the one that ages best. A long-lived service account key is a password that never expires, sitting in a file, waiting to be copied into a log, a laptop, a CI config, or a screenshot. The entire history of cloud credential leaks is footnotes to that sentence. So the target state is zero exported keys. A workload should prove who it is from where it runs, not carry a secret that proves it.
For workloads inside GCP this is nearly free, and it is why the identities above have no keys at all. A Cloud Run service or a VM is issued short-lived credentials for its attached service account by the platform itself; nothing is ever exported. The VM in this project fetches its one secret at boot using that attached identity and runs, and there is no key material anywhere in its metadata or in Terraform state. The secret's value never touches the machine image, the instance metadata, or the state file. Only the plumbing to fetch it does.
The harder case is the one crossing a boundary: continuous integration outside the cloud that needs to deploy into it. The old answer was to export a service account key and paste it into the CI system's secrets, which is precisely the never-expiring password in a file that we are trying to eliminate. The modern answer is workload identity federation. The CI system presents a short-lived token that proves which repository the run came from, and the cloud exchanges it for temporary credentials. No stored key, and the trust is pinned to an exact source repository, so a run from anywhere else cannot exchange for anything.
The identity I wrote and turned off
Here is the honest receipt. I wrote the keyless CI path, the applier identity and the federation that lets a specific repository assume it, and then I deliberately left it disabled. Deploys today happen from an operator's own short-lived credentials, by hand.
The reasoning is a security decision, not laziness. Keyless federation removes the long-lived key, which is good. But standing up that path also means creating a broadly powerful identity and leaving it sitting there, permanently ready to be assumed, whether or not any deploy is happening. An idle powerful identity is not neutral. It is attack surface that exists every hour of every day, in exchange for convenience I am not yet using at a cadence that justifies it. Manual deploys from a human's expiring credentials mean the powerful path exists only for the minutes it is actually in use, and no dormant privileged account is sitting around the rest of the time.
The whole thing is written, commented, and one uncomment away from live, with the re-enable steps recorded next to the disabled code. When deploy frequency crosses the line where a standing keyless runner is worth its idle risk, I flip it on in an afternoon. Until then, the most secure configuration for an identity I am not using is switched off. The point of least privilege is not to have the perfect grant. It is to not hold power you are not currently spending.
Previously in this series: The Root of Trust Comes Before the Automation. Next: Deny by Default.
The same decision on AWS: Identity Before Infrastructure.