v10.0 · the signed execution path

Security

Privileged work is staged, signed with a Cloud KMS asymmetric key, executed in the same call, and journalled at every stage. There is no approval queue and no per-job tap. This page says exactly what that means, why it is the right shape, and where the honest limits are.

KMS-signed Journalled Hardware key at the edge Zero datastore access Nine lockout classes
The posture

Stated plainly, then defended

A strain that needs to do something privileged stages a job. The job is signed with a Cloud KMS asymmetric key and executed in the same call. The journal records who staged it, the exact command, and what it returned. Nothing waits for a human tap, because on a default install there is no tap to wait for.

That is the whole posture, and we are going to defend it rather than dress it up. Plenty of products in this space advertise a confirmation prompt in front of every command and let the buyer infer that the prompt is the security. It is not. It is a user interface element that converts a security decision into a reflex.

Think about what a per-command tap actually asks of a person. It arrives dozens of times an hour, in the middle of other work, attached to a command they did not write, describing an effect they would have to reason about carefully to evaluate — and the correct answer is "approve" almost every single time. Human beings are extremely good at learning that pattern. Within a week the tap is muscle memory, and the one command in three thousand that deserved a hard look gets the same half-second of attention as the two thousand nine hundred and ninety-nine that did not. The prompt did not add a control. It added latency, and it moved the blame from the system to the person who clicked.

So v10.0 spends its security budget somewhere the machine can be relied on. Every privileged job is bound to a cryptographic signature over its own identity and its own contents, executed by a component that can verify that signature and cannot forge it, in an environment stripped of everything it does not need, with an immutable record of what happened. That is a control. It works at three in the morning, it works on the ten thousandth command, and it does not get tired.

The selling point, said out loud. This accelerates security-minded engineering. The cost of doing the right thing — staged, signed, scoped, journalled — is zero extra human actions, so nobody is ever tempted to route around it to get work done. Controls that slow people down get disabled by the people they slow down. This one has nothing to disable.

What the journal holds

who staged it
The strain identity that requested the job. Per-strain identity means this is a role, not a shared account — the record names something specific.
the command
The exact command, as staged. Not a paraphrase, not a category, not "ran a maintenance task".
what it returned
The result. The record is written whether the job succeeded, failed or was refused.

The journal is the accountability mechanism here, and it is worth being precise about the trade it represents. A prompt is a control that happens before and depends on a human being alert. A journal is a control that happens always and depends on nobody. Given one budget and a choice between them, the second is the one that survives contact with a Tuesday afternoon.

The switch

PC_AUTO_APPROVE=0 makes the product stop, not ask

If your environment genuinely requires that no privileged command executes automatically, there is a switch. It does not turn on an approval queue. It turns off execution.

On a default install, install.sh sets PC_AUTO_APPROVE=1: a staged job is signed and run in the same call. Set it to 0 and a staged job goes to pending and does not run. Not on a timer. Not on a retry. Not when the queue drains, not when someone reconnects, not ever. There is no background worker that comes along later and picks pending jobs up, because building one would recreate exactly the risk the setting exists to remove.

# shipped defaults, written by install.sh
PC_AUTO_APPROVE=1      # stage + KMS-sign + execute in one call, journalled
PC_GUARDRAILS=0        # set 1 to restore two runtime refusals
PC_REQUIRE_PASSKEY=0   # set 1 to re-arm the whole WebAuthn layer

# PC_AUTO_APPROVE=0  ->  staged job becomes `pending` and is never executed.
#                        Not on a timer. Not on a retry. There is no queue
#                        to approve it from, and no per-job tap anywhere.
Read this twice, because the previous version of this page got it wrong. There is no per-job passkey approval in v10.0. The console page that offered one was deleted. Nothing in this product waits for you to tap a key before a staged job runs. If another page, another README or an older screenshot tells you otherwise, that page is out of date and this sentence is the correct one.

So PC_AUTO_APPROVE=0 is a fail-stopped mode, not a fail-slow one. It is the right setting for a substrate you have deliberately frozen, for a demonstration environment, or for the interval between noticing something wrong and understanding it. It is the wrong setting for a working install, because it does not make the work safer — it makes the work stop.

The two other switches

PC_GUARDRAILS
Ships at 0. Setting it to 1 restores two runtime refusals; the exact predicates live in the source rather than in marketing copy, so read them there and decide whether they match your environment.
PC_REQUIRE_PASSKEY
Ships at 0. Setting it to 1 re-arms the entire WebAuthn layer, which is still entirely in the tree — nothing was removed, it is simply not on by default. This governs the session layer described below. It does not add a per-job approval tap; that surface does not exist.
PC_AUTO_APPROVE
Ships at 1. 0 stops execution of staged jobs entirely, as above.
Getting in

How the console is actually protected

If there is no per-command tap, the honest question is what stands between an attacker and a console that can stage privileged jobs. Three controls, and the third is the one most installations get wrong.

1

A phishing-resistant hardware key

Console access is fronted by Google Identity-Aware Proxy and requires a hardware security key. Phishing-resistant means the credential is bound to the origin: a convincing look-alike domain cannot elicit a usable assertion, because the key will not produce one for the wrong origin. That removes the single most common way an administrative console is actually lost — not a broken cipher, a convincing email.

2

Enforcement at Google's edge, before your container is reached

IAP enforces at the edge. An unauthenticated or unauthorised request is refused before it reaches your Cloud Run container — your application code is not the thing deciding whether the caller is allowed in, and a bug in your application cannot be reached by someone who never got past the edge. The ES256 assertion IAP produces is verified against Google's JWKS with iss, exp and aud pinned. X-Goog-Authenticated-User-Email is never trusted on its own, because a header is not a proof. A cold JWKS cache fails closed: if the verifier cannot obtain the keys, it refuses, rather than serving traffic it was unable to check.

3

An org policy that refuses the binding at write time

The organisation policy constraint constraints/iam.allowedPolicyMemberDomains restricts which identities can appear in an IAM policy at all. An out-of-domain account — a personal Gmail address, a contractor's own login, the throwaway you made to test something — is refused at binding write, not at request time. The grant does not fail later. It does not get made and then get flagged in a report. It never gets made.

Why the third control is a different kind of thing

Dwell on this one, because it is the difference between a policy and a control, and most security programmes never notice they only bought the first.

A policy is a sentence in a document: "do not grant console access to personal accounts". It is true, it is sensible, everyone agrees with it, and somebody does it anyway. Not out of malice — out of Friday. A contractor needs access for two hours, their corporate account is not provisioned yet, the deadline is real, and the grant takes eleven seconds. The policy was never violated by a person who disagreed with it. It was violated by a person who agreed with it and had a problem.

allowedPolicyMemberDomains is a control because it refuses the write. The eleven-second workaround returns an error. The person with the deadline has to do something else — provision the corporate account, use a different mechanism, or escalate — and any of those outcomes is better than the one where an identity outside your domain quietly holds a binding on the project that stages privileged jobs.

The honest note. Turning the constraint off, adding the account, and turning it back on does work. Nothing here prevents it: an org policy administrator can lift the constraint, write the binding, and restore the constraint, and the binding survives — the constraint governs writes, not the bindings that already exist. What the control buys you is that this is now a deliberate, multi-step, logged act by someone with organisation-level authority, instead of an eleven-second convenience. That is exactly why the exception you told yourself was temporary is permanent: nothing ever comes back to remove it, because the constraint you re-enabled is happily allowing the binding it never saw being written. If you do this, write the removal date in the wiki and put a real reminder somewhere that outlives your memory of the Friday.
The executor

It verifies a signature it could not have produced

The component that actually runs a privileged command is deliberately the least trusted, least connected and least capable thing in the system. It cannot read your data, cannot consult your database, and cannot sign anything — it can only check.

Approvals are Cloud KMS asymmetric signatures. The control plane holds the private half; the executor holds only the public half. The consequence is the sentence in the heading, and it is the design's whole point: the executor can verify that an approval is authentic, and it has no ability whatsoever to manufacture one. Compromising the executor gets an attacker the power to refuse valid jobs. It does not get them the power to authorise anything.

The signature covers the job ID and the command digest, together. Privilege elevation is therefore bound to one specific job running one specific command. Edit the command by a byte after signing and the digest no longer matches: the job is refused. Replay a valid signature against a different job ID and the binding fails: refused. There is no approval in this system that means "this strain may run things"; every approval means "this job may run this command", and nothing else.

signature scope
KMS asymmetric signature over the job ID plus the command digest. Both, bound together. An edited command is a different digest and is refused; a reused signature is a different job ID and is refused.
key split
Control plane: private half, in Cloud KMS. Executor: public half only. The executor verifies what it cannot forge. A compromised executor cannot self-approve.
approved_sha256
The pin. The command that runs is hashed and compared against the approved digest at execution time, not at staging time. The value that was approved is the value that runs, or nothing runs.
staleness window
A signed approval is valid for a bounded interval. An approval captured from a log and presented later is outside the window and is refused. Signatures do not accumulate into a stockpile of usable authority.
atomic single-use claim
A job is claimed exactly once, atomically. Two executors racing on the same job produce one winner and one refusal, not two executions. This is what stops a retry storm from running a destructive command twice.
PATH jail
The subprocess runs with a restricted PATH. A command cannot reach an arbitrary binary that happens to exist on the image; resolution is confined to the jail.
hard timeout
Execution is bounded. A job that hangs is killed rather than holding a claim and a container indefinitely. Wall-clock is a resource like any other.
create-only results
Result objects are written create-only. The executor can record an outcome; it cannot overwrite an outcome it already recorded, and it cannot revise history to say something else happened.
zero datastore access
The executor has no datastore access at all. It cannot read Firestore, cannot read your project state, cannot look up a policy to decide whether it likes a job. It trusts exactly what the signature covers — nothing more is reachable.

Why zero datastore access is the strongest line in the table

An executor that could consult a database would have a second input, and every second input is a second thing to compromise. It would mean an attacker who could write to the right document could change what the executor decides — without touching the key, without forging a signature, without going anywhere near KMS. Denying the executor any datastore at all collapses its decision surface down to the signed material in front of it. There is nothing to poison because there is nothing to read.

It also makes the component genuinely auditable. You do not need to reason about the state of your database to know what the executor will do. Its inputs are the job, the command, the signature and the clock. That is a small enough set to hold in your head, which is a property worth more than most features.

stage -> sign(job_id + sha256(command))          [control plane, KMS private half]
      -> verify(signature)                       [executor, public half only]
      -> compare sha256(command) == approved_sha256
      -> check staleness window
      -> atomic single-use claim  (exactly one winner)
      -> exec in PATH jail, hard timeout
      -> write result object (create-only)
      -> journal: who staged it, the command, what it returned
Sessions

The WebAuthn layer, and a 401 that stays where it is

The session machinery is entirely in the tree and re-armed with PC_REQUIRE_PASSKEY=1. It governs who holds a console session — not whether an individual job runs.

Session cookies are HMAC-SHA256. The layer fails closed on a weak secret: a secret that does not meet the requirement is not accepted with a warning in a log nobody reads — it refuses to serve. The failure mode of a session layer configured badly should be "nothing works", loudly, on the first request, rather than "everything works" and a forgeable cookie.

401 in place

An unauthenticated request gets a 401 where it stands. No redirect to a login page. No ?next= parameter carrying the URL you were trying to reach. No WWW-Authenticate prompt from the browser.

The ?next= detail is small and it matters. A login redirect that preserves the destination is an enumeration oracle: an unauthenticated attacker can walk a list of candidate paths and read the difference between "this route exists and wants auth" and "this route does not exist" straight out of the redirect behaviour, and they can do it without any credential at all. It is also a stock open-redirect surface, since something eventually has to decide whether the next value is safe to bounce to, and that decision is a validator that people get wrong. Returning 401 in place answers with the minimum: not authenticated. It does not confirm what is behind the door, and it does not hand an attacker a bounce.

Suppressing the WWW-Authenticate prompt is the same instinct applied to the browser: a native credential dialog on an API surface trains people to type passwords into a dialog that any origin can raise.

Topology

Two surfaces, one image — and it is a boundary, not packaging

The console and the MCP endpoint are two Cloud Run services built from one image. That split exists for a security reason, and the reason is worth knowing before you try to simplify it.

IAP consumes the Authorization header. That is not a quirk to work around; it is how the edge does its job. It also means an MCP client — which authenticates with OAuth 2.1 by putting a bearer token in exactly that header — can never live behind IAP. The token would be eaten by the proxy before your service saw it. So there are two services: one behind IAP for humans with hardware keys, one in front of OAuth 2.1 for machines with tokens. Trying to collapse them means giving up either the hardware-key requirement on the console or the standards-compliant auth on the API.

The split is enforced by a 90-entry route table checked at build time. Every route is assigned to exactly one surface, and a route that lands on neither service throws at boot — the same deliberate failure discipline used for the MCP SDK assertion. A new handler cannot be added and quietly served from both places, and the mistake where an administrative route accidentally appears on the machine-facing surface is not a code review's responsibility. It is a build failure.

The boundary in one line. Humans authenticate at Google's edge with a hardware key; machines authenticate at your service with OAuth 2.1. Neither path can be used to reach the other's routes, and which route lives where is decided at build time, not at request time.
Lockouts

Nine codified classes of "you cannot do that to yourself"

The most likely catastrophe in a system like this is not an intruder. It is you, or a strain acting for you, making a correct-looking change that removes your own way back in.

Nine lockout classes are codified in the source. They block the changes that destroy your own route back to the console — the class of mistake where everything succeeds, the change applies cleanly, and the door is now locked from the inside with the key on the wrong side of it. Among what they refuse:

last approver
Removing the only identity on the allow-list — frequently the one making the change.
own access
A change whose immediate effect is to revoke the access of the caller performing it.
edge protection
Detaching or disabling the IAP protection in front of the console surface.
key material
Destroying or disabling the KMS key material that approvals and envelopes depend on.
domain constraint
Changes to the domain-restriction posture that would leave the project unable to bind anyone at all.
surface removal
Deleting or unrouting the surface that would have to serve the request that fixes the problem.
state destruction
Operations that would remove the project state the control plane needs to come back up.

These are described here by what they protect rather than reproduced as a literal enumeration; the codified list is in the tree and is the authority. The design rule behind all nine is the same: a system that can be bricked by one plausible action will eventually be bricked by one plausible action, and the recovery cost is always paid at the worst possible moment.

Honest limits

What we do not claim

Everything above is a mechanism you can go and read. This section is the part most security pages leave out, and it is here because a security page that only contains good news is not a security page.

No independent audit. No third party has audited this code. Nobody has been paid to try to break it. The cryptographic envelope is cross-validated against an independent implementation, 57 of 57 vectors, which is evidence about that specific implementation and is not the same thing as a review of the system. Treat every claim on this page as a description of intent that you should verify against the source, because that is exactly what it is.
No compliance claims. Not SOC 2. Not ISO 27001. Not HIPAA, not PCI, not FedRAMP, not "enterprise-grade", not "certified", not "compliant" in any sense any auditor would recognise. No attestation exists. If your organisation requires one, this software does not have it and no amount of reading this page will change that.
The build is reproducible; the image is not bit-identical. The release generator is deterministic — cut twice, diff -r, expect zero — and the archives are reproducible with fixed mtime, gzip 9 and modes taken from the tree, with a SHA-256 manifest per file. That covers the source artefact. It does not cover the container: rebuilding produces an equivalent image, not a bit-identical one, because base layers and upstream packages move underneath you. Do not tell an auditor that image digests will match across rebuilds. They will not.
This release has been installed from zero a small number of times. v10.0 is new. The path from an empty project with billing enabled to a working install has been exercised, but not thousands of times across every shape of organisation policy, VPC configuration, quota posture and pre-existing IAM tangle that exists in the world. If your project is unusual, you may be the first person to run this against that particular unusual thing. Budget for it, and please open an issue when it happens.
Console access rests on one Google identity. On a normal install, the ability to reach the console comes down to a single Google identity being on the approver allow-list. Lose that account — leave the company, break the hardware key, get the account suspended, drop the phone in the sea — and the lockout classes above are protecting a door you no longer have a key for. Add a second in-domain account to the allow-list before you need it. In domain, because allowedPolicyMemberDomains will refuse an outside one at write time, and the moment you need the spare is precisely the moment you will not want to be lifting an org policy constraint under pressure. Do it during setup, on a day when nothing is wrong. This is the single most common way an installation of something like this becomes unrecoverable, and it takes about two minutes to prevent.
Disclosure

Reporting a vulnerability

If you find something, we want to hear it, and we would rather hear it early and wrong than late and right.

The reporting process, the contact route and the expectations for both sides are in SECURITY.md in the repository. That file is the authority; it travels with the code and it is updated with the code, which is more than can be said for a paragraph on a marketing site.

Two things that help enormously. Tell us which release you are on — v10.0 is current, and the tag is v10 — and tell us the values of PC_AUTO_APPROVE, PC_GUARDRAILS and PC_REQUIRE_PASSKEY on the install where you saw it. Those three settings change the shape of the system enough that a report without them can take a day longer to reproduce than it needs to.

One last framing, because it is the thesis of this entire page. A security control should be judged the way you would judge sterilisation, not the way you would judge a signature on a form. It does not ask permission for each item on the tray. It applies the same uncompromising process to everything that goes in, every time, and it writes down what it did. That is the standard a security control should be held to — not whether it made a person feel consulted, but whether it is still working correctly on the ten-thousandth run at three in the morning when nobody is watching.