feat(proxy,deploy): split client cert/key, and let the chart issue both - #111
Merged
Merged
Conversation
The inference proxy could only take its Control API client identity as one combined PEM. cert-manager can produce that, but only via additionalOutputFormats, which is off by default before 1.15 and needs a cluster-wide feature gate on both the controller and the webhook. Vault, SPIRE and `kubectl create secret tls` can't produce it at all. Add HARNESS_PROXY_CLIENT_CERT_FILE/_PEM + _KEY_FILE/_PEM alongside the existing identity vars, mirroring the split pair the control-api already takes. The two shapes are mutually exclusive and half a pair is an error, so a half-migrated deployment fails loudly and names the offending variable. reqwest's two-buffer constructor is native-tls-only and we're rustls-only, so the pair is joined in memory and handed to the same Identity::from_pem; the combined path is unchanged. Blank env values now read as unset, which is how a template says "not configured". The fingerprint hashes whichever identity files are configured, so rotating only the key is still picked up within 30s. Chart gets blue.internalTransport.clientSecretFormat (combined|split), default combined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
|
This pull request was created from a Blocks session. |
AlejandroEsquivel
marked this pull request as ready for review
September 14, 2026 22:07
CodeQL read `missing_key` as key material being written to a log. It holds an error message; loop over the two half-pair cases instead so the binding is just `error`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…' into feat/proxy-split-client-identity
Adds blue.internalTransport.certManager so the chart declares the Issuer and Certificate resources itself. cert-manager then generates and renews both certificates, and the chart derives the two things a hand-written manifest gets wrong: the internal Service name in the server SAN, and the Secret names the pods mount. Leave issuerRef empty for a self-signed CA scoped to the release, or point it at an existing CA-type Issuer/ClusterIssuer. ACME issuers do not work — both sides read ca.crt out of their own Secret and ACME does not write it. serverSecret/clientSecret installs render byte-identically; their guard still fails when neither name nor certManager is set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #110
Fixes #112
Two halves of the same thing: the proxy learns to read a cert-manager-shaped Secret, and the chart learns to produce one.
1. Split client cert/key in the proxy (#110)
The proxy could only take its Control API client identity as one combined PEM. That file is something you have to build yourself, so it doesn't fit automated issuers: cert-manager only emits it via
additionalOutputFormats(off by default before 1.15, and enabling it earlier needs a cluster-wide feature gate on the controller and the webhook), andkubectl create secret tlscan't emit it at all. Vault and SPIRE write plaintls.crt+tls.key.This adds
HARNESS_PROXY_CLIENT_CERT_FILE/_PEM+HARNESS_PROXY_CLIENT_KEY_FILE/_PEMnext to the existing identity vars, named after the pair the control-api already takes. Nothing changes on the server side and existingclient.pemdeployments render and behave identically.To be clear, this isn't a bug fix —
Identity::from_pemsorts PEM sections by kind, so the combined path already works with cert-manager's key-then-cert output. It's about dropping the 1.15 floor and working with any issuer.Notes for review
Identity::from_pkcs8_pem(the two-buffer constructor) isnative-tls-gated and we're rustls-only, so the pair is joined in memory and handed to the samefrom_pem. No new deps, no crypto-provider ordering hazard...datadirectory atomically — and the existing last-known-good behaviour covers bind mounts.2. Chart-issued certificates (#112)
Reading a cert-manager Secret still left the operator hand-writing the ~60 lines that produce one, including four values the chart already derives: the internal Service DNS name in the server SAN, the namespace, the two Secret names, and the fact that the client Secret is then
split. Each of those renders fine when wrong and fails at handshake time.So the chart now declares the
IssuerandCertificateresources itself:A whole gateway install is then two lines, with no pre-made Secrets:
which renders the self-signed → CA chain plus both leaves. The server SAN is filled in from the Service name:
Point it at an existing PKI and the bootstrap chain disappears entirely, both leaves carrying your
issuerRef:Notes for review
genCA/genSignedCertwas the road not taken. It re-rolls the CA on everyhelm upgradeunless you bolt on alookuphack, and it puts key material in the release. Declaring intent and letting cert-manager own the crypto is the conventional path, and it's the same one Lago and LiteLLM's charts take.ca.crtout of their own Secret, and cert-manager writes that key only when the CA is known — true forSelfSignedandCA, false for ACME. Documented, not enforced; the chart can't tell what anissuerRefpoints at..Capabilities.APIVersions-gated. That helper is false underhelm templatewith no cluster, which would silently drop every Certificate from rendered output and fromverify-deployment.sh. The CRD prerequisite is documented instead. No cert-manager version floor either — this path uses noadditionalOutputFormats, so it needs onlycert-manager.io/v1, GA since 1.0.clientSecretFormatis derived, not validated. Helm can't distinguish "user setcombined" from "user left the default", so failing oncertManager+combinedwould reject every ordinary cert-manager install. It's forced tosplitand documented as ignored.serverSecretName,clientSecretName,clientSecretFormat) mean no consumer template branches on the flag.certManagerunderinsecure-httpfails (nothing consumes the certificates), andissuerRef.namewithoutkindfails — cert-manager defaults it toIssuer, which looks only in the release namespace and leaves the Certificate stuckPendingif you meantClusterIssuer.Tests
There were no Rust tests for any of the identity code. Added 8 covering the precedence table, each error case, split/combined equivalence, and the missing-trailing-newline join. One asserts the insecure-http guard lists every var
client_identity_pemreads, so a future var can't escape the guard.The tests use placeholder PEM stand-ins rather than real key material — gitleaks runs over the repo and the functions under test only select and join strings. I did verify against a real cert/key out-of-tree that
Identity::from_pemaccepts the concatenated bundle, including when the cert file has no trailing newline.tests/e2e/docker-compose.ymlcan now point the proxy at the split pair (BLUE_E2E_PROXY_CERT_FILE/_KEY_FILE, documented in the e2e README) against files the fixture already generates. Default stays combined.verify-deployment.shgets six new renders: cert-manager on with no Secret names at all (3 Certificates, 2 Issuers, both derivedsecretNames, both SANs, split env vars and none of the combined one), an explicitclientSecretFormat=combinedthat must still render split, an externalClusterIssuerthat must drop the bootstrap chain, and three negatives —issuerRefwithoutkind,certManager+insecure-http, and the pre-existing "mtls with no Secret names" guard, which is the regression proving the relaxation didn't disable it.I also diffed old-chart against new-chart renders for
combined,split, and governance-only value sets: byte-identical, so existing installs are untouched.cargo fmt --all --check,cargo clippy --all-targets,cargo test --workspace(38 binaries),helm lint, andscripts/verify-deployment.shall pass.🤖 Generated with Claude Code