Skip to content

feat(helm): cert-manager external issuer + OpenShift passthrough Route for gateway TLS #2466

Description

@jhjaggars

OpenShell's Helm chart today only supports two TLS provisioning modes: the built-in pkiInitJob (self-signed, bootstrapped via a pre-install Job) and certManager.enabled=true (cert-manager owns rotation, but is hardcoded to a self-signed CA the chart itself creates). Neither lets the gateway's public-facing server cert come from a real issuer (e.g. an ACME ClusterIssuer), so external clients (browsers, grpcurl, unmanaged tooling) always hit a self-signed cert trust error unless they're manually configured to trust the chart's internal CA.
There's also no OpenShift Route template at all. The documented OpenShift install path (docs/kubernetes/openshift.mdx) is server.disableTls=true + oc port-forward, explicitly flagged experimental/eval-only — there's no supported way to expose the gateway externally on OpenShift with real TLS.
This has been implemented and validated end-to-end against a live OpenShift (ROSA) cluster, on branch experiment/cert-manager-openshift-tls in a fork (https://github.com/jhjaggars/OpenShell/tree/experiment/cert-manager-openshift-tls).

  • certManager.serverIssuerRef / certManager.clientIssuerRef (new values): override the issuerRef on the server/client Certificate resources in templates/cert-manager-pki.yaml. Falls back to the chart's own self-signed CA issuer when unset — fully backward compatible, verified byte-identical render output for the default and existing ci/values-cert-manager.yaml scenarios.
  • When an external server issuer is configured, the server Certificate's dnsNames/ipAddresses/commonName are scoped to only the operator-configured external SANs (no internal *.svc.cluster.local/localhost/loopback names). ACME issuers reject internal-only names per CA/Browser Forum baseline requirements, and require commonName to also be one of the SANs — the chart previously hardcoded a commonName that didn't satisfy this and always included internal-only SANs unconditionally.
  • Related bug fix: the gateway's client-CA-for-verification (used to validate sandbox supervisor mTLS certs) previously always extracted ca.crt from the server TLS secret. This breaks once the server cert comes from a different issuer than the client cert. Now sources from certManager.caSecretName when clientCaFromServerTlsSecret=false, so it stays correct regardless of what issues the server cert.
  • New templates/route.yaml: an OpenShift Route with tls.termination: passthrough, gated by a new openshiftRoute.enabled value. The router forwards the raw TLS session to the gateway pod by SNI without decrypting, so the gateway keeps terminating its own TLS/mTLS.
  • Supervisor-side fix (Rust, crates/openshell-core): the sandbox supervisor's gRPC client only trusted the CA configured via OPENSHELL_TLS_CA (the same CA that signs its own client cert). Once the server cert comes from a different (public) issuer, every supervisor connection failed the TLS handshake with UnknownCA. Fixed by also trusting the public root stores (native + webpki) in addition to the configured CA — tonic's root store is a union of all configured sources, so this doesn't weaken verification for existing self-signed deployments, it only adds coverage for the case where server and client certs now differ.
    Validated end-to-end: passthrough Route serving a real Let's Encrypt certificate issued via a Route53 DNS-01 ClusterIssuer, verified with a standard TLS client (no custom CA trust required), OIDC-authenticated CLI access via Keycloak, and a sandbox pod's supervisor successfully mTLS-connecting back to the gateway using the fixed trust store.
  • Split ingress (edge-terminated Route/Envoy Gateway HTTPS listener for external access, self-signed cert kept purely internal): avoids touching the supervisor's trust store at all, but requires giving up passthrough — worth noting the CLI only authenticates via OIDC bearer tokens for remote gateways today (not mTLS), so passthrough's main remaining benefit is that supervisors and external clients can share one ingress path rather than needing two.
  • Getting the ACME issuer to also sign the supervisor's client certificate (unifying both certs under one public CA): rejected — ACME only validates domain ownership, not arbitrary workload identity, so this would mean minting a fake DNS name per sandbox purely to satisfy the ACME identifier requirement, and would put a public-CA rate limit (e.g. Let's Encrypt's per-account order limits) directly in the sandbox-creation hot path for zero security benefit, since nothing outside the cluster needs to trust the supervisor's identity.
  • Gateway API TLSRoute as a portable, vendor-neutral alternative to the OpenShift-specific Route: viable (graduated to the Gateway API standard channel around v1.5.0, and Envoy Gateway supports it), but there are still some version-pairing rough edges between newer Gateway API CRDs and recent Envoy Gateway releases, and OpenShift's native Route is more mature/stable there today. A TLSRoute template could be added later as a parallel path for non-OpenShift Gateway-API clusters, similar to how the chart already offers both openshiftRoute and grpcRoute side by side for edge termination.
    Full implementation, live-cluster validation, and adversarial review of the chart's existing TLS logic (templates/cert-manager-pki.yaml, templates/_gateway-workload.tpl) were done in an interactive session culminating in this proposal. Key findings backing the design above:
  • Confirmed via helm template that the serverIssuerRef/clientIssuerRef overrides render byte-identical output to current behavior when unset.
  • Confirmed via a live ACME ClusterIssuer (Let's Encrypt via Route53 DNS-01) that the chart's prior unconditional inclusion of internal-only SANs and a fixed commonName both cause hard ACME issuance failures, which the design above resolves.
  • Confirmed via a running sandbox pod that the UnknownCA supervisor connection failure only manifests when server and client certs diverge in issuer, and that the tonic trust-store fix resolves it without regressing existing mTLS rejection behavior (verified via the existing mtls_wrong_ca_client_cert_rejected / mtls_valid_client_cert_accepted integration tests, both still passing).
    A PR implementing this design will follow, including chart documentation updates, a CI render-coverage values file, and Helm unittest coverage (deferred from the initial spike, being added before the PR is opened).

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions