publishing.host | "example.org" | The main domain for all services created under Cozystack, such as the dashboard, Grafana, Keycloak, etc. |
publishing.apiServerEndpoint | "" | Used for generating kubeconfig files for your users. It is recommended to use a routable FQDN or IP address instead of local-only addresses. Example: "https://api.example.org". |
publishing.exposedServices | [api, dashboard, vm-exportproxy, cdi-uploadproxy] | List of services to expose. Possible values: api, dashboard, cdi-uploadproxy, vm-exportproxy. |
publishing.ingressName | "tenant-root" | Ingress controller to use for exposing services. |
publishing.externalIPs | [] | List of external IPs used for the specified ingress controller. If not specified, a LoadBalancer service is used by default. |
publishing.ingressNameAdmin | "" | Name of a separate admin ingress that system components attach their admin routes to (currently only Keycloak) — for example one bound to a private IP and unreachable from the internet. On the Gateway path the value is a namespace (of a Gateway named cozystack); on the ingress-nginx path it is an ingressClassName. The two coincide because Cozystack names each ingressClass after its tenant namespace. Empty (the default) keeps admin endpoints on publishing.ingressName. Two caveats: Cozystack does not create the admin Gateway — provision it out of band first, or the admin routes attach to nothing; and the admin routes render only when Keycloak’s ingress.adminHost is set, so setting this key alone is a no-op. |
publishing.exposureClass.name | "" | Logical name of the cluster-scoped ExposureClass to render. Empty (the default) leaves the publishing.externalIPs path untouched. When set, the host ingress — with publishing.externalIPs empty — publishes its Service as type: LoadBalancer, and cozystack-controller translates the class into the chosen backend’s VIP pool and announcer. This is the migration path off Service.spec.externalIPs, deprecated in Kubernetes v1.36 (KEP-5707). The removal is phased: the AllowServiceExternalIPs feature gate ships enabled in v1.36, then defaults to false (KEP target: v1.40), at which point kube-proxy stops programming rules for the field — the API still accepts it, so the Service looks healthy while no traffic arrives. Later phases lock the gate off and strip kube-proxy’s support outright, and finally drop the gate and the DenyServiceExternalIPs admission controller. The KEP gives those later releases as approximate and the v1.36 announcement commits only to “a future minor release”, so treat the default flipping to false as the deadline and migrate onto a class before that upgrade rather than planning against a specific version. |
publishing.exposureClass.backend | "externalIPs" | LoadBalancer mechanism behind the class. externalIPs pins node IPs with no pool or announcer (the historical behaviour); metallb renders an IPAddressPool + L2Advertisement; cilium renders a CiliumLoadBalancerIPPool + CiliumL2AnnouncementPolicy; robotlb uses a cloud (Hetzner) load balancer, where allocation and announcement happen off-cluster. Once publishing.exposureClass.name is set, a pool or cloud backend (metallb, cilium, robotlb) is mutually exclusive with a non-empty publishing.externalIPs — the platform render fails with an explicit error rather than shipping the dead end, because the host ingress Service would stay ClusterIP + externalIPs, never become type: LoadBalancer, and the allocated pool would go unused. |
publishing.exposureClass.addresses | [] | VIP CIDRs or start-end ranges for the pool-backed backends (metallb, cilium). |
publishing.exposureClass.l2 | true | L2 (ARP / NDP) announcement for the bare-metal backends. |
publishing.exposureClass.isDefault | true | Make this the class used by any ServiceExposure that names none. A ServiceExposure is the cluster-internal resource cozystack-controller reconciles to publish a Service through the class’s backend; workloads do not create one directly. Because this defaults to true, hand-creating a second ExposureClass without clearing its default flag leaves two default classes, and the controller then fails every unnamed ServiceExposure with AmbiguousDefaultClass — keep exactly one default. |
publishing.certificates.solver | "http01" | ACME challenge solver type for default letsencrypt issuer. Possible values: http01, dns01. |
publishing.certificates.issuerName | "letsencrypt-prod" | ClusterIssuer name for TLS certificates used in system Helm releases. |
publishing.certificates.wildcard | false | Opt-in shared wildcard certificate on the default ingress-nginx path (gateway.enabled=false). When true with solver=dns01 and no publishing.certificates.wildcardSecretName, the platform issues one *.<root-host> + <root-host> Certificate via the DNS-01 ClusterIssuer and serves it as the ingress controller’s default SSL certificate, so system services stop minting a per-host ACME certificate each — avoiding Let’s Encrypt rate limits at scale, at parity with the Gateway API path. Ignored on http01 (cannot issue wildcards) and when gateway.enabled=true (the TenantGateway controller issues the wildcard there). Enabling it feeds the issued Secret name into the same cluster values channel as publishing.certificates.wildcardSecretName, so it carries the same child-tenant hazard and the same open bug (
cozystack/cozystack#3296): every tenant’s system ingresses drop their per-host ACME certificate cluster-wide, but the wildcard is served only by the publishing controller. A child tenant running its own ingress controller (ingress: true) is then left with no certificate and serves ingress-nginx’s built-in self-signed one; an inheriting child on the shared controller instead gets a hostname mismatch, because a single-label *.<root-host> wildcard does not cover a nested <service>.<tenant>.<root-host> host or a custom ingress.host on another domain. Only enable it when every exposed host is covered and no child tenant runs its own ingress controller. Off by default so a dns01 cluster is never switched silently on upgrade. |
publishing.certificates.wildcardSecretName | "" | Operator-provided wildcard TLS Secret. When set, platform services and the root tenant’s ingress/Gateway serve this pre-existing Secret instead of minting per-host ACME certificates (only the NAME travels the values channel — never the key material), and takes precedence over publishing.certificates.wildcard. The Secret must exist in the publishing namespace (tenant-root by default), hold valid PEM under tls.crt / tls.key (created as kubernetes.io/tls, though only the material is validated), and cover the served hosts. Scoped to the root tenant but not enforced — the name reaches every tenant, so a child running its own ingress controller can be left serving a self-signed certificate (
cozystack/cozystack#3296). See
Gateway API → Certificates for the full behaviour and the three child-tenant cases. Leave empty to keep ACME issuance. |
publishing.certificates.dns01.provider | "cloudflare" | DNS-01 provider when solver=dns01. Possible values: cloudflare, route53, digitalocean, rfc2136. Both the per-tenant Issuer (rendered by cozystack-controller from the TenantGateway CR) and the cluster-wide letsencrypt-prod / letsencrypt-stage ClusterIssuers used by the legacy ingress flow read this. |
publishing.certificates.dns01.cloudflare.secretName | "cloudflare-api-token-secret" | Secret name holding a Cloudflare API token with Zone:Read + Zone:DNS:Edit on the apex zone. |
publishing.certificates.dns01.cloudflare.secretKey | "api-token" | Key inside the Secret holding the API token. |
publishing.certificates.dns01.route53.region | "" | AWS region of the Route53 hosted zone. Required when provider=route53. |
publishing.certificates.dns01.route53.accessKeyID | "" | IAM access key ID. Optional when running with IRSA / instance profile. |
publishing.certificates.dns01.route53.secretName | "" | Secret name holding the IAM secret access key. Optional when running with IRSA / instance profile. |
publishing.certificates.dns01.route53.secretKey | "secret-access-key" | Key inside the Route53 Secret holding the secret access key. |
publishing.certificates.dns01.digitalocean.secretName | "digitalocean-api-token-secret" | Secret name holding a DigitalOcean API token with write access to the apex domain. |
publishing.certificates.dns01.digitalocean.secretKey | "access-token" | Key inside the Secret holding the DigitalOcean token. |
publishing.certificates.dns01.rfc2136.nameserver | "" | host:port of the authoritative nameserver accepting RFC 2136 dynamic updates. Required when provider=rfc2136. |
publishing.certificates.dns01.rfc2136.tsigKeyName | "" | TSIG key name authorising the dynamic updates. Required when provider=rfc2136. |
publishing.certificates.dns01.rfc2136.tsigAlgorithm | "HMACSHA256" | TSIG HMAC algorithm. |
publishing.certificates.dns01.rfc2136.secretName | "" | Secret name holding the TSIG key material. Required when provider=rfc2136. |
publishing.certificates.dns01.rfc2136.secretKey | "tsig-secret-key" | Key inside the Secret holding the TSIG key. |
publishing.proxyProtocol | false | Enables PROXY-protocol on the host ingress-nginx and auto-deploys
ouroboros to fix the resulting hairpin-NAT problem. The upstream L4 LB in front of ingress-nginx must already be injecting PROXY-v1 headers before this flag flips on; see the linked page for verification recipes and the disable path. |
publishing.proxyProtocolAcknowledgeUnclean | false | Acknowledgement gate for the helm.sh/resource-policy: keep asymmetry on the host disable path. Flipping publishing.proxyProtocol from true back to false stops emitting the cozystack.ouroboros Package CR but does not uninstall the existing one — the platform render fails until either the Package CR is deleted (which triggers the chart’s pre-delete cleanup hook) or this flag is set to true to confirm the operator has handled the asymmetry. See
hairpin-proxy-protocol → Disable path for the full sequence. |