Unreleased documentation. Choose your installed release in the version menu. Features described here may be absent from that release.
TLS certificates¶
The Helm chart creates a default certificate for HTTPS. It uses cert-manager when available, or generates a self-signed Secret otherwise. For public traffic, supply a trusted certificate for your hostnames. Choose cert-manager for automatic renewal or an existing certificate from your issuer. Controller-to-agent certificates are separate; see agent certificates.
Choose which connection to secure¶
Client HTTPS and backend TLS are separate connections. A certificate on an Ingress or Gateway secures the client connection; it doesn't enable TLS to the application:
flowchart TB
accTitle: Client HTTPS and backend TLS
accDescr: Clients connect to HAProxy using HTTPS. HAProxy makes a separate connection to the application, using HTTP unless backend TLS is configured.
C[Client] -->|HTTPS| H[HAProxy]
H -->|HTTP or configured TLS| A[Application]
| Connection | Configure |
|---|---|
| Client to an Ingress | A TLS Secret in spec.tls |
| Client to a Gateway | An HTTPS listener |
| HAProxy to an application | Ingress backend TLS or Gateway BackendTLSPolicy |
For client HTTPS, an exact hostname takes precedence over a wildcard. The default certificate covers unmatched hostnames and clients that send no Server Name Indication (SNI).
Default SSL certificate¶
Default behavior (development/testing)¶
The chart creates a self-signed certificate for localdev.me and *.localdev.me:
- cert-manager installed (the
cert-manager.io/v1API is present when Helm renders): the chart creates a self-signedIssuernamed<fullname>-ssl-selfsignedand aCertificate; cert-manager provisions thedefault-ssl-certSecret and renews it before expiry. - cert-manager absent: the chart generates a self-signed
default-ssl-certSecret itself, for the same DNS names. This certificate is valid for 10 years and isn't auto-rotated. The Secret survives uninstall and upgrade (helm.sh/resource-policy: keep), and the chart only generates it when the Secret doesn't already exist — a Secret you created out-of-band is left untouched.
The localdev.me domain resolves to 127.0.0.1 for local testing. For clients
to trust your public endpoint, use a certificate from their trusted issuer.
GitOps tools that render without cluster access
The no-cert-manager fallback checks for an existing Secret with Helm's lookup function, which returns nothing when the chart is rendered without cluster access (helm template, Argo CD) — every sync would then generate a fresh certificate. For those deployments, install cert-manager, or provide the certificate explicitly: inline via defaultSSLCertificate.create/cert/key together with defaultSSLCertificate.certManager.enabled=false, or as a manually created Secret (see Alternative: Manual Certificate). The chart rejects inline creation while cert-manager is enabled because two actors must not own the same Secret.
Production deployment¶
For production, override the default certificate configuration with your actual domain and a trusted issuer:
defaultSSLCertificate:
certManager:
createIssuer: false # Use your own issuer
dnsNames:
- "app.example.com"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
This example assumes cert-manager and a letsencrypt-prod ClusterIssuer are
already configured. Point your hostname at HAPTIC. For an HTTP-01 issuer, the
hostname must be publicly reachable on port 80; see
cert-manager HTTP-01 setup.
Apply the values through your Helm deployment. The chart creates a Certificate
resource; cert-manager provisions and renews its TLS Secret. For a wildcard such
as *.example.com, use a DNS-01 issuer
instead: HTTP-01 can't issue wildcard certificates.
Alternative: Manual certificate¶
Obtain a certificate for your domain and save its PEM certificate chain as
tls.crt and its private key as tls.key. Create a Secret with a new name so
you don't overwrite one managed by Helm or cert-manager:
kubectl create namespace haptic --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret tls app-default-tls \
--cert=tls.crt --key=tls.key --namespace=haptic
Then add these settings to your Helm values and install or upgrade HAPTIC:
You are responsible for renewing this certificate.
Custom certificate names¶
To use a different Secret name or namespace:
The controller references the Secret at certificates/my-wildcard-cert. To use
an existing Secret, also set defaultSSLCertificate.certManager.enabled: false
as in the manual certificate procedure above.
kubectl create secret tls creates a kubernetes.io/tls Secret with the required
tls.crt and tls.key entries. Keep the certificate chain in tls.crt, starting
with the host certificate.
Disable the default certificate¶
To require each HTTPS route to supply its own certificate, disable the default:
This doesn't disable HTTPS for Ingresses or Gateways with their own TLS configuration. For an HTTP-only installation, also leave those routes without TLS configuration and don't configure TLS passthrough.
Certificate rotation¶
With cert-manager: Certificates are automatically renewed before expiration.
Chart-generated self-signed Secret (no cert-manager): never rotated automatically — it's valid for 10 years. Replace it like a manual certificate if you need a different one.
Manual certificates: Update the Secret before the old certificate expires.
For the app-default-tls Secret created above:
# Update Secret with new certificate
kubectl create secret tls app-default-tls \
--cert=new-tls.crt \
--key=new-tls.key \
--namespace=haptic \
--dry-run=client -o yaml | kubectl apply -f -
The controller watches the Secret and automatically deploys the updated certificate to HAProxy.
SSL troubleshooting¶
For SSL symptom diagnosis — "Secret not found" errors, HAProxy failing to start with SSL errors, or a certificate that isn't updating — see Troubleshooting → SSL/TLS Issues.
HTTP strict transport security (HSTS)¶
To send the Strict-Transport-Security response header on every HTTPS response — across all TLS hosts — set these Helm values:
controller:
config:
templatingSettings:
extraContext:
tls:
hsts:
enabled: true
maxAge: "31536000" # one year (default)
includeSubdomains: false
preload: false
HSTS takes effect only over HTTPS, so pair it with an HTTP-to-HTTPS redirect. The rendered config emits a warning when HSTS is on but no redirect is configured.
This sets the header for every host. To enable HSTS per host instead — or override the global value for specific hosts — use the per-Ingress hsts annotations (see Annotations). A per-Ingress annotation wins over the global default for its hosts.
TLS cipher suites and protocol versions¶
The default TLS policy allows TLS 1.2 and 1.3, with cipher suites for RSA and
ECDSA certificates. Set extraContext.tls to change that policy for all HTTPS
listeners:
These are the chart defaults:
controller:
config:
templatingSettings:
extraContext:
tls:
# TLS 1.2 cipher list → ssl-default-bind-ciphers
ciphers: "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA"
# TLS 1.3 cipher suites → ssl-default-bind-ciphersuites
ciphersuites: "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
# Minimum protocol version → ssl-default-bind-options ssl-min-ver
minVersion: "TLSv1.2"
You override one sub-key without restating the others — Helm deep-merges your value with the defaults. Set any value to an empty string ("") to omit its directive and fall back to HAProxy's built-in default. Per-listener Gateway TLS options still override this policy for their own bind.
Certificate algorithm compatibility¶
The default cipher policy supports RSA, ECDSA, and dual certificates. You don't need a separate cipher list for each key type.
Supporting legacy clients¶
Check which protocols and cipher suites the client supports before changing the policy. For a TLS 1.2 client that requires static-RSA key exchange, this example adds static-RSA suites to the default cipher list. Those connections lose forward secrecy:
controller:
config:
templatingSettings:
extraContext:
tls:
ciphers: "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA"
minVersion: "TLSv1.2"
Static-RSA suites require an RSA certificate. Change the minimum TLS version only if the client's protocol support also requires it; a cipher mismatch alone doesn't require allowing TLS 1.0 or 1.1.
Dual RSA and ECDSA certificates¶
You can serve both an ECDSA and an RSA certificate for the same host. HAProxy presents the ECDSA certificate to clients that support it — a smaller, faster handshake — and falls back to the RSA certificate for older clients. It selects per connection from the client's capabilities, so you don't choose which to serve; you provide both.
The setup is always two steps: issue two kubernetes.io/tls Secrets for the host (one ECDSA, one RSA), then reference both from whatever fronts the host — an Ingress, a Gateway listener, or the chart's default certificate. HAPTIC writes every referenced certificate into HAProxy's certificate list under the host's SNI.
Issue the two certificates (cert-manager)¶
Create two Certificate resources for the same DNS names, one per key algorithm:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-ecdsa
namespace: my-app
spec:
secretName: app-tls-ecdsa
dnsNames: ["app.example.com"]
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-rsa
namespace: my-app
spec:
secretName: app-tls-rsa
dnsNames: ["app.example.com"]
privateKey:
algorithm: RSA
size: 2048
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
Use an issuer that supports both key algorithms. For a wildcard certificate, use a DNS-01 issuer; HTTP-01 can't validate wildcards.
Ingress¶
Reference both Secrets from the Ingress with two spec.tls entries for the same host:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
namespace: my-app
spec:
ingressClassName: haptic
tls:
- hosts: ["app.example.com"]
secretName: app-tls-ecdsa
- hosts: ["app.example.com"]
secretName: app-tls-rsa
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 80
A single-algorithm setup needs only one Certificate and one spec.tls entry.
The default cipher policy supports RSA and ECDSA certificates.
Gateway API¶
A Gateway HTTPS listener takes a list of certificate references, so serve dual by giving it both Secrets:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: app
namespace: my-app
spec:
gatewayClassName: haptic
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: app.example.com
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: app-tls-ecdsa
- kind: Secret
name: app-tls-rsa
HAPTIC emits both certificates into that listener's certificate list under its hostname, exactly as it does for an Ingress.
Default certificate¶
The default certificate is what HAProxy serves when a connection sends no SNI, or an SNI that matches no configured host. It's set once for the whole controller through defaultSSLCertificate, and it can be dual as well — useful when a wildcard default (for example *.example.com) fronts many teams' hosts and you want the faster ECDSA handshake there without each team managing its own certificate.
Point defaultSSLCertificate.secretName at the primary (RSA) Secret and set defaultSSLCertificate.ecdsaSecretName to the ECDSA companion:
defaultSSLCertificate:
secretName: wildcard-rsa-tls # primary, e.g. RSA *.example.com
ecdsaSecretName: wildcard-ecdsa-tls # ECDSA companion for the same names
certManager:
enabled: false # The Secrets already exist
Both Secrets must live in the same namespace (defaultSSLCertificate.namespace, which defaults to the release namespace). HAPTIC then emits two default certificate-list lines and HAProxy serves ECDSA to modern clients and the primary certificate to the rest on the default path.
For a single-algorithm default, leave ecdsaSecretName empty (the default) and put whichever certificate you want in secretName — an RSA cert for RSA-only, or an ECDSA cert for ECDSA-only. secretName is the default certificate regardless of key type; ecdsaSecretName only adds the second, ECDSA-preferred certificate when you want both.
Gateway-owned default
If a Gateway HTTPS listener with no hostname claims the default slot, that listener owns the default certificate — give it two certificateRefs for a dual default there. ecdsaSecretName applies to the chart's defaultSSLCertificate, not to a Gateway-owned default.
TLS session resumption¶
TLS session resumption reduces handshake work when a client reconnects. A session ticket lets the client reuse parameters from a previous connection.
Enable shared ticket keys so clients can resume sessions across HAProxy pods. Shared keys are off by default; that setting doesn't disable HAProxy's own session resumption:
Key rotation¶
On the first render of a new day in Coordinated Universal Time (UTC), HAPTIC appends a new random key and removes the oldest, retaining three keys. HAProxy uses the middle key to issue tickets and all three to decrypt them, as described in the HAProxy ticket-key reference. Rotation takes effect through a configuration reload. Whether a client can resume also depends on its ticket lifetime and the server's session settings.
Webhook certificates¶
Admission webhook certificates protect Kubernetes configuration changes. Use webhook certificate management for their renewal, cert-manager setup, and manual CA bundles.
See also¶
- Security — webhook hardening, RBAC, and network exposure
- Troubleshooting — SSL symptom diagnosis and general debugging