Repository navigation
feat(operator): trust a CA ConfigMap for passthrough providers - #208
Conversation
PassthroughModel always rendered its BackendTLSPolicy with wellKnownCACertificates: System, so the gateway only accepted provider certificates that chain to the public roots in the Envoy proxy image. A self-hosted OpenAI-compatible server (llama.cpp on a workstation, vLLM on another host) with a certificate from a private or corporate CA could not be reached at all, and no field allowed otherwise. Add spec.provider.tls.caConfigMapName. When set, the BackendTLSPolicy references that ConfigMap through caCertificateRefs instead of the system roots; Gateway API allows exactly one trust source, so the bundle replaces the public roots for that provider only. Envoy Gateway reads the "ca.crt" key or a ConfigMap's only key, so a trust-manager Bundle target can be referenced directly, and it watches the ConfigMap, so CA renewals apply without restarts. Omitting tls keeps the existing behavior unchanged. Document the field, add a Self-hosted servers guide with a llama.cpp example, and add a troubleshooting entry for upstream TLS failures.
|
📄 Docs preview for |
Put the requirements and generic server setup first: install a certificate for the server's name, serve HTTPS with it from whatever framework runs the model (or a reverse proxy), require an API key, and keep it running. Move everything specific to llama.cpp on a Mac Studio (Homebrew, llama-server flags, launchd, sleep, firewall) into an example section at the end, and drop the PKCS#12 splitting, which belongs to the operator's CA process rather than this guide.
Switch one PassthroughModel between the system roots and a CA ConfigMap in both directions through BuildPassthroughResources and the shared apply helper, and check the stored BackendTLSPolicy holds exactly one trust source after each step. Also check that a Bedrock provider with a private endpoint and a CA ConfigMap renders caCertificateRefs while keeping its Bedrock schema and AWS credentials.
tylerpotts
left a comment
There was a problem hiding this comment.
Thanks @Adam-D-Lewis, this is a clean, well-scoped change. The new field takes the same route as hostname and port (ProviderSpec → provider.Resolved → builder), the webhook enforces it through the same Resolve call, and building exactly one trust source plus the full-Update apply means switching modes can't leave both fields behind. The PR description is a model of how to write one.
What I verified at 04749ec:
- Operator build/vet are clean, and the provider, reconcilers, controller and webhook suites pass with envtest (Kubernetes 1.34.1). Docs
npm testpasses (31 tests). - Mutation checks: making the builder emit both trust sources fails
TestBackendTLSPolicyTrustSourceSwitchesBothWaysat step 1 and bothTrustsConfiguredCAtests. Removing the resolver's name check fails theCorp_CAwebhook spec. The new tests catch what they're meant to. - The chart CRD is byte-identical to
config/crd/bases. - Upstream claims in the guide hold up against Envoy Gateway v1.8.4 and llama.cpp v0.7.0:
ca.crt-or-only-key, re-translation on ConfigMap edits, theNoValidCACertificate/InvalidCACertificateRefreasons, IP/single-label rejection, the llama-server flags, and Homebrew building with OpenSSL. - Not run: a live handshake through Envoy Gateway, same as you noted.
Before merge: two doc lines describe Envoy behaviour that doesn't happen (inline below). Also, scripts/check-links.sh currently fails on self-hosted/index.html -> /upgrading/. That's the expected dependency on #210, whose ## Routine upgrade anchor does exist. The "LINKS_OK" line in the PR description predates the commit that added the link, so CI will be red until #210 merges and this branch is rebased.
Follow-up, not blocking: the credential Secret has an informational UpstreamCredentialResolved condition, backed by an index and a watch. The CA ConfigMap is the second user-supplied reference and has no equivalent, so a missing ConfigMap only shows up on <name>-backend-tls. The operator ClusterRole already allows get/list/watch on configmaps, so an UpstreamCAResolved condition would need no new RBAC. Your status note explains the current choice, so a follow-up issue is fine.
Approving. Please make the two doc fixes before merging.
|
|
||
| The `<name>-api-keys` Secret is the key-manager's user API-key store, not an upstream credential; a 401 issued by the gateway itself points at the user's minted key, not the AWS identity. | ||
|
|
||
| **Upstream TLS verification fails (HTTP 503, `CERTIFICATE_VERIFY_FAILED`).** The gateway reached the provider but did not trust its certificate. By default it accepts only the public CA roots in the Envoy proxy image, so a certificate from a private or corporate CA needs `spec.provider.tls.caConfigMapName` (see [Self-hosted servers](/self-hosted/)). When that field is set, check the policy status first: |
There was a problem hiding this comment.
The 503 is right, but CERTIFICATE_VERIFY_FAILED never reaches the client. Envoy v1.38.4 (the default proxy image in Envoy Gateway v1.8.4) hides the transport failure reason from the response body by default: envoy.reloadable_features.hide_transport_failure_reason_in_response_body is a RUNTIME_GUARD (default true, source/common/runtime/runtime_features.cc:69), checked in source/common/router/router.cc:1612-1621. Envoy Gateway doesn't override it. The client gets:
upstream connect error or disconnect/reset before headers. reset reason: connection failure
The TLS_error:...CERTIFICATE_VERIFY_FAILED detail only shows up in the proxy access log (%RESPONSE_CODE_DETAILS%). Someone grepping their curl output for the string in this heading won't find it and may rule out TLS. Suggest quoting the real body in the heading and adding a step to check the Envoy proxy logs for CERTIFICATE_VERIFY_FAILED.
| --ssl-cert-file macstudio.crt \ | ||
| --ssl-key-file macstudio.key \ | ||
| --api-key-file api-key \ | ||
| --no-webui |
There was a problem hiding this comment.
Nit: in llama.cpp v0.7.0, --no-webui is a deprecated alias of --no-ui (common/arg.cpp:3491), and the server logs a deprecation warning. It still works. Keeping it is reasonable if you'd rather not break older Homebrew builds, so take it or leave it.
|
@tylerpotts Review follow-up: the requested doc corrections and the optional flag nit are addressed in 81571fb and 0300853:
The previously missing live data-plane test is now done. Operator commit 81571fb ran on Kind/Kubernetes 1.36.1 with Envoy Gateway 1.8.1, Envoy 1.38.1, and AI Gateway 1.1.0. A real Prism llama.cpp router running Ternary-Bonsai-2-27B sat behind a temporary authenticated HTTPS proxy. Both TLS hops were verified; the client did not use Normal and streaming requests through the operator-generated route returned One observed caveat is now documented: malformed PEM can leave the policy accepted while Envoy rejects the TLS secret; an invalid update to a working bundle can retain the previous valid trust. Rotation verification therefore needs proxy logs and a request against the new server certificate, not policy status alone. These were local integration tests, not committed CI coverage. TLS terminated at the proxy in front of llama.cpp; native llama.cpp TLS and the full Keycloak/key-manager flow were not tested. The PR description now records these results and limits. Follow-ups: #212 tracks the informational CA-reference status/watch you suggested. #213 tracks a separate dev-setup Gateway API CRD channel conflict found during testing. All 31 local docs tests pass. The latest CI has 12 passing checks; docs still fails on the |
tylerpotts
left a comment
There was a problem hiding this comment.
Thanks @Adam-D-Lewis. I re-checked at 0300853. The two doc fixes I asked for before merge are done, so my approval stands. The new rotation and malformed-PEM guidance is a good addition from the live test.
Previous review, checked at 0300853:
| Item | Status | Evidence |
|---|---|---|
Troubleshooting heading quoted CERTIFICATE_VERIFY_FAILED, which never reaches the client |
Fixed | troubleshooting.md:301 now quotes the client body and sends readers to the proxy logs |
| "SHA-256 or stronger" listed as a requirement in troubleshooting | Fixed | Removed from the checklist at troubleshooting.md:323 |
| Same SHA-256 wording in the self-hosted guide | Fixed | self-hosted.mdx:52 now says "(SHA-256 or stronger recommended)" |
Nit: --no-webui is a deprecated alias |
Fixed | --no-ui at self-hosted.mdx:148 and :153 |
| Follow-up: CA ConfigMap resolution condition | Tracked | #212 |
Merge order: #210 is approved and should merge first. I trial-merged it into this branch:
- The only conflict is the
docs/astro.config.mjssidebar. Resolving it as the description says givesSelf-hosted Servers,Installation,Upgrading. - On the merged tree, docs
npm testpasses (31),scripts/check-links.shreportsLINKS_OK, andself-hostedlinks to/llm-serving-pack/upgrading/#routine-upgradeunder the subpath build. - The only failure in this PR's current docs CI is the expected
self-hosted/index.html -> /upgrading/broken link. It should go green once this branch is updated after #210 merges.
New troubleshooting text, checked against upstream source (Envoy Gateway v1.8.4, which defaults to Envoy v1.38.4):
- The response body hides the transport failure reason by default (
router.cc:1612-1621and the runtime guard atruntime_features.cc:69). - EG's default JSON access log is enabled and writes
%RESPONSE_CODE_DETAILS%and%UPSTREAM_TRANSPORT_FAILURE_REASON%(internal/xds/translator/accesslog.go:46,48). That means thegrep CERTIFICATE_VERIFY_FAILEDmatches. This holds as long as no EnvoyProxy overrides the access-log format, and this repo has none. Failed to load trusted CA certificates from <inline>is the real Envoy message for a bad inline CA (default_validator.cc:86-89). EG delivers the ConfigMap as inline bytes over SDS, so the path really is<inline>.Secret is not supplied by SDSis real too (ssl_socket.cc:26). It is a transport failure reason, not a standalone log line. Envoy replaces spaces with underscores inresponse_code_details, so this string only matches through theupstream_transport_failure_reasonfield. Yourgrep -Estill catches it.- The proxy container is named
envoy(internal/infrastructure/kubernetes/proxy/resource.go:30).
Nit, non-blocking: troubleshooting.md:304 has readers pick <envoy-proxy-pod> out of a bare get pods, which also lists the controller. The same file already uses -l gateway.envoyproxy.io/owning-gateway-name=<gateway> (lines 191-192), so kubectl logs -l ... -c envoy would skip the copy-paste step. Passthrough routes go on the internal or external gateway, not necessarily nebari-gateway, so the placeholder should stay generic.
Not run this round: the operator suites and mutation checks. 81571fb and 0300853 change only docs, so last round's results at 04749ec still apply. I didn't run a live data-plane test; your Kind run covers that.
…ider-ca-trust Resolve the docs sidebar conflict with #210: keep Self-hosted Servers after AWS Bedrock and take main's Upgrading entry in place of Gateway Upgrades.
Both proxy-log commands had readers pick <envoy-proxy-pod> out of a bare get pods, which also lists the Envoy Gateway controller. Select the proxy pods by their owning-gateway-name label instead, as the rest of the page does, which also reads every replica. Passthrough routes attach to both the external and internal gateways, so the placeholder stays generic, with the chart defaults named. Pass --tail=-1: with a label selector, kubectl logs otherwise returns only the last 10 lines of each pod, even with --since.
|
@tylerpotts Took the nit in de163f6: both proxy-log commands in the upstream TLS entries now select pods with |
Backport of 00ee3cf (#208) from main. Add spec.provider.tls.caConfigMapName to PassthroughModel. When set, the provider's BackendTLSPolicy references that ConfigMap through caCertificateRefs instead of wellKnownCACertificates: System, so the gateway can reach a self-hosted server whose certificate comes from a private or corporate CA. Gateway API allows one trust source, so the bundle replaces the public roots for that provider only. Omitting tls keeps the existing behavior. This branch predates main's provider resolver, so the ConfigMap name check lives in the PassthroughModel webhook instead.
Summary
A
PassthroughModelcan now front an OpenAI-compatible server whose certificate comes from a private or corporate CA. Examples are llama.cpp'sllama-serveron a Mac Studio, or vLLM on another host on the LAN. A new optional field,spec.provider.tls.caConfigMapName, points the gateway at a ConfigMap of trusted CAs for that provider. Omitting it keeps today's behavior.The PR also adds a Self-hosted servers guide with a llama.cpp walkthrough, a matching example manifest, and troubleshooting entries for upstream TLS failures.
Problem
The operator always renders the provider's
BackendTLSPolicylike this:Envoy Gateway implements
Systemas the CA file inside the Envoy proxy image (/etc/ssl/certs/ca-certificates.crt). That file holds only the public roots. The connection to the provider is always TLS, and no field selects a different trust source. So a self-hosted server on a private network can't be used:Change
API.
ProviderSpecgains an optionaltlsblock:caConfigMapNameis required insidetlsand has a minimum length of 1. The provider resolver checks that it's a valid object name, so the webhook rejects values such asCorp_CA.Rendering. When the field is set, the
BackendTLSPolicyusescaCertificateRefs(group: "",kind: ConfigMap) in place ofwellKnownCACertificates. Gateway API's CEL rules allow exactly one of the two, so the bundle replaces the public roots for that provider. Other PassthroughModels are unaffected. The operator already applies generated resources with a fullUpdate, so switching an existing model between the two modes doesn't leave both fields behind.Behavior that comes from Envoy Gateway, with nothing added in the operator. Each was checked in the v1.8.4 source:
ca.crtkey, or from the ConfigMap's only key. So a trust-managerBundletarget (one key, usuallyca-certificates.crt) can be referenced directly.BackendTLSPolicyis indexed by its referenced ConfigMaps, so editing the ConfigMap re-translates the policy. Valid CA updates need no restart.Accepted=False / NoValidCACertificateandResolvedRefs=False / InvalidCACertificateRef. Malformed PEM is different: in live testing with Envoy Gateway 1.8.1, the policy could report accepted while Envoy rejected the TLS secret. An invalid replacement of a working bundle could retain the previous valid trust. The troubleshooting guide explains how to check proxy logs and verify rotation with a real request.Docs.
New
self-hosted.mdxpage, added to the sidebar after AWS Bedrock. It leads with steps that don't depend on the framework:PassthroughModelA caution at the top lists the single-server trade-offs: no failover or load balancing, overload left to the serving framework (the pack adds no queueing or rate limiting), the fixed 120s timeout, and no health checks. An Example: llama.cpp on a Mac Studio section at the end holds the specifics: Homebrew, the
llama-serverflags, launchd, sleep and firewall.examples/passthrough-llamacpp.yaml, embedded throughExampleCode, like the other examples.configuration.mdx: new row forspec.provider.tls.caConfigMapName.troubleshooting.md: two new entries, "Upstream connection fails (HTTP 503)" and "A CA bundle update does not take effect".One-line cross-links in
quickstart.mdx,providers.mdxandarchitecture.mdx. The architecture resource table no longer says "System CAs" only.Upgrade impact
v1alpha1, with no version bump, no conversion, and no change to defaults. An existingPassthroughModelwithouttlsrenders the identicalBackendTLSPolicy, so upgrading changes nothing on the cluster for it.crds/directory on sync unlessskipCrdsis set, so Argo users get the new schema with the upgrade. Plainhelm upgradedoes not; that path is covered by Upgrading (added by docs: one Upgrading page for routine and release-specific steps #210), and the new guide links there.tlsis simply absent. The CRD update is a prerequisite only for using the field, and the new guide says so.type: enhancement 💅🏼so the auto-generated notes list it under Features. The notes carry only the PR title, so if the release body is edited by hand, a one-liner would help: "PassthroughModel can trust a private CA (spec.provider.tls.caConfigMapName); Helm users must update the CRD to use it, see Upgrading."Files changed
operator/api/v1alpha1/passthroughmodel_types.go:ProviderTLStype andProviderSpec.TLSoperator/api/v1alpha1/zz_generated.deepcopy.go: regeneratedoperator/config/crd/bases/llm.nebari.dev_passthroughmodels.yaml: regeneratedcharts/nebari-llm-serving/crds/passthroughmodel-crd.yaml: byte-identical copy of the base CRDoperator/internal/provider/provider.go:Resolved.CAConfigMapNameand validationoperator/internal/controller/reconcilers/passthrough.go:BackendTLSPolicytrust sourceoperator/internal/provider/provider_test.go,operator/internal/controller/reconcilers/passthrough_test.go,operator/internal/controller/backendtlspolicy_trust_test.go(new),operator/internal/webhook/v1alpha1/passthroughmodel_webhook_test.go: testsexamples/passthrough-llamacpp.yaml: newdocs/src/content/docs/self-hosted.mdx: newdocs/astro.config.mjs,docs/src/content/docs/{configuration,troubleshooting,quickstart,providers,architecture}.*,docs/test/docs-examples.test.mjsTesting
All runs were local on this branch.
controller-genv0.19.0 (the Makefile pin) forobjectandcrd. The only generated changes are the new type and field.make test: all packages pass. That includes the new webhook specs, which admitexamples/passthrough-llamacpp.yamlfrom disk and rejectCorp_CAthrough a real API server.TestBackendTLSPolicyTrustSourceSwitchesBothWaysdrives one model System → CA → System → CA throughBuildPassthroughResourcesand the shared apply helper (fake client), and checks the stored policy holds exactly one trust source after each step. I confirmed it bites: making the builder emit both sources fails it at the first switch to the CA.TestBuildPassthroughResourcesBedrockTrustsConfiguredCAcovers Bedrock with a private endpoint and a CA ConfigMap.golangci-lintv2.4.0 (the CI pin) onoperator/: 0 issues.key-managergo test ./...: passes. It imports the operator module.npm testpasses (31 tests, including newself-hostedchecks that keep the--aliasand the declared id in step). Rerun after mergingmain(with docs: one Upgrading page for routine and release-specific steps #210) into this branch and after the troubleshooting update inde163f6:npm teststill passes, and a build with the production base (BASE=/llm-serving-pack/) passesscripts/check-links.shwithLINKS_OK. The self-hosted guide's/upgrading/#routine-upgradelink points at an existing anchor.BackendTLSPolicyin both modes. Both were accepted, and the stored object keptcaCertificateRefs: [{group: "", kind: ConfigMap, name: ...}]. A policy with both trust sources was rejected withmust not contain both CACertificateRefs and WellKnownCACertificates, which confirms the CEL rule the builder relies on. CI'sTestGatewayAdmissionloads only the AI Gateway CRDs, so it does not coverBackendTLSPolicy.Live data-plane verification
Tested operator commit
81571fbon a disposable Kind 0.32.0 / Kubernetes 1.36.1 cluster using Helm 4.3.0, Envoy Gateway 1.8.1, Envoy 1.38.1 and Envoy AI Gateway 1.1.0. Later commits change only documentation, plus one code comment that came in with the merge ofmain.Requests traversed the operator-generated route and TLS policy to a temporary, API-key-protected HTTPS nginx proxy in front of an existing Prism llama.cpp router running
Ternary-Bonsai-2-27B. Both client-facing and upstream certificates were verified; the client did not use--insecure.TLS_OK; the stream completed with[DONE].Limits: this used TLS termination at nginx, not llama.cpp's native TLS listener. It did not exercise production DNS/ACME, Keycloak login, or key-manager key issuance; a test client key was inserted into the operator-managed Secret using the documented local-dev workflow. The test scripts/evidence were retained locally and are not committed CI coverage.
The local dev setup encountered a Gateway API standard/experimental CRD channel conflict. The test preserved the standard CRDs, applied Envoy-specific CRDs separately and installed the Envoy chart with
--skip-crds; no admission policy was disabled. The setup defect and tested workaround are tracked in #213.Notes for reviewers
caConfigMapName, to matchcredentialSecretName, rather than mirroring Gateway API'scaCertificateRefslist. One trust-manager bundle can hold any number of CAs. If reviewers prefer the list shape, or wantSecretsupport (which Envoy Gateway also accepts), the change is local toProviderTLSandbuildProviderBackendTLSPolicy.BackendTLSPolicystatus into the PassthroughModel, soReadymeans "resources applied". An informational CA ConfigMap resolution condition/watch is tracked separately in Add informational CA ConfigMap resolution status to PassthroughModel #212. Docs distinguish reference resolution from actual certificate loading and point readers to proxy logs and live requests as well as policy status.