Skip to content

fix(provx/gcp): trust the issuer the caller names, not the one compiled in - #5

Merged
JeroenSoeters merged 2 commits into
mainfrom
fix/provx-gcp-issuer
Aug 28, 2026
Merged

fix(provx/gcp): trust the issuer the caller names, not the one compiled in#5
JeroenSoeters merged 2 commits into
mainfrom
fix/provx-gcp-issuer

Conversation

@JeroenSoeters

@JeroenSoeters JeroenSoeters commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Two commits: the fix, and the test weakness that let it exist.

1. fix(provx/gcp): trust the issuer the caller names

provx/gcp resolved its issuer from the provx.Endpoint constant, so every workload identity provider it created trusted https://oidc.cloud.formae.ai regardless of the issuer the caller had been given. gcp.New now takes the issuer and validates it with provx.ParseIssuer — the same canonical-origin rule the AWS provisioner has always held it to.

Why it matters. The caller was already carrying an issuer and had nowhere to put it. formae's internal/cli/connect/gcpprovision.go takes an issuer argument, openSession validates it against what the control plane reports, and then the call to gcp.New drops it.

Production hides this because both values are the same string there. Anywhere else — a staging installation, an acceptance test with its own issuer — provisioning reports success and establishes trust that can never accept a token, and the failure surfaces later as an opaque exchange rejection rather than as the wrong issuer it is.

This is the AWS path's existing contract: provxaws.New takes the issuer, and the issuer travels verbatim from the control plane into the artifacts. This makes GCP match.

Testing. The gap that let this stand was that no test asserted the created provider's issuerUri. The fake-backed tests now trust a deliberately non-production issuer, so a value compiled in rather than passed through fails them:

  • TestProviderTrustsTheIssuerItWasGiven — the created provider carries the caller's issuer.
  • TestRefusesAnIssuerThatIsNotACanonicalOrigin — empty, http, trailing slash and ported issuers are refused at construction, not at provisioning.

2. test(provx/aws): stop testing the issuer against the value it would be hardcoded to

The same weakness, on the side that happens to be correct.

provx/aws does take its issuer as a parameter and does thread it into the provider URL and the trust policy, and the tests pin both — the Url check in connect_provider_test.go and the whole trust-policy document in role_test.go, condition keys included. But testIssuer was the production issuer, so all of it would pass equally against an issuer resolved from provx.Endpoint. Those assertions pinned the issuer's shape and never its provenance, which is precisely how the GCP bug above went unnoticed.

testIssuer now points at a non-production origin. No production code changes and the suite still passes — which is the useful outcome: it confirms the parameter really is what reaches the artifacts, instead of leaving that true by luck and unproven.

Scope

go test ./... passes across provx, provx/aws and provx/gcp. cmd/oox builds; its two GCP call sites now pass provx.Endpoint explicitly, so its behaviour is unchanged.

Follow-up

formae must re-pin oox/provx and pass its issuer through newGCPProvisioner for the fix to reach formae connect gcp. That unblocks the GCP half of platform-engineering-labs/formae#696, whose test currently skips for exactly this reason.

…ed in

The GCP provisioner resolved its issuer from the provx.Endpoint constant,
so the workload identity provider it created always trusted
https://oidc.cloud.formae.ai no matter which issuer the caller had been
given. The AWS provisioner has taken the issuer as a parameter since it
was written, for the reason that applies equally here: the issuer is
produced by the control plane and travels verbatim into the trust
artifacts, so a provisioner that substitutes its own establishes trust
for an issuer nobody named.

The caller was already carrying one and had nowhere to put it: formae's
connect path takes an issuer argument, validates it against what the
control plane reports, and then drops it on the floor at the call to
gcp.New. Production hides this because both values are the same string
there; anywhere else -- a staging installation, an acceptance test with
its own issuer -- provisioning reports success and establishes trust that
can never accept a token.

New now takes the issuer and validates it with provx.ParseIssuer, the
same canonical-origin rule the AWS side holds it to: a value with a port
or a path cannot be compared against what Google stores, so it is refused
at construction rather than producing a provider that silently never
matches.

The absence of a test asserting the created provider's issuerUri is what
let this stand, so the fake-backed tests now trust a non-production
issuer and one of them asserts the provider carries it.
…e hardcoded to

The AWS provisioner takes its issuer as a parameter and threads it into
the provider URL and the trust policy, and the tests pin both. But
testIssuer was the production issuer, so every one of those assertions
would pass just as well against an issuer resolved from provx.Endpoint
instead of from the argument. They pinned the issuer's shape, never its
provenance.

That is the hole the GCP provisioner fell through: it did resolve its
issuer from the constant, and no test noticed, because a test written
with the production value cannot tell the two apart.

Pointing testIssuer at a non-production origin closes it here. Nothing in
the production code changes and the suite still passes, which is the
result worth having: it confirms the parameter is genuinely what reaches
the artifacts, rather than leaving that unproven and true by luck.
@JeroenSoeters
JeroenSoeters merged commit f2ab99b into main Aug 28, 2026
8 checks passed
@JeroenSoeters
JeroenSoeters deleted the fix/provx-gcp-issuer branch August 28, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant