diff --git a/README.md b/README.md index 9ce0aa09..5a79cd59 100644 --- a/README.md +++ b/README.md @@ -910,6 +910,11 @@ These changes don't require changes to your `values.yaml`, but they change what explicit FQDN for each pod instead of the wildcard. `none` adds only the service names, so you can add your own with `tls..dnsNames` and `tls..ipAddresses`. Restart the component pods after changing it so that they pick up the reissued certificates. +- **Externally managed certificates:** every component now supports `tls..createCert: false`, not + only the proxy and the function worker, so you can provide all the certificate Secrets yourself. The proxy + Secret no longer needs the `tls-combined.pem` key, so a publicly trusted certificate can be used for the + proxy while the other components use certificates from a private CA. The `## TLS` comments in `values.yaml` + describe the supported setups and the required Secret keys. ## Upgrading to Helm chart version 4.6.0 diff --git a/charts/pulsar/templates/_certs.tpl b/charts/pulsar/templates/_certs.tpl index 2b8040aa..06725f6a 100644 --- a/charts/pulsar/templates/_certs.tpl +++ b/charts/pulsar/templates/_certs.tpl @@ -54,11 +54,23 @@ Define the pulsar certs ca issuer secret name {{- if .Values.certs.issuers.ca.secretName -}} {{- .Values.certs.issuers.ca.secretName -}} {{- else -}} -{{- fail "certs.issuers.ca.secretName is required when TLS is enabled and certs.internal_issuer.enabled is false" -}} +{{- fail "certs.issuers.ca.secretName is required when TLS is enabled and certs.internal_issuer.enabled is false. Set it to the name of a Secret with a ca.crt key that contains the CA certificates the components use to verify each other's TLS certificates." -}} {{- end -}} {{- end -}} {{- end -}} +{{/* +Return "true" when the chart should create the cert-manager Certificate for a component. +Only an explicit `createCert: false` disables it, so that values without the key +(for example from `helm upgrade --reuse-values`) keep creating the certificate. +Usage: {{ include "pulsar.tls.createCert" .Values.tls.broker }} +*/}} +{{- define "pulsar.tls.createCert" -}} +{{- if not (and (hasKey . "createCert") (eq (toString .createCert) "false")) -}} +true +{{- end -}} +{{- end -}} + {{/* Return the service name for a component. Usage: {{ include "pulsar.certs.component.service" (dict "root" . "componentKey" "broker") }} diff --git a/charts/pulsar/templates/_proxy.tpl b/charts/pulsar/templates/_proxy.tpl index 48905ca8..2dc1f062 100644 --- a/charts/pulsar/templates/_proxy.tpl +++ b/charts/pulsar/templates/_proxy.tpl @@ -69,10 +69,6 @@ Define proxy tls certs volumes path: tls.crt - key: tls.key path: tls.key - {{- if .Values.tls.zookeeper.enabled }} - - key: tls-combined.pem - path: tls-combined.pem - {{- end }} {{- end }} - name: ca secret: diff --git a/charts/pulsar/templates/tls-certs-internal.yaml b/charts/pulsar/templates/tls-certs-internal.yaml index 6d00df22..e7c7b604 100644 --- a/charts/pulsar/templates/tls-certs-internal.yaml +++ b/charts/pulsar/templates/tls-certs-internal.yaml @@ -20,42 +20,48 @@ {{- if .Values.tls.enabled }} {{- if .Values.tls.proxy.enabled }} -{{- if .Values.tls.proxy.createCert }} +{{- if include "pulsar.tls.createCert" .Values.tls.proxy }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "proxy" "componentConfig" .Values.proxy "tlsConfig" .Values.tls.proxy) }} --- {{- end }} {{- end }} {{- if or .Values.tls.broker.enabled (or .Values.tls.bookie.enabled .Values.tls.zookeeper.enabled) }} -{{- if not .Values.standalone.enabled }} +{{- if and (not .Values.standalone.enabled) (include "pulsar.tls.createCert" .Values.tls.broker) }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "broker" "componentConfig" .Values.broker "tlsConfig" .Values.tls.broker) }} --- {{- end }} {{- end }} {{- if .Values.tls.function_worker.enabled }} -{{- if and .Values.components.function_worker .Values.tls.function_worker.createCert }} +{{- if and .Values.components.function_worker (include "pulsar.tls.createCert" .Values.tls.function_worker) }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "function_worker" "componentConfig" .Values.function_worker "tlsConfig" .Values.tls.function_worker) }} --- {{- end }} {{- end }} -{{- if .Values.standalone.enabled }} +{{- if and .Values.standalone.enabled (include "pulsar.tls.createCert" .Values.tls.standalone) }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "standalone" "componentConfig" .Values.standalone "tlsConfig" .Values.tls.standalone) }} --- {{- end }} -{{- if or .Values.tls.bookie.enabled .Values.tls.zookeeper.enabled }} +{{- if and (or .Values.tls.bookie.enabled .Values.tls.zookeeper.enabled) (include "pulsar.tls.createCert" .Values.tls.bookie) }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "bookkeeper" "componentConfig" .Values.bookkeeper "tlsConfig" .Values.tls.bookie) }} --- {{- end }} {{- if .Values.tls.zookeeper.enabled }} +{{- if include "pulsar.tls.createCert" .Values.tls.autorecovery }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "autorecovery" "componentConfig" .Values.autorecovery "tlsConfig" .Values.tls.autorecovery) }} --- +{{- end }} +{{- if include "pulsar.tls.createCert" .Values.tls.toolset }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "toolset" "componentConfig" .Values.toolset "tlsConfig" .Values.tls.toolset) }} --- +{{- end }} +{{- if include "pulsar.tls.createCert" .Values.tls.zookeeper }} {{ include "pulsar.cert.template" (dict "root" . "componentKey" "zookeeper" "componentConfig" .Values.zookeeper "tlsConfig" .Values.tls.zookeeper) }} {{- end }} +{{- end }} {{- end }} diff --git a/charts/pulsar/values.yaml b/charts/pulsar/values.yaml index de1dc1df..0e8f5ba1 100755 --- a/charts/pulsar/values.yaml +++ b/charts/pulsar/values.yaml @@ -315,10 +315,77 @@ images: pullPolicy: ## TLS -## templates/tls-certs.yaml +## templates/tls-certs-internal.yaml ## -## The chart is using cert-manager for provisioning TLS certs for -## brokers and proxies. +## The chart uses cert-manager to create the TLS certificates of the components. Each component reads its +## certificate and private key from the Secret "-.cert_name>" (for example +## "pulsar-tls-broker"), and verifies the certificates of the other components with the CA certificates in the +## "ca.crt" key of the CA Secret (see `certs.internal_issuer` and `certs.issuers.ca` below). +## +## Certificates issued by a public CA (for example with an ACME issuer such as Let's Encrypt): +## Publicly trusted certificates can't be used for the connections between the components, for two reasons: +## - A public CA can't issue certificates for cluster-internal names such as +## "-pulsar-broker..svc.cluster.local". The components connect to each other using these +## Kubernetes service and pod names and verify that the server certificate matches the name, so the +## certificates used between the components must include them. The chart therefore always adds the +## Kubernetes service names to the certificates it creates (see `tls.common.sanMode`). +## - The components also present their certificate as a TLS client certificate: the brokers to the bookies, +## and all ZooKeeper clients to ZooKeeper when `tls.zookeeper.enabled` is true. Public CAs are removing the +## clientAuth extended key usage from the TLS certificates they issue (Let's Encrypt removed it from its +## default profile in February 2026), so a renewed public certificate can only be used as a server +## certificate. See https://news.apache.org/foundation/entry/the-public-ca-clientauth-eku-sunset-what-apache-software-deployers-need-to-know +## Use a private CA for the certificates of the components: `certs.internal_issuer`, or for production, a +## cert-manager issuer backed by a private CA such as OpenBao, HashiCorp Vault or step-ca, configured with +## `certs.internal_issuer.enabled: false` and `certs.issuers.ca` (name, kind, group and secretName). +## The recommended setup is to use a publicly trusted certificate only where clients connect from outside the +## cluster, which is the proxy. It only needs the server auth usage: +## tls: +## enabled: true +## proxy: +## enabled: true +## # the chart doesn't create the proxy certificate; create the Secret "-tls-proxy" yourself, +## # for example with a cert-manager Certificate that uses your public ClusterIssuer and has +## # `secretName: -tls-proxy` and the public DNS names of the proxy in `dnsNames` +## createCert: false +## broker: +## enabled: true +## certs: +## internal_issuer: +## enabled: true +## The proxy uses only the "tls.crt" and "tls.key" keys of its Secret, and it verifies the broker certificates +## with the internal CA. Clients can verify the publicly trusted proxy certificate with their default trust +## store. Clients that authenticate with TLS client certificates (mTLS) need certificates from a private CA +## for the same reason, since a public certificate without the clientAuth usage can't be used as a client +## certificate. +## When clients connect to the brokers directly instead of through the proxy, a topic lookup returns the +## address advertised by the broker that owns the topic, and the client verifies the broker certificate +## against that address. By default, the brokers advertise their pod FQDN in the broker headless service, +## which external clients can't reach and a public CA can't issue certificates for. Direct access from +## outside the cluster requires an external listener in `advertisedListeners` with a reachable DNS name for +## each broker, and a broker certificate that matches each of these names, either with a wildcard or with an +## individual name for each broker. The chart doesn't support this yet. See +## https://pulsar.apache.org/docs/next/concepts-multiple-advertised-listeners/#kubernetes-deployment-suggestions +## +## Certificates that you manage yourself (`createCert: false`): +## Set `tls..createCert: false` for each component whose certificate you provide, and create the +## Secret "-.cert_name>" before installing the chart. The Secret must contain: +## - "tls.crt": the certificate chain, starting with the component's certificate. +## - "tls.key": the private key in PKCS#8 PEM format ("-----BEGIN PRIVATE KEY-----"). +## - "tls-combined.pem" (only when `tls.zookeeper.enabled` is true, for the broker, bookie, autorecovery, +## toolset and zookeeper certificates): the private key followed by the certificate chain in a single PEM +## file. ZooKeeper reads this file as its PEM key store. It can only read PKCS#8 keys, so a PKCS#1 key +## ("-----BEGIN RSA PRIVATE KEY-----") fails with +## "InvalidKeySpecException: Inappropriate key specification: Unable to decode key". +## With cert-manager, these keys are created by a Certificate that has `privateKey.encoding: PKCS8` and +## `additionalOutputFormats: [{type: CombinedPEM}]`. CombinedPEM requires cert-manager 1.15 or later, or the +## `AdditionalCertificateOutputFormats=true` feature gate in earlier versions. +## The certificates must include the Kubernetes names that the other components use to connect to the +## component, as listed for `tls.common.sanMode`. Except for the proxy certificate, they must allow both the +## server auth and client auth usages, so they must be issued by a private CA (see above). +## When `certs.internal_issuer.enabled` is false, set `certs.issuers.ca.secretName` to a Secret with a +## "ca.crt" key that contains the CA certificates that issued the component certificates. The components +## use it as their trust store, so it's required even when the chart doesn't create any certificates. +## `certs.issuers.ca.name` is only needed when the chart creates certificates. tls: enabled: false ca_suffix: ca-tls @@ -348,13 +415,20 @@ tls: # - none: no wildcard or per-pod SANs; only the service FQDN and short name are added # (plus the headless name for broker/zookeeper). Use tls..dnsNames # and/or ipAddresses to add your own. + # In every mode, the certificates include the Kubernetes names that the other components use to connect + # to the component, because the connections verify that the certificate matches the name: + # - "-pulsar-" and "-pulsar-..svc." + # - for broker and zookeeper, also "-pulsar--headless..svc." + # These names can't be removed. Certificates from a public CA can't include them, so use a publicly + # trusted certificate only for the proxy, as described in the TLS comments above. # /!\ Any modification will require a restart of the component pods to pick up the new certs. sanMode: wildcard # settings for generating certs for proxy proxy: enabled: false cert_name: tls-proxy - # set to false if you want to use an existing certificate + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. createCert: true # The dnsNames field specifies a list of Subject Alternative Names to be associated with the certificate. # Works in addition to the automatically generated SANs (if any) based on the `sanMode` setting. @@ -377,6 +451,9 @@ tls: broker: enabled: false cert_name: tls-broker + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. + createCert: true # The dnsNames field specifies a list of Subject Alternative Names to be associated with the certificate. # Works in addition to the automatically generated SANs (if any) based on the `sanMode` setting. dnsNames: @@ -398,6 +475,9 @@ tls: bookie: enabled: false cert_name: tls-bookie + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. + createCert: true # The dnsNames field specifies a list of Subject Alternative Names to be associated with the certificate. # Works in addition to the automatically generated SANs (if any) based on the `sanMode` setting. dnsNames: @@ -415,6 +495,9 @@ tls: zookeeper: enabled: false cert_name: tls-zookeeper + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. + createCert: true # The dnsNames field specifies a list of Subject Alternative Names to be associated with the certificate. # Works in addition to the automatically generated SANs (if any) based on the `sanMode` setting. dnsNames: @@ -431,6 +514,9 @@ tls: # settings for generating certs for recovery autorecovery: cert_name: tls-recovery + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. + createCert: true # The dnsNames field specifies a list of Subject Alternative Names to be associated with the certificate. # Works in addition to the automatically generated SANs (if any) based on the `sanMode` setting. dnsNames: @@ -447,6 +533,9 @@ tls: # settings for generating certs for toolset toolset: cert_name: tls-toolset + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. + createCert: true # The dnsNames field specifies a list of Subject Alternative Names to be associated with the certificate. # Works in addition to the automatically generated SANs (if any) based on the `sanMode` setting. dnsNames: @@ -463,6 +552,9 @@ tls: # settings for standalone TLS cert (used when tls.enabled is true and standalone.enabled is true) standalone: cert_name: tls-standalone + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. + createCert: true # Annotations to apply to the secrets generated by the cert-manager certificate # secretAnnotations: {} # TLS setting for function runtime instance @@ -473,7 +565,8 @@ tls: function_worker: enabled: false cert_name: tls-function-worker - # set to false if you want to use an existing certificate + # Set to false to provide the certificate Secret "-" yourself instead of having the + # chart create it with cert-manager. See the TLS comments above for the required Secret keys. createCert: true # The dnsNames field specifies a list of Subject Alternative Names to be associated with the certificate. # Works in addition to the automatically generated SANs (if any) based on the `sanMode` setting. @@ -619,11 +712,13 @@ certs: secretName: # used for certs.internal_issuer.type as ca or when internal_issuer is disabled ca: - # The name of the issuer, it is mandatory to specify this value if TLS is enabled - # and selfsigning is not used + # The name of the cert-manager issuer that issues the component certificates. Required when TLS is enabled, + # selfsigning is not used and the chart creates at least one certificate (createCert is not false). name: - # The secret name of the CA certificate, it is mandatory to specify this value if TLS is enabled - # and selfsigning is not used + # The name of the Secret with the CA certificate in its "ca.crt" key. Required when TLS is enabled and + # selfsigning is not used. The components mount it as the trust store that they use to verify each other's + # certificates, so it's required even when you provide all the certificates yourself (createCert: false). + # With certs.internal_issuer.type ca, the Secret must also contain the CA private key ("tls.key"). secretName: # The k8s kind of the ca issuer: Issuer or ClusterIssuer kind: Issuer