Skip to content

Operator-provided / shared wildcard certificate for platform and tenant ingress #2812

Closed

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

  1. Global values channel. Distribute the wildcard via the cozystack-values _cluster.* Secret, which the operator already injects into every HelmRelease through valuesFrom (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.
  2. 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) and gateway-enabled=true (Gateway API); the latter defaults off (packages/core/platform/templates/apps.yaml:80).

Part of #2811.

Activity

  1. added
    kind/featureCategorizes issue or PR as related to a new feature
    area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)
    area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)
    triage/acceptedIndicates an issue is ready to be actively worked on
    priority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to complete
    on Jun 3, 2026
  2. lexfrei commented on Jun 3, 2026

    @lexfrei
    ContributorAuthor

    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-ca rides the _cluster.* channel because it is a public certificate — no private key. A wildcard is a cert plus a private key. The cozystack-values Secret is injected via valuesFrom into 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.

    • One value 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 in tenant-root.
    • ingress-nginx path (gateway-enabled=false): set the root ingress controller's controller.extraArgs.default-ssl-certificate: tenant-root/<name>. System service Ingresses, in wildcard mode, drop their cert-manager.io/cluster-issuer annotation and tls: block; nginx serves the default certificate. No per-host ACME.
    • Gateway path (gateway-enabled=true): a new TenantGateway certMode: existingSecret with a wildcardSecretRef. The controller references that Secret in the wildcard/apex listener certificateRefs instead 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-certificate is 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) and gateway-enabled=true (existingSecret cert mode).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/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 completetriage/acceptedIndicates an issue is ready to be actively worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions