Repository navigation
Operator-provided / shared wildcard certificate for platform and tenant ingress #2812
Copy link
Copy link
Closed
Labels
area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)Issues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/featureCategorizes issue or PR as related to a new featureCategorizes issue or PR as related to a new featurepriority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to completeImportant over the long term, but may not be staffed and/or may need multiple releases to completetriage/acceptedIndicates an issue is ready to be actively worked onIndicates an issue is ready to be actively worked on
Description
Context
On the default platform every system ingress mints its own per-host certificate through the cert-manager cluster-issuer annotation + ingress-shim:
- dashboard —
packages/system/dashboard/templates/ingress.yaml:13,45 - keycloak —
packages/system/keycloak/templates/ingress.yaml:18,26 - grafana —
packages/system/monitoring/templates/grafana/grafana.yaml:81,98 - harbor —
packages/apps/harbor/templates/ingress.yaml:22,28
The issuer is fixed to the ACME ClusterIssuer selected by _cluster.issuer-name (packages/system/cert-manager-issuers/templates/cluster-issuers.yaml:87-134). There is no escape hatch to point an ingress at a pre-existing, operator-supplied TLS secret, so an operator who already holds a wildcard (purchased, or issued by a corporate CA) cannot make platform services serve under it.
Goal
Provide one operator-facing way to supply an existing wildcard TLS secret and have platform services and tenant ingress consume it instead of minting per-host ACME certificates.
Options to decide between
- Global values channel. Distribute the wildcard via the
cozystack-values_cluster.*Secret, which the operator already injects into every HelmRelease throughvaluesFrom(internal/operator/package_reconciler.go:257-263). The Kubernetes root CA already rides this channel (packages/core/platform/templates/apps.yaml:95-96), so the plumbing exists. - Copy-issuer. A cert-manager issuer (webhook) that copies a pre-populated wildcard secret into target namespaces on demand, so ingress-shim resolves it like any other issued certificate.
Acceptance
- An operator sets one value/secret and dashboard + grafana serve under the supplied wildcard.
- No per-host ACME issuance on this path.
- Works for both
gateway-enabled=false(ingress-nginx) andgateway-enabled=true(Gateway API); the latter defaults off (packages/core/platform/templates/apps.yaml:80).
Part of #2811.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)Issues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/featureCategorizes issue or PR as related to a new featureCategorizes issue or PR as related to a new featurepriority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to completeImportant over the long term, but may not be staffed and/or may need multiple releases to completetriage/acceptedIndicates an issue is ready to be actively worked onIndicates an issue is ready to be actively worked on
Activity
lexfrei commentedon Jun 3, 2026
Decision: native references (neither option 1 nor 2 as-is)
Both listed options have a drawback that pushed me to a third approach.
Why not option 1 (global values channel).
kube-root-carides the_cluster.*channel because it is a public certificate — no private key. A wildcard is a cert plus a private key. Thecozystack-valuesSecret is injected viavaluesFrominto every HelmRelease in every namespace, including tenant namespaces (internal/operator/package_reconciler.go). Putting the wildcard private key on that channel broadcasts the cluster's most sensitive key into every tenant namespace — a security regression in the same class as the per-app TLS private-key coupling tracked elsewhere in this epic.Why not option 2 (copy-issuer). It needs a new external cert-manager issuer component to build and maintain (webhook + image + RBAC + deployment). The private key is still copied into every consuming namespace, so there is no security gain over the status quo — only added surface to own.
Chosen: native references, one operator input, no new component. Private key lives only where TLS terminates.
publishing.certificates.wildcardSecretName(default empty), propagated through the existing channel as_cluster.wildcard-secret-name. Only the name travels, never the cert/key data. The operator places the Secret intenant-root.gateway-enabled=false): set the root ingress controller'scontroller.extraArgs.default-ssl-certificate: tenant-root/<name>. System service Ingresses, in wildcard mode, drop theircert-manager.io/cluster-issuerannotation andtls:block; nginx serves the default certificate. No per-host ACME.gateway-enabled=true): a newTenantGatewaycertMode: existingSecretwith awildcardSecretRef. The controller references that Secret in the wildcard/apex listenercertificateRefsinstead of minting a cert-manager Certificate/Issuer.Scope. MVP covers platform services + the root tenant (
tenant-root) — everything is same-namespace, so no RBAC/NetworkPolicy changes.--default-ssl-certificateis per-controller and each tenant runs its own controller in its own namespace with namespace-scoped RBAC under a default-deny network policy, so extending wildcard mode to child tenants needs the one Secret replicated into each tenant namespace. The cluster already ships a ClusterSecret operator that fits that job; I will track per-tenant propagation as a separate follow-up rather than block this on it.Acceptance mapping. Operator sets one value + places one Secret → dashboard and grafana serve under the supplied wildcard; no per-host ACME on this path; works for both
gateway-enabled=false(default-ssl-certificate) andgateway-enabled=true(existingSecret cert mode).feat(platform): operator-provided wildcard certificate for platform a…