Skip to content

sec(secrets): resolve clientId/clientPassword from the existing Secret - #859

Merged
LukasWodka merged 5 commits into
developfrom
feat/2571-client-creds-existing-secret
Aug 27, 2026
Merged

sec(secrets): resolve clientId/clientPassword from the existing Secret#859
LukasWodka merged 5 commits into
developfrom
feat/2571-client-creds-existing-secret

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Part of tracebloc/backend#2571 — the structural half.

Deliberately notCloses: #2571 also carries the Docker Hub PAT rotation and the
credential rotations, which are human actions this PR cannot perform. Auto-closing the
ticket on merge would retire those with it. Per org-standards (.github#354), partial
work says Part of and keeps the ticket number out of the PR subject.

The problem

clientId and clientPassword were required in both the template and values.schema.json's top-level required, with minLength: 1. They therefore had to be supplied as values on every install and every upgrade — which means they were necessarily written into the Helm release's user-supplied values, in cleartext, in every retained revision (10 by default).

Measured on our own fleets: 3 fleets × 10 revisions, each a readable copy. Rotating the credential does not clear the older revisions, and anyone with get secret in the namespace can read all of them. Unlike the chart's other five credentials, there was no way to avoid it.

The fix

Three-tier resolution — the shape this file already uses for podTokenSigningSecret, credmgrPassword, tbMetaPassword, tbIngestPassword and bootstrapDbPassword:

  1. explicit values, else
  2. the CLIENT_ID / CLIENT_PASSWORD keys already in the live Secret, else
  3. fail, naming both remedies.

Tier 3 is a hard failure rather than randAlphaNum — the backend issues these, so the chart must never invent one. An operator can now pre-create the Secret, or install once with values and drop them on the next upgrade, and the credential never enters release values.

The part worth reviewing closely: the schema was the real enforcement

Changing only the template was a no-op. JSON Schema validation runs before rendering, so an absent or empty value was rejected before lookup could fire — the template's own refusal was dead code and tier 2 was unreachable.

So this PR removes both from the schema's required and drops minLength: 1. Enforcement moves to the template, which is the only layer that can see the Secret. Placeholder rejection stays in the schema and is now applied to the resolved value, so a badly pre-created Secret is caught too.

I found this by mutation testing, not by reading. Worth a reviewer's eye on whether relaxing the schema is acceptable — my argument is that the template's tier 3 fails closed with a strictly better message and covers a path the schema cannot see.

Test coverage — including what is NOT covered

The unit suite cannot observe tier 2.lookup is inert under helm-unittest: deleting the entire tier-2 branch leaves all 30 unit tests green. Measured, not assumed. Shipping on unit tests alone would have meant the central mechanism of this PR was unverified.

So tier 2 is covered where it can be — a new path 5 in scripts/tests/e2e-auto-upgrade.sh, which already installs on real k3d. It upgrades with --reset-values (every user value discarded, so the credentials are genuinely absent) and asserts they survive in the Secret. If they do, the lookup is the only thing that could have supplied them.

Vacuous assertions fixed on the way

failedTemplate with errorPattern is silently ignored in helm-unittest 0.5.2. A pattern appearing nowhere in the output still passes. Proven by mutating two messages that such tests claim to assert — 49 tests stayed green. My own first draft used errorPattern and was completely hollow.

  • Converted this file's two to errorMessage (honoured, exact) and re-ran the same mutation: it now reddens.
  • Six more live in five other test files (auto_upgrade, image_refresh, network_policy, priority_class_pdb, rbac). Filed separately rather than dragged into a credentials PR.
  • should reject empty credentials was passing on schema validation, not on anything in secrets.yaml. Its bare failedTemplate: {} could not tell the difference, and this change moved which layer refuses it.
  • Placeholder tests stay bare deliberately: schema failures surface as a chart-load error no message assertion can match. Commented rather than dressed up.

Test plan

  • helm unittest client579/579 pass, 34 suites
  • helm lint clean
  • Offline render without credentials fails with the tier-3 message at secrets.yaml:40
  • Offline render with credentials produces 56 documents carrying CLIENT_ID/CLIENT_PASSWORD
  • Mutation: break the tier-3 message → 2 tests redden
  • Mutation: break mysqlRootPassword must be alphanumeric → reddens now, was green before the conversion
  • Mutation: delete the tier-2 branch → 30 unit tests stay green (the gap path 5 exists to close)
  • e2e-auto-upgrade.sh path 5 on a real k3d cluster — needs a CI run; GitHub runners were starved when this was written

Note on rollout

This does not remove the credentials already in our fleets' release values. That is an operational follow-up on backend#2571, and it must be coordinated: dev and prod currently share the same clientPassword, so it cannot be rotated per-fleet.

Refs: backend#2571 · backend#947 · backend#1528

🤖 Generated with Claude Code


Note

High Risk
Changes how platform authentication credentials are sourced and relaxes JSON Schema while shifting enforcement to templates; installer one-client detection behavior inverts for id-only-in-Secret releases.

Overview
clientId and clientPassword no longer have to stay in Helm values on every upgrade. The secrets template resolves them in three tiers—explicit values, then keys on the live release Secret via lookup, then a hard fail—matching other credentials in the chart but without a generated fallback because the backend issues these values.

Schema enforcement moved to the template.clientId / clientPassword were removed from top-level required and lost minLength: 1 so empty values can reach render and tier 2 can run; missing credentials are refused in secrets.yaml, and empty Secret keys count as absent so blank credentials cannot ship to pods.

Install and upgrade guards follow the same contract. Bash (_client_id_from_secret) and PowerShell (Get-ClientIdFromSecret) read CLIENT_ID from <release>-secrets when it is missing from helm get values; a client-chart release with no id in values or Secret is treated as unidentifiable and blocks another install instead of being ignored.

Tests and docs spell out limits. Helm unittest cannot exercise lookup (tier 2); e2e-auto-upgrade path 5 upgrades with --reset-values to prove Secret resolution. values.yaml documents Helm Secret adoption and that client-side GitOps renderers without cluster access must still keep these two in values.

Reviewed by Cursor Bugbot for commit 77f86fb. Bugbot is set up for automated code reviews on this repo. Configure here.

Both were `required` in the template AND in values.schema.json's top-level
`required` with `minLength: 1`. So they had to be supplied as values on every
install and every upgrade -- which meant they were necessarily written into the
Helm release's user-supplied values, in cleartext, in EVERY retained revision
(10 by default). Rotating the credential did not clear the older revisions, and
anyone with `get secret` in the namespace could read all of them. Measured: 3
fleets x 10 revisions.
They are now resolved in three tiers, the shape this file already uses five
times over (podTokenSigningSecret, credmgrPassword, tbMetaPassword,
tbIngestPassword, bootstrapDbPassword):
1. explicit values, else
2. the CLIENT_ID / CLIENT_PASSWORD keys already in the live Secret, else
3. fail, naming both remedies.
Tier 3 is a HARD FAILURE, not `randAlphaNum`: the backend issues these, so the
chart must never invent one. An operator can now pre-create the Secret, or
install once with values and drop them afterwards, and the credential never
enters release values.
THE SCHEMA WAS THE REAL ENFORCEMENT. Changing only the template was a no-op --
JSON Schema validation runs BEFORE rendering, so an absent or empty value was
rejected before `lookup` could fire and the template's own refusal was dead
code. Removing clientId/clientPassword from `required` and dropping
`minLength: 1` is what actually makes tier 2 reachable. Enforcement moves to the
template, which is the only layer that can see the Secret; placeholder
rejection stays in the schema and is now also applied to the RESOLVED value, so
a badly pre-created Secret is caught too.
Tests. The unit suite CANNOT observe tier 2: `lookup` is inert under
helm-unittest, and deleting the whole tier-2 branch leaves all 30 unit tests
green. So the mechanism is covered where it can be -- a new path 5 in
scripts/tests/e2e-auto-upgrade.sh upgrades a real k3d install with
`--reset-values` (every user value discarded, so the credentials are genuinely
absent) and asserts they survive in the Secret. If they do, the lookup is the
only thing that could have supplied them.
Also fixes vacuous assertions found while doing this:
* `failedTemplate` with `errorPattern` is SILENTLY IGNORED by helm-unittest
0.5.2 -- a pattern appearing nowhere in the output still passes. Proven by
mutating two messages that such tests claim to assert: 49 tests stayed
green. Converted this file's two to `errorMessage` (honoured, exact) and
re-ran the same mutation: it now reddens. Six more live in five other test
files and are filed separately rather than dragged into this PR.
* "should reject empty credentials" was passing on SCHEMA validation, not on
anything in secrets.yaml -- its bare `failedTemplate: {}` could not tell the
difference, and this change moved which layer refuses it.
* Placeholder tests stay bare, deliberately: schema failures surface as a
chart-load error that no message assertion can match. Commented rather than
dressed up.
Evidence: 579/579 unit tests pass; helm lint clean; offline render without
credentials fails with the tier-3 message at secrets.yaml:40; with credentials
renders 56 documents carrying CLIENT_ID/CLIENT_PASSWORD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment threadclient/values.yaml
Comment threadclient/values.yaml Outdated
Comment threadclient/templates/secrets.yaml Outdated
Comment threadclient/values.yaml
Comment threadclient/templates/secrets.yaml Outdated
Comment threadclient/templates/secrets.yaml Outdated

@saqlainsyed007saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direction is right and public-repo hygiene is clean — reading the credentials from an existing Secret so they never live in release values is exactly the backend#2571 goal. Requesting changes on one security-correctness gap, with two workflow issues to fix-or-scope. Details are in the three inline threads.

Blocker — empty stored key silently renders an empty credential (templates/secrets.yaml:38, and 46 for the password). The old code used required, which fails on an empty string. Tier 2 now takes the branch on hasKey $existingSecret.data "CLIENT_ID", and hasKey is true for a present-but-empty key — so a Secret carrying CLIENT_ID: "" resolves $clientId = "", the tier-3 fail (absent-key only) never fires, and the placeholder guard regexMatch "^<.*>$" doesn't match "". The chart then renders an empty credential and the client authenticates with nothing. Dropping minLength/required left no non-empty check on the resolved value. Please restore one (e.g. fail when the resolved $clientId/$clientPassword is empty), so the PR's own promise — "a badly pre-created Secret is caught" — actually holds.

Please address or explicitly scope in docs:

  • Greenfield adoption (values.yaml:999): the documented preferred flow (pre-create the Secret out of band, empty values) fails a first helm install — Helm 3 won't adopt a kubectl-created Secret that lacks app.kubernetes.io/managed-by: Helm, aborting with "invalid ownership metadata". Tier 2 only works once Helm already owns the Secret (install-with-values-then-drop). Worth stating that ordering in the README, or the "preferred" label is misleading.

  • GitOps / helm template (secrets.yaml:40): lookup is inert under helm template, --dry-run, and client-side ArgoCD/Flux rendering, so tier 2 can't fire there and operators must keep clientId/clientPassword in values — the cleartext-in-revisions problem this PR targets. If GitOps isn't a supported render path for this chart, a one-line caveat is enough; if it is, tier 2 alone doesn't cover it.

Two minor, non-blocking (not posted inline): tier 2 (the central mechanism) has no green test yet — its only coverage is e2e path 5 with the checkbox unchecked; per the repo's "evidence, not assertion" bar, land that run before merge. And path 5's helm upgrade --reset-values discards overrides set by earlier paths — harmless as the last path today, a latent trap if another is appended after it.

…dential
Three Bugbot findings on #859, all real. Each one is a consequence of the
same change -- dropping `required`/`minLength` so tier 2 could run -- so
they are fixed together rather than singly.
1. HIGH: the preferred remedy could not work on a first install.
secrets.yaml ALWAYS emits `<release>-secrets`, so the values.yaml advice
to `kubectl create secret` before installing produced a Helm ownership
error, and the tier-3 message named that same broken remedy. Helm adopts
a pre-existing object carrying its three ownership fields, so values.yaml
now gives the label + annotate commands alongside the create, says the
names must match the install exactly, and leads with the simpler
install-once-then-drop path. The fail message points at it.
2. HIGH: dropping clientId from values hid the live client.
detect_installed_client read "values readable, no clientId" as NOT A
CLIENT -- true while clientId was `required`, false the moment this chart
told operators to drop it. The one-client guard compares on a non-empty
id, so a client installed the new way was invisible and a re-run could
re-point the machine. The id is now read from the release Secret when
values do not carry it, and a client-chart release naming an id in
NEITHER place is UNKNOWN (fail closed), not absent. Same fix in the
PowerShell peer, which had the identical null-clientId `continue`.
3. MEDIUM: an empty Secret key counted as resolved.
Tier 2 keyed off `hasKey`, so `CLIENT_ID: ""` resolved and helm shipped
blank credentials -- the case `minLength: 1` used to catch before it had
to come out. Tier 2 now tests the DECODED value, so an empty key reads as
absent and falls to tier 3.
TESTS. detect_installed_client's own scanning loop had no test at all --
every existing test stubs the function out, which is why nothing caught (2).
Four added, and each is mutation-proven: removing the Secret fallback
reddens 2 of them, dropping the fail-closed reddens the other 2, and
restoring returns all four green. The empty-key path is unobservable to
helm-unittest (`lookup` is inert there, as this PR already documents), so it
is asserted in e2e-auto-upgrade.sh where tier 2 is real: blank the key,
upgrade, require the failure AND require it to name clientId.
Chart.yaml 1.9.72 -> 1.9.73 (version + appVersion) for the version-bump
gate; manifest.sha256 regenerated for the two installer scripts.
Verified: helm-unittest 579/579, make lint 0, make drift 20/20 guards green,
helm-lint/vocab/template clean, the 4 new bats tests green + both mutations.
NOT verified locally: Pester and the full bats file both hang on this Mac at
`DEFAULT (TB_STORAGE_MODE unset)` -- reproduced on the UNMODIFIED PR head, so
it is environmental, and neither runs on darwin in CI (ubuntu + windows only).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment threadscripts/lib/install-client-helm.sh
…ts there
Saqlain's third thread on #859, the one no code change answers: tier 2 is a
`lookup`, so it needs a live cluster. The template already said that about
`helm template`/`--dry-run`; it did not say the same limit applies to every
CLIENT-SIDE RENDERER -- ArgoCD's default renderer and Flux post-render among
them -- and that is where it actually bites someone.
The asymmetry is the part worth writing down. The chart's other five
credentials survive an inert `lookup` because their tier 3 is `randAlphaNum`,
so a value-free render degrades to a generated secret. clientId/clientPassword
cannot: the backend issues them, so tier 3 is a hard `fail` and the render
STOPS rather than degrading. A GitOps install therefore has to keep both in its
values -- which means the cleartext-in-revisions problem this PR fixes is NOT
fixed for that deployment style.
Said plainly in both places an operator reads, because a limitation discovered
at install time is a support ticket: values.yaml (planning) and secrets.yaml
(the mechanism). Both name what makes the value-free path reachable -- render
against a live cluster -- and secrets.yaml names the out-of-scope remedy for
the rest (an external secrets operator writing the Secret before render) so
the gap is bounded rather than open.
Docs only; no template logic changes, so the resolution tiers and their tests
are untouched.
Verified: make check green (parse, shellcheck, drift 20/20, helm-lint,
chart-env-vocabulary 50/50) and helm-unittest 579/579 on this tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@LukasWodkaLukasWodka changed the title sec(2571): resolve clientId/clientPassword from the existing Secretsec(secrets): resolve clientId/clientPassword from the existing SecretAug 27, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 13ab07f. Configure here.

@saqlainsyed007saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 13ab07f. The blocker is resolved — thanks for the thorough fixes.

Blocker (empty stored key → empty credential): verified fixed. Tier 2 now gates on the decoded value's truthiness, not hasKey: a pre-created Secret carrying CLIENT_ID: "" decodes to $secretClientId = "", the {{- else if $secretClientId -}} branch is falsy, and resolution falls through to the tier-3 fail. An empty key correctly reads as ABSENT instead of silently rendering a blank credential into the pod. Same for clientPassword. The placeholder guard running on the resolved value is a good addition. All three of my threads (greenfield adoption ordering, the empty-key blocker, the GitOps/helm template caveat) are addressed and resolved — I'm satisfied on the code.

Holding my approval only on CI — and it's this PR's own test. The Pester suite is red (ubuntu + windows) with exactly one failure:

[-] values without a clientId key do not trip the guard — it aborts with InvalidOperationException: A 'break' or 'continue' statement with a label that does not match any enclosing loop, which per Pester #2669 kills the block with no result rather than asserting. That's the test covering the very guard this PR changes, so it needs to go green (the mislabeled break/continue looks like the immediate cause). Unit tests / bats / Bugbot are still pending too.

Get CI green and I'll approve — the code side is no longer blocked on me.

LukasWodkaand others added 2 commits August 27, 2026 06:57
Pester went red on both ubuntu-latest and windows-latest at e0981f4 and it was
a real regression, not the environment. The commit that fixed Bugbot's
fail-open finding changed detect_installed_client's contract and left the
PowerShell test asserting the OLD one:
[-] values without a clientId key do not trip the guard
That test read: a readable client release carrying no `clientId` is NOT a
client, so the installer upgrades over it. True only while `clientId` was
`required`, which made a clientId-free client release impossible. This PR drops
that requirement and tells operators to remove clientId from values once the
Secret holds it -- so the test was pinning the exact fail-open the finding was
about. Both earlier attempts at this fix missed it, because Pester does not run
on darwin.
REPRODUCED LOCALLY FIRST, rather than inferred from the CI log. pwsh 7.5.2,
filtered to the one test: the installer prints "Refusing to replace an
unidentifiable existing client" and refuses -- which is the new contract working
exactly as intended, failing a test that wanted the old one.
The test is rewritten to the new contract, not the fix reverted:
* renamed to what it now checks -- fails CLOSED on a readable client release
with no clientId in values or Secret. `kubectl` has no cluster under Pester,
so Get-ClientIdFromSecret returns "" and the both-places-empty path is what
gets exercised, which is the case worth pinning.
* NAMES THE REFUSAL (CLAUDE.md rule 10). Every other fail-closed path in this
Describe also throws, so a bare `Should -Throw` would have passed on the
wrong refusal -- an unreadable-values or garbage-list abort is
indistinguishable from the one the test is named for. It asserts
`Should -Invoke Err -ParameterFilter { $m -match 'unidentifiable existing
client' }` as well as the throw and the absent upgrade.
* carries the why above it, so the next reader does not "fix" it back.
MUTATION-PROVEN, and the anchor was asserted rather than assumed: restoring the
pre-fix `if ($null -eq $vals -or $null -eq $vals.clientId) { continue }` reddens
it (PASSED=0 FAILED=1); restoring the fix greens it. The mutation script aborts
if its anchor does not match, so an inert mutation cannot read as coverage.
Also dropped the trailing bare `continue` e0981f4 added. It was the last
statement of the loop body, so it bought nothing -- and PowerShell reported it
escaping as an unmatched loop label (pester/Pester#2669), which aborts the whole
run rather than failing one test. The two `continue`s above it are load-bearing
and stay. manifest.sha256 regenerated for install-k8s.ps1.
Verified on this tree: full Pester suite 784 total / 770 passed / 0 failed / 14
skipped (was 1 failed), and `make check` green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bugbot Medium on #859, and it is right that this PR is what makes it matter.
`_client_id_from_secret` / `Get-ClientIdFromSecret` issue a fresh
`kubectl get secret` with no `--request-timeout`, and kubectl's default is no
timeout at all -- it waits forever.
WHAT CHANGED THE RISK. Before this PR the fallback did not exist; the id was
always in release values. This PR tells operators to DROP clientId from values,
so once they do, every scanned client release reaches this call on every
detect_installed_client / Get-InstalledClientInfo. That runs in the
pre-provision pre-flight and the Helm-step one-client guard, both of which can
run headless -- so a wedged API server hangs the install with no further output
and no way to tell what it is waiting on. The enumeration around it is already
bounded (Test-ApiReachable, and the `helm list` gate that documents exactly this
hazard); this one call was the hole left in it.
--request-timeout=5s on both twins. 5s is this repo's existing figure for a
cheap existence probe rather than a new number -- install-k8s.ps1 already uses
it for the namespace, daemonset and allocatable-GPU reads.
A TIMEOUT LANDS IN A PATH THAT ALREADY EXISTS, which is why this is a one-flag
change: kubectl exits non-zero, which both twins already treat as "could not
read" (`|| return 0`, `return ""`). The caller turns that into an UNIDENTIFIABLE
client, not an absent one, so a timeout fails CLOSED -- the guard refuses rather
than waving through an install that re-points a live machine. The fail-open this
PR set out to close is not reopened by the fix for it.
Fixed in both languages. The bash and PowerShell readers are twins by design
(they name each other), and fixing one would split them -- the divergence class
backend#2220 found five of.
VERIFIED THE STUBS STILL MATCH, because that was the real risk of adding an
argument. install-client-helm.bats:875 keys its kubectl stub on POSITION --
`[ "$5" = "liverel-secrets" ]` -- so a flag inserted mid-vector would have
silently broken three tests. The flag is appended last, leaving $1..$7 intact;
exercised against a copy of that exact stub, `_client_id_from_secret liverel
munich` still returns `uuid-from-secret`. Both no-kubectl and non-zero-kubectl
(the timeout shape) still return empty with rc 0, as the contract requires.
manifest.sha256 regenerated for both scripts.
Verified: full Pester 784 total / 770 passed / 0 failed / 14 skipped; `make
check` green; `bash -n` + `shellcheck -S warning -x` clean on the bash twin;
install-k8s.ps1 parses clean. The full bats file hangs on this Mac before
reaching these tests (reproduced on the unmodified PR head by a previous
session, and bats runs on ubuntu in CI, not darwin) -- hence the direct
stub-fidelity check above rather than a claim I did not make.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

@saqlainsyed007 — all three of your threads are addressed and resolved, plus two things CI found after your re-review. Summary of what moved since 13ab07f:

Your threads

  1. Pre-created Secret / Helm ownership — took your first option and reordered: values.yaml leads with install-once-then-drop (no adoption step at all), and the pre-create path now carries the label/annotate commands for Helm's three ownership fields, with the note that <release>/<ns> must match the install exactly. Both tier-3 fail messages point there instead of at the bare create secret.
  2. Present-but-empty key — took your suggestion with one shape change: the decode happens once into $secretClientId / $secretClientPassword and the tier-2 branch tests that for truthiness, rather than | b64dec inside the condition (which decodes twice). Same semantics for the empty case.
  3. GitOps / client-side renderers — documented in 13ab07f, since no code change answers it. Your asymmetry framing is what I wrote down: the other five degrade to randAlphaNum on an inert lookup; these two can't, so tier 3 is a hard fail and the render stops. Both values.yaml and secrets.yaml now say outright that a GitOps install must keep both in values and that the cleartext-in-revisions problem is not solved for it.

Two found after that, both real

  1. Pester was red on ubuntu and windows, and it was a genuine regressione0981f4 inverted detect_installed_client's contract and left the PowerShell test asserting the old one ("values without a clientId key do not trip the guard"). That test was pinning the exact fail-open finding Develop #2 was about. Reproduced locally on pwsh 7.5.2 first, then rewrote it to the new contract, named the specific refusal (Should -Invoke Err -ParameterFilter { \$m -match 'unidentifiable existing client' } — every other fail-closed path here also throws, so a bare Should -Throw would pass on the wrong refusal), and mutation-proved it: restoring the pre-fix continue reddens it, restoring the fix greens it. Also dropped a trailing bare continue that PowerShell reported escaping as an unmatched loop label.
  2. Bugbot Medium: the Secret read had no --request-timeout — and this PR is what makes that matter, since dropping clientId from values puts the call on the common path of two headless code paths. --request-timeout=5s on both twins (the repo's existing figure). A timeout exits non-zero into the existing "could not read" branch, which the caller turns into an unidentifiable client — so it fails closed and does not reopen the fail-open.

Verified: full Pester 784 total / 770 passed / 0 failed / 14 skipped (was 1 failed); helm-unittest 579/579; make check green; bash -n + shellcheck -S warning -x clean.

The PR was also retitled (ticket number out of the subject) and the body now says Part of tracebloc/backend#2571 rather than Closes — #2571 also carries the PAT and credential rotations, which are human actions this PR can't perform, so auto-closing it on merge would retire those with it.

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 77f86fb. Configure here.

@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

@saqlainsyed007 CI is green at 77f86fb — the condition you held on is met.

Pester (ubuntu-latest) and Pester (windows-latest) both pass; the mislabeled break/continue in values without a clientId key do not trip the guard was the cause, as you read it. Full rollup at this head: 45 SUCCESS, 4 skipped, 0 failing. Bugbot's latest review (77f86fb) found no new issues, and all review threads are resolved.

Re-requesting your review so the change-request can clear.

@saqlainsyed007saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 77f86fb. Everything I raised is resolved and CI is green.

  • The blocker (empty stored key → empty credential) stays fixed: tier 2 gates on the decoded value's truthiness (else if $secretClientId → tier-3 fail), and templates/secrets.yaml is unchanged since I verified it at 13ab07f.
  • CI is green. The Pester failure I flagged — values without a clientId key do not trip the guard, aborting on the unmatched-loop-label bug (Pester #2669) — is fixed by dropping the no-op trailing continue in Get-InstalledClientInfo, keeping the two real ones above.
  • The delta since my review is sound and I'd own it. Adding --request-timeout=5s to the kubectl get secret CLIENT_ID read on both peers (install-k8s.ps1 and install-client-helm.sh) is the right call now that this read is on the common path once clientId leaves release values — an unbounded read against a wedged API would hang a headless install, and the timeout falls into the same fail-closed return ""/return 0 (unidentified client, never absent). manifest.sha256 re-pinned to match.

All three of my original threads (greenfield adoption ordering, the empty-key blocker, the GitOps caveat) are addressed and resolved. Nice work on the credential-hygiene hardening — LGTM.

@LukasWodka
LukasWodka merged commit c83001c into developAug 27, 2026
49 checks passed
@LukasWodka
LukasWodka deleted the feat/2571-client-creds-existing-secret branch August 27, 2026 05:52

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answering the question you asked directly: yes, relaxing the schema is acceptable, and it's a net increase in enforcement rather than a trade. I checked the two things that decide it.

Nothing the schema used to catch is now uncaught. minLength: 1 refused an empty value; the template refuses an empty resolved credential by truthiness — if .Values.clientId / else if $secretClientId / else fail, where "" is falsy in both tiers, so an empty CLIENT_ID in a pre-created Secret reads as ABSENT and lands on the hard fail. Keying tier 2 off the decoded value rather than hasKey is what makes that true, and it's the difference between this and a version that ships blank credentials into the pods.

And the placeholder guards now run on $clientId/$clientPassword rather than .Values.*, so a Secret pre-created with <CLIENT_ID> is refused — a path the schema could never see. Strictly more coverage, one layer lower.

The rest I'd have raised is already in the file, which is why this reviewed quickly:

"The unit suite cannot observe tier 2." Deleting the entire tier-2 branch leaves all 30 unit tests green because lookup is inert under helm-unittest — measured, not assumed, and remedied by putting the coverage on real k3d with --reset-values. That's the check-that-can-observe-the-property problem, found and fixed by the author on their own PR. It's the single most common defect I've seen this week and normally it's the reviewer who has to find it.

The GitOps limit, stated rather than left to be discovered.lookup needs a live cluster, so ArgoCD's default renderer and Flux post-render hit tier 3 — and unlike the other five credentials there's no randAlphaNum to degrade to, so it stops rather than degrading. Saying outright that "a GitOps-rendered install must keep clientId/clientPassword in its values, and for it the cleartext-in-revisions problem this change fixes is NOT fixed" is the honest scoping, and naming the external-secrets mechanism that would close it while declaring it out of scope is the right place to stop.

Part of rather than Closes, because #2571 also carries rotations a PR can't perform — correct, and matching .github#354 which landed yesterday.

One thing I could not confirm:mergeStateStatus came back UNKNOWN/UNKNOWN on three separate reads several minutes apart, so I have no confirmation this is conflict-free — checks are green and there are no open threads, but treat the merge state as unverified rather than clean and re-check before merging. My approval is about the code. 👍

@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Correction to my own commit message on 77f86fb. It says the full bats file "hangs on this Mac" and offers a stub-fidelity check in place of running it. The first half is wrong and I would rather correct it here than leave it standing in the history.

bats does not hang. It is slow — about 25 minutes on this machine — and completes:

$ bats scripts/tests/install-client-helm.bats # on 77f86fb, clean tree
BATS_EXIT=0
ok: 235
not ok: 0

I inherited the claim from an earlier session's commit message on this branch (e0981f4, which reported it hanging at DEFAULT (TB_STORAGE_MODE unset)) and repeated it after my own first run appeared to stall. It had not stalled; I had not waited long enough. Verifying an inherited claim before restating it is the thing I skipped.

This also retires a second inherited claim.370251f's message reported one pre-existing failure, fresh arm64 auto -> values carry the 8.4 mysqlClient block, "which aborts the run at 174 of 232". There is no such failure on this head — 235 of 235 pass, and the tests in that neighbourhood (174–179, the mysql-engine and arm64 cases) are all green.

What this changes about the evidence for this PR: nothing is weakened, and the four Secret-fallback tests are now covered behaviourally rather than by my hand-rolled stand-in.

ok 68 detect_installed_client: no clientId in values -> reads it from the release Secret (backend#2571)
ok 69 detect_installed_client: no clientId in values AND no readable Secret -> UNKNOWN, never 'no client'
ok 70 detect_installed_client: an EMPTY CLIENT_ID in the Secret is not an id -> UNKNOWN
ok 71 install_client_helm: a client with its id only in the Secret still blocks a DIFFERENT client

Test 68 is the one that mattered for the --request-timeout=5s addition: its kubectl stub keys on argument position ([ "$5" = "liverel-secrets" ]), so a flag inserted mid-vector would have broken it silently. It passes, which is the real confirmation the flag is appended rather than inserted — my earlier stub replica only predicted that.

Full evidence for this head, all of it now actually run: bats 235/235, Pester 784 total / 770 passed / 0 failed / 14 skipped, helm-unittest 579/579, make check green, and every CI check on 77f86fb passing.

aptracebloc added a commit that referenced this pull request Aug 27, 2026
…9.74)
Only manifest.sha256 conflicted (regenerated); develop #859 also reached chart
1.9.73, so bump to 1.9.74. GPU handling intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@LukasWodka@saadqbal@saqlainsyed007