Skip to content

feat(gcp): OpenBao on GCP — offline-root PKI, code-only half of workstream 11 - #1827

Merged
Smana merged 21 commits into
mainfrom
worktree-gcp-openbao
Aug 25, 2026
Merged

feat(gcp): OpenBao on GCP — offline-root PKI, code-only half of workstream 11#1827
Smana merged 21 commits into
mainfrom
worktree-gcp-openbao

Conversation

@Smana

@Smana Smana commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Workstream 11 of the GCP support design: OpenBao on GCP, giving the GKE cluster a private certificate authority.

This is the code-only half. Four of the plan's eight tasks are implemented. Tasks 3, 6, 7 and 8 need a live GCP deployment — the PKI ceremony, the actual terramate script run deploy, the PKI configuration and the cert-manager wiring — and are deliberately not in this PR. What is here stands on its own: two OpenTofu stacks' worth of code, a script that now serves both clouds, and an IAM role, none of which has been applied to a cloud.

Design: 2026-08-24-gcp-openbao-design.md · Plan: 2026-08-24-gcp-openbao.md

The design decision that changed during review

The 2026-08-18 design chose two independent roots, one per cloud, and rejected a shared root because cross-signing would be "a manual ceremony on every rebuild".

That premise was wrong, and this PR retracts it. The intermediate lives in Secret Manager independently of OpenBao's lifecycle — which is exactly why AWS's survives rebuilds today — so signing happens once per cloud, ever. The better option was rejected over a cost it does not have.

Worse, the first draft carried over the AWS shape where config_ca imports a pem_bundle containing the root key into the live PKI mount. The repo's own pki-and-secrets.md warns against precisely this: "do not carry it into a deployment where the root CA matters." Compromising OpenBao would have yielded the root.

Now: one offline root, per-cloud intermediates, intermediate imported directly as the issuer. The root key never reaches Secret Manager, the mount, or OpenTofu state. It also removes four resources — the key → intermediate_cert_request → root_sign_intermediate → set_signed sequence exists only to generate an internal intermediate under an imported root.

GCP adopts it first; AWS follows later. The interim cost is recorded honestly in the design: until AWS migrates, clients trust two anchors.

What's in the four tasks

Task Change
1 xplane_secret_reader custom role, allowlisted so a GCPWorkloadIdentity claim can grant External Secrets read access to Secret Manager
2 --cloud gcp on scripts/openbao-config.sh — three AWS-coupled seams replaced by dispatching helpers
4 opentofu/gcp/openbao/cluster/ scaffolding — backend, providers, variables, remote state, KMS data sources, service account, scoped IAM
5 Instance template, single-node MIG, and the boot script that installs OpenBao and starts it auto-unsealed by Cloud KMS

Cloud KMS is a bootstrap prerequisite, not a managed resource

The plan originally had this stack create the key ring and crypto key with prevent_destroy = true. An implementer caught that it was wrong; review later corrected why, which is worth reading below under the KMS rationale — the obvious explanation is not the true one.

They are now data sources, with the creation commands documented in kms.tf the same way the S3 state bucket is:

gcloud kms keyrings create openbao-dev --location europe-west4 --project ogenki-435905
gcloud kms keys create openbao-unseal --location europe-west4 --keyring openbao-dev --purpose encryption --project ogenki-435905

The key surviving teardown is now intentional rather than an error being swallowed, and the destroy script is genuinely strict — this stack owns billable compute, so a failed destroy must stop a --reverse teardown rather than let it delete the VPC out from under surviving instances.

Notable details carried deliberately

  • gcpckms takes the key ring and key by NAME, not id. The wrong one fails at unseal, not boot.
  • The pinned OpenBao GPG fingerprint survives the port with its reasoning. Without the pin, the signing key is fetched fresh each boot and trusted on sight, making the signature prove only that key and binary came from the same place.
  • Nothing secret is templated into instance metadata — GCP metadata is readable by anything on the box. Only a secret name flows through templatefile; TLS material is fetched at runtime by the instance's service account.
  • disable_mlock is deliberately dropped. Review traced it to upstream: mlock was removed from OpenBao in PR chore(deps): update helm release kube-prometheus-stack to v61.8.0 #363 (GA 2.0.0), so the setting is a deprecated no-op in every version this repo could deploy.
  • The Secret Manager IAM binding is scoped to one secret, not project-wide — project-wide would let the OpenBao node read Flux's GitHub App credentials.

Evidence

Every task passed an independent review that re-ran the checks rather than trusting the implementer's report.

Gate Result
tofu validate / fmt -check clean on both touched stacks
trivy config --exit-code=1 0 misconfigurations
tflint (incl. terraform_unused_declarations) clean — no pragma masks a real unused declaration
shellcheck -e SC2154 clean
terramate fmt --check clean
detect-secrets 0 findings on changed files

Not verified

None of this has run against a live GCP project. No cluster was deployed, no certificate issued, no binding created. The design's success criteria are all cluster-observable and remain unchecked, including the one assumption the whole chain rests on: that config_ca alone, without the CSR/sign/set-signed sequence, leaves the mount able to issue. The plan verifies that by hand as Task 7's first step, before any HCL is written.

What the whole-branch review caught

Every mechanical gate passed — tofu validate, fmt, trivy, tflint, shellcheck, links, doc-claims — and a review still found one Critical and five Important issues. None was reachable by tooling.

Critical: the Secret Manager role granted project-wide read of every secret. GCPWorkloadIdentity renders ProjectIAMMember, which is project-scoped, so External Secrets would have been able to read openbao-priv-gcp-root-token, the recovery keys, and the intermediate CA private key — enough to mint any *.priv.gcp.ogenki.io certificate offline, indefinitely. The branch contradicted itself: openbao/cluster/iam.tf explicitly refuses project-wide secretAccessor for the node, and the same boundary was opened one file over. Reduced to a single versions.access permission, with the residual project-wide scope stated plainly and per-secret bindings recorded as the better approach for Task 8. This fails open — no deploy would ever have surfaced it.

The stack validated but could not apply. openbao_version had no default and was absent from variables.tfvars. tofu validate does not need variable values, so every gate stayed green while the first real terramate script run deploy would have prompted on a TTY or died in CI.

A contract mismatch between the script and the secret it reads. openbao-config.sh ca parsed .ca from JSON; the GCP CA chain is raw PEM. The exact invocation in the plan would have died on a jq parse error. Settled before Task 3 writes the secret rather than after.

A boot path that fails silently. The two boot scripts are join-ed into one process, so setup-local-disks.sh's exit 0 on a missing data disk terminated the entire script — instance RUNNING, joined to the MIG, with no OpenBao installed. Now a hard exit 1, with the package install moved ahead of the disk setup that needs mkfs.xfs.

A template change that never reached the node. The MIG had no update_policy, so GCP's default OPPORTUNISTIC would adopt a new template without replacing the running instance — the GCP analogue of the AWS launch-template trap already recorded for this same service.

A correction to the KMS rationale

The design's stated reason for moving Cloud KMS out of the stack was wrong, and the comment now says so. The provider's delete for key rings and crypto keys is a no-op that returns successtofu destroy does not fail on them. The real reasons are that a rebuild's create hits ALREADY_EXISTS against a key ring that still exists server-side, and that destroying a managed google_kms_crypto_key schedules its key versions for destruction while exiting 0 — a silent trap, not a loud one.

The comment explicitly pre-empts the wrong conclusion: re-testing "does destroy fail on these" and finding it doesn't is exactly what would tempt someone to move KMS back into the stack.

Smana added 13 commits August 24, 2026 18:47
Workstream 11. Scope is PKI only — a root of trust, an issuing CA, a
cert-manager role and one AppRole. The app tenant namespace, kv-v2 mount,
snapshot AppRole and operator userpass are deliberately not ported: none has a
consumer on GCP.

The substantive change is to the certificate chain, and it supersedes the
decision recorded in the 2026-08-18 design.

That design chose two independent roots, one per cloud, and rejected a shared
root because cross-signing would be "a manual ceremony on every rebuild". The
premise was false. The intermediate lives in Secret Manager independently of
OpenBao's lifecycle — which is exactly why AWS's survives rebuilds today — so
signing happens once per cloud, ever. The better option was rejected over a cost
it does not have.

The first draft of this design also carried over the AWS shape where config_ca
imports a pem_bundle containing the ROOT key into the live pki mount. The
repository's own pki-and-secrets.md warns against it verbatim: "do not carry it
into a deployment where the root CA matters." Compromising OpenBao would yield
the root, which inverts the property an offline root exists to provide.

So: one OFFLINE root, per-cloud intermediates, and the intermediate imported
directly as the issuer. The root key is never in Secret Manager, never in the
mount, never in OpenTofu state. This also removes four resources — the
vault_pki_secret_backend_key -> intermediate_cert_request ->
root_sign_intermediate -> intermediate_set_signed sequence exists only to
generate an OpenBao-internal intermediate under an imported root.

GCP adopts it first, AWS follows later. The interim cost is recorded honestly:
until AWS migrates, clients trust two anchors. That was permanent under the
superseded decision and is now a state with a defined end.

Two findings the design surfaced that were not in the workstream row:

- scripts/openbao-config.sh is AWS-only and needs a --cloud gcp flag. Its
  coupling is three seams (secret read, secret write, CLI prefix), which is what
  makes a flag right rather than a sibling script.
- GCP Secret Manager IDs permit only letters, digits, hyphen and underscore.
  AWS's names are paths (certificates/priv.aws.ogenki.io/root-ca), so every name
  must be rewritten and the two clouds cannot share one convention.

It also needs an addition to slice 5's IAM allowlist: External Secrets reading
Secret Manager requires a GCPWorkloadIdentity claim, and the condition in
gke/init/iam.tf allowlists only xplane_dns_editor. A second pre-created custom
role is required. That is the mechanism working as intended.

Risks are recorded rather than resolved, including the one assumption the whole
chain rests on: that config_ca alone, without the CSR/sign/set-signed sequence,
leaves the mount able to issue. Standard import-an-existing-CA flow, but never
exercised in this repository — verify it early.

Evidence: validate-links.sh exit 0.
Eight tasks for docs/superpowers/specs/2026-08-24-gcp-openbao-design.md.

The verification model is adapted rather than borrowed: this is infrastructure,
so "write the failing test first" becomes "run the verification command and
watch it fail for the right reason". The gates are the repo's real ones — tofu
validate, trivy config, tofu plan, shellcheck, validate-manifests.sh. Forcing a
unit-test framework onto Terraform would have produced ceremony, not evidence.

Task ordering is deliberate:

- Task 1 (the Secret Manager IAM role) is first because it is independent of
  everything else and safe to merge alone.
- Task 3 (the offline PKI ceremony) comes before any stack, because cluster/
  reads the server certificate at boot. It produces secrets and a runbook, not
  code.
- Task 7 Step 1 verifies the design's one untested assumption BEFORE any HCL is
  written: that config_ca alone, without the CSR/sign/set-signed sequence,
  leaves the mount able to issue. If it fails, the four removed resources may be
  required and the design needs revisiting. Discovering that after writing the
  stack would be the expensive order.

Two things the plan carries from hard experience in this repo rather than from
the spec: read Terramate's output rather than its exit code, which has reported
0 over a failed run repeatedly; and the KMS key ring survives teardown by design
via prevent_destroy, so it must not be mistaken for a leak.

Self-review found two inconsistencies, both fixed inline: the startup script's
templated-variable list omitted region, kms_key_ring and kms_crypto_key, which
templatefile would have failed on at plan time; and dns.tf referenced
google_compute_address.openbao while the load-balancer step never named that
resource.

The .secrets.baseline entry is a false positive: detect-secrets' Secret Keyword
heuristic fires on a PROSE line naming manifest fields (caBundleSecretRef). A
pragma would have been visible text in the rendered document, so the baseline is
the right mechanism. The diff adds exactly one entry and suppresses nothing else.

Evidence: validate-links.sh exit 0; no placeholders; the six Secret Manager
names are consistent across all eight tasks.
Task 1's fallback check said grep -c 'google_project_iam_custom_role\.' should
return at least 4, "two definitions, two references". Both halves were wrong:
grep -c counts matching LINES, and the trailing dot matches only REFERENCES —
resource declarations use a quote and never match. The real count is 3.

Caught by Task 1's reviewer, which noticed the implementer's report cited a
number that did not reproduce. The wiring was correct and independently
verified; only the check was bad.

Replaced with a grep for the specific reference, which names what it looks for
instead of asserting a bar that must be recomputed whenever a role is added.
C1: GCPWorkloadIdentity renders project-scoped ProjectIAMMember, so the
previous permission set (versions.access + versions.list + secrets.get +
secrets.list) let any holder enumerate every Secret Manager secret in the
project. Drop the three list/get permissions -- ESO's gcpsm provider only
needs versions.access to read a NAMED secret, and Task 8's ExternalSecrets
are written against two named secrets, not dataFrom.find.

The role stays project-wide by name even after this change (ProjectIAMMember
has no per-secret scope). Record that honestly in the resource comment and
point Task 8 at the stronger fix -- google_secret_manager_secret_iam_member
per secret, as opentofu/gcp/openbao/cluster/iam.tf already does for the
server certificate. Not applied here: Task 8 is unwritten, and the decision
belongs with its own OpenTofu.

Same guidance recorded in the design (A dependency on slice 5) and the plan
(Task 8) so neither document is silent about the residual blast radius.
I1: variables.tf declared openbao_version with no default and
variables.tfvars didn't set it, so tofu apply would prompt on a TTY or fail
outright in CI with "No value for required variable".

Default to 2.6.2, matching the AWS pin, and carry the Renovate marker plus
the openbao/openbao#3411 deadlock note. Also fix variables.tfvars's comment,
which asserted every non-project_id variable was left on its default while
one required variable had none.
I3: write_ca() unconditionally ran secret_read | jq -r '.ca // empty', which
assumes the AWS JSON shape. GCP's openbao-priv-gcp-ca-chain secret is raw
PEM by design (gcloud secrets create --data-file=ca-chain.pem, plan Task 3),
so bao ca --cloud gcp would either read nothing back or, under pipefail, die
on jq's own parse error before the script's friendly message ever printed.

Branch on $CLOUD: use the GCP payload as-is, keep the AWS `.ca` unwrap, and
guard the jq call explicitly so a parse failure reports through this
script's error path instead of jq's raw stderr. Also documents the two
secrets' shapes in usage() with a GCP example.
… install

I5: setup-local-disks.sh and startup-script.sh are joined into ONE
metadata_startup_script and share a single process. The missing-disk branch
ran exit 0, which under set -o errexit terminates the WHOLE combined script
-- not just the disk-setup half -- so a missing data disk means OpenBao is
never installed, configured or started, while the instance still boots,
joins the MIG and reports RUNNING.

Change that branch to exit 1: unlike AWS's optional ephemeral NVMe, this
stack's data disk is declared by compute.tf and always attached, so its
absence is a provisioning bug, not a state to boot past.

Separately, setup-local-disks.sh requires mkfs.xfs but xfsprogs was only
installed later, in startup-script.sh's apt-get -- the half that runs AFTER
disk setup. Move the xfsprogs install into setup-local-disks.sh, ahead of
its check_command calls, and drop it from startup-script.sh's package list.
…nges

I4: the MIG had no update_policy, so GCP's default OPPORTUNISTIC applies --
a new instance_template is adopted but the running instance is never
replaced. Bumping openbao_version or editing either boot script would make
tofu apply report success while the node keeps running the old template
indefinitely, with no drift signal on the next plan.

Add PROACTIVE/REPLACE with max_unavailable_fixed=1, max_surge_fixed=0 --
this is a single-node MIG, so there's no second instance to surge onto.
Same failure mode AWS hit on its launch template's default version
(autoscaling_group.tf, update_default_version = true).
Minor cleanup, no behavior change:

- stack.tm.hcl: description said the stack creates the KMS key and predates
  compute; both are false now (kms.tf reads it via data source, compute.tf
  shipped in the previous commit).
- versions.tf: 'every resource below' listed the KMS key ring/key as managed
  here; it's a data source.
- iam.tf: replaced the false 'this stack only runs tofu validate, never
  apply' with the real constraint -- the server-certificate secret (Task 3)
  must exist before this stack applies, an ordering enforced by the boot
  script's runtime fetch, not by OpenTofu.
- data.tf: 'Kept for Tasks 5/6' -- Task 5 (compute) shipped without using
  data.google_project.
- kms.tf: the stated reason for keeping KMS out of this stack's state was
  wrong. tofu destroy does NOT fail on a key ring/crypto key -- the
  provider's delete is a no-op that removes the object from state and
  returns success. The real reasons are (a) a rebuild's create then hits
  ALREADY_EXISTS against a key ring that still exists server-side, and (b)
  destroying google_kms_crypto_key schedules its VERSIONS for destruction
  while still exiting 0 -- a teardown that silently schedules the unseal
  key's destruction. Rewritten so a future re-test of 'does destroy fail'
  doesn't conclude the bootstrap step is cargo cult.
- variables.tf: data_disk_size_gb/openbao_data_path described 'raft
  storage'/'raft data'; this deployment runs storage "file".
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Rendered manifest diff — this PR vs main (desired state)

No changes to the rendered desired state. ✅

Smana added 8 commits August 24, 2026 23:50
…ents

Simplify pass over the branch. Three findings applied, one declined.

Removed three declarations that nothing references: data.google_project.this,
local.private_dns_zone and local.private_domain_name. Each carried a
tflint-ignore pragma and a paragraph justifying itself as "kept for Task 6" —
roughly fifteen lines of comment defending dead code for a task that is not
written yet. Task 6 can add them back when something consumes them. The
stale-comment commit earlier in this branch had already noticed data.tf's
justification was false ("Task 5 shipped without needing it") and left the
declaration in place anyway, which is how scaffolding becomes permanent.

Deduplicated the S3-bucket-region explanation, which appeared near-verbatim in
backend.tf and data.tf with the same example region. data.tf now points at
backend.tf, matching how it already defers there for the S3-not-GCS rationale.

Dropped one of two identical "(single-node, storage file -- not raft)"
parentheticals in variables.tf; compute.tf's header already establishes it, and
the qualifier is only genuinely useful on the disk-size variable, where asking
how big invites the raft question.

DECLINED: factoring secret_write()'s per-cloud control flow into a shared
skeleton. The exists-check/create-or-update/error-handle shape is written once
per cloud and only four lines differ, so roughly fifteen lines are recoverable —
but it trades directness for indirection in a bash script that is read rarely
and usually while something is broken. The reviewer weighed it moderate-value
and left the call to me; directness wins here.

Also recorded in the design's Risks section that xplane_secret_reader's grant is
project-wide until Task 8 narrows it, and that it must be narrowed or removed
rather than inherited if Task 8 slips. That debt was previously visible only in
a code comment, unlike the PKI two-anchor interim state which the Risks section
already flags.

Evidence: tofu validate Success, fmt clean, tflint exit 0 (confirming no removed
pragma masked a real unused declaration), trivy 0 findings, validate-links.sh
exit 0.
…trap

Running the documented bootstrap on a fresh project failed:

  ERROR: PERMISSION_DENIED: Google Cloud KMS API has not been used in project
  323586397743 before or it is disabled.

gcloud offers to enable it interactively, but the prompt defaults to no, so a
non-interactive run just fails — and the message reads like a permissions
problem rather than a missing API.

Neither the design nor the plan mentioned it. Added to kms.tf's bootstrap
comment, which is where someone hitting this will look.
The design said the instance service account needs
roles/cloudkms.cryptoKeyEncrypterDecrypter "and nothing else". That is wrong,
and no review caught it — only a live boot did:

  Error configuring seal "gcpckms": error checking key existence:
  PermissionDenied: Permission 'cloudkms.cryptoKeys.get' denied on resource
  .../cryptoKeys/openbao-unseal (or it may not exist).

OpenBao's gcpckms seal verifies the key EXISTS before using it, and
cryptoKeyEncrypterDecrypter grants encrypt/decrypt without cryptoKeys.get.
Added roles/cloudkms.viewer, the least-privileged predefined role carrying that
permission, bound at the crypto-key level so it sees only this one key.

The failure mode is the quiet one this stack was written to avoid: the seal is
configured after the process starts, so the instance reaches RUNNING and joins
the MIG while openbao.service crashloops. Nothing short of the serial console
says why.
Task 6 of the plan. Makes the OpenBao node reachable at
bao.priv.gcp.ogenki.io:8200 from the tailnet.

Passthrough LB, not a proxy: OpenBao terminates TLS with a certificate from the
offline root, and a proxying LB would need that private CA in its trust store
or would terminate TLS itself. Passthrough forwards the stream untouched so the
client verifies OpenBao's own certificate end to end.

The health check is TCP rather than HTTPS for the same reason — Google's probers
cannot be given a private CA. TCP also deliberately does not answer 'is OpenBao
sealed', because a sealed node still has to be reachable for an operator to
unseal it.

The DNS record is load-bearing, not cosmetic: the server certificate carries
DNS:bao.priv.gcp.ogenki.io and no IP SAN, so connecting to the load balancer by
address cannot verify TLS. local.fqdn derives both the record and the endpoint
output so they cannot drift.

Firewall is scoped by target service account and source range rather than
opened to the VPC — the GKE nodes share this network and have no business
reaching OpenBao's API directly. The health-check ranges are separate: without
them the backend marks every instance UNHEALTHY and the forwarding rule
blackholes traffic, with the only symptom buried in backend health status.

IAP SSH stays off by default. Worth noting the crashlooping openbao.service
debugged during this deploy was diagnosed entirely from the serial console,
which needs no firewall rule at all.
The provider defaults a backend to UTILIZATION, which an INTERNAL backend
service rejects outright:

  Error 400: Invalid value for field 'resource.backends[0].balancingMode':
  'UTILIZATION'. Balancing mode must be CONNECTION for an INTERNAL backend
  service.

Passthrough load balancers distribute connections rather than requests, so
there is no utilization signal to balance on. Recorded the error verbatim in
the file, because the fix is not guessable from the field name.
Deployed the OpenBao stack against real GCP, verified the PKI chain end to end,
and tore it down to zero billable resources.

Three design criteria proven: auto-unseal without operator input (criterion 2),
a chain verifying to the offline root with a DNS-only SAN (criterion 3), and a
teardown that removes everything billable (criterion 8). Plus the design's one
untested premise — that config_ca alone leaves the mount able to issue, which
justifies deleting four resources the AWS stack uses. It holds: the import
produced an issuer and a key, a role issued a leaf, and the leaf verified to the
offline root.

Nine findings. Four fixed on this branch, five recorded.

The one that matters most: the design said the instance service account needs
cloudkms.cryptoKeyEncrypterDecrypter 'and nothing else'. False. The gcpckms seal
checks the key EXISTS before using it, which needs cryptoKeys.get. Three reviews
read that sentence and none could have caught it — the seal is configured after
the process starts, so the instance reaches RUNNING and reports healthy while
openbao.service crashloops.

The worst still-open one is pre-existing: the network stack's Tailscale
split-DNS is EMPTY after a first apply, because depends_on on a data source does
not wait for the inbound DNS policy to finish allocating its address. The deploy
reports success and no tailnet client can resolve the private zone at all. A
second apply fixes it. Verified the VPC resolver was never at fault.

Also open: systemd gives up permanently after three rapid failures, and the MIG
still has no auto_healing_policies, so a transient boot problem leaves a dead
service on a RUNNING instance forever.

Records what remains unverified rather than implying completeness: no
cert-manager Certificate was issued (needs Tasks 7-8 and a cluster), no
0-change-plan check, and the config_ca probe used a throwaway mount rather than
the one the management stack will create.
Finding 7 from the live deploy. The network stack builds the Tailscale split
nameserver from a data source filtered on purpose = DNS_RESOLVER, with
depends_on the inbound DNS policy. That is not sufficient: creating the policy
RETURNS before GCP finishes allocating the resolver address, and depends_on
defers the data-source read until after the policy RESOURCE exists, not after
the ADDRESS does.

So a first apply matched nothing and wrote nameservers = []. Nothing failed. The
deploy reported success while every tailnet client silently could not resolve
*.priv.gcp.ogenki.io — the VPC resolver was fine, the private zone was fine,
there was simply no nameserver configured. Measured 2026-08-25: first apply
produced [], re-apply produced ["10.10.0.2"]. Confirmed the VPC side was never
at fault by querying the resolver directly throughout.

Two changes, and the second matters more than the first:

- A 60s time_sleep between the policy and the data read, using the same
  hashicorp/time provider the AWS llm-platform stack already uses for EFS
  mount-target propagation.
- A precondition asserting the address list is non-empty. A fixed wait alone
  would still degrade to the original bug on a slow allocation — a successful
  apply configuring no resolver. The precondition converts that silent failure
  into a loud one naming the consequence, which is recoverable by re-running.

The bug was pre-existing and not introduced by the OpenBao work, but it blocks
reaching OpenBao, so it belongs to this workstream.
Finding 7 moves from OPEN to FIXED (f9222ed), and the header count with it.

Notes explicitly that the fix is not verified against a live first apply —
proving it needs another full network build and teardown. What the precondition
does buy is that the untested path now fails loudly rather than silently, which
was the actual defect.
@Smana
Smana merged commit f10a2c2 into main Aug 25, 2026
8 checks passed
@Smana
Smana deleted the worktree-gcp-openbao branch August 25, 2026 06:07
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