TLS and mTLS
By default the pod serves plain HTTP and something in front of it terminates TLS: the Ingress controller through ingress.tls, or the Gateway listener an HTTPRoute attaches to. That model is the simpler one and it is what most deployments should keep. See Networking for both routes.
The tls block covers the deployments where that is not enough:
- the hop between the proxy and the pod must be encrypted too, because a mesh re-encrypts backend traffic, because a Gateway API
BackendTLSPolicyrequires it, or because a control mandates encryption in transit everywhere - nothing fronts the Service, so the pod is the TLS endpoint
- clients must prove who they are with a certificate, not only with an OIDC token
Prerequisites
A server image that supports the server.tls configuration section. A server that does not know the section ignores it and keeps serving plain HTTP, while the chart has already pointed the probes at HTTPS, so the pods fail readiness and the release never finishes rolling.
Support landed after server v0.0.3, and at the time of writing no tagged release carries it. image.tag defaults to the chart’s appVersion, so a default install pulls an image without server.tls: set image.tag to a release that has it. Compatibility lists the verified combinations.
A build new enough for server.tls is also new enough to require a top-level bundle_enforcement value, which the chart does not render. Supply it through extraEnv as described under Policy bundles, or the pod fails to start before TLS ever comes into play.
A certificate and key in a Secret you own. The chart never generates certificates. A kubernetes.io/tls Secret from cert-manager, from an internal PKI, or from kubectl create secret tls works as it is, because tls.certKey and tls.keyKey already default to tls.crt and tls.key. A self-signed certificate is a local testing tool: it forces every client to trust a CA created for one workload, which is the opposite of what a certificate is for. Local TLS testing covers that workflow.
Serve HTTPS from the pod
tls:
enabled: true
existingSecret: github-sts-tls
reloadInterval: "1h"The chart refuses to render tls.enabled: true without tls.existingSecret, rather than starting a pod that has no certificate to serve.
Enabling TLS changes five things, and deliberately not a sixth:
| What | Change |
|---|---|
| Certificate material | Projected read-only at tls.mountPath, default /etc/github-sts-tls, and written into the ConfigMap as server.tls.cert_file and server.tls.key_file. |
| Container and Service port | Named https instead of http, and the Service port gains appProtocol: https. Mesh implementations and some ingress controllers read the port name to decide how to speak to the backend. |
| Probes | httpGet with scheme: HTTPS. The kubelet does not verify the certificate on a probe, so a certificate valid only for the public hostname still passes. |
| Prometheus scrapes | ServiceMonitor and PodMonitor endpoints get scheme: https. |
helm test hooks | Fetch over HTTPS without verifying the certificate. |
| The proxy in front | Nothing. Ingress and Gateway keep sending plain HTTP until you configure them, which is the next section. |
The default is a directory of its own rather than a subdirectory of /etc/github-sts, which keeps the certificate material separate from the configuration mount and from the per-app key mounts. Override tls.mountPath with any path the container does not already use.
Rotate without a restart
tls.reloadInterval is a Go duration, and the server polls the mounted files on that interval and reloads the key pair in place when either changes. Leave it empty and the process keeps the certificate it read at startup, so a renewal only takes effect the next time the pod restarts.
Kubernetes refreshes a mounted Secret in place, so nothing else has to happen for the new file to appear in the pod. Pick an interval well under the renewal window. cert-manager renews at two thirds of the certificate lifetime by default, so an hourly poll is comfortable for a 90 day certificate and still fast enough for a short-lived one.
Tell the proxy to speak HTTPS
Turning on tls does not reconfigure the thing in front of the Service, and the backend protocol is controller-specific.
For ingress-nginx:
ingress:
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"For Gateway API, add a BackendTLSPolicy next to the release. The chart does not render one, because the API version tracks the Gateway API installation in your cluster rather than the chart:
apiVersion: gateway.networking.k8s.io/v1alpha3
kind: BackendTLSPolicy
metadata:
name: github-sts
spec:
targetRefs:
- group: ""
kind: Service
name: github-sts
sectionName: https
validation:
caCertificateRefs:
- group: ""
kind: Secret
name: github-sts-tls
hostname: sts.example.comA proxy that still sends plain HTTP to an HTTPS listener returns a gateway error to every client, and the pods stay healthy while it happens, so the failure looks like a routing problem rather than a TLS one.
Require client certificates
tls:
enabled: true
existingSecret: github-sts-tls
clientAuth:
enabled: true
existingSecret: github-sts-client-ca
caKey: ca.crtLeave clientAuth.existingSecret empty and the CA bundle is read from tls.existingSecret, which is where cert-manager writes ca.crt next to the serving certificate.
The server requires and verifies a client certificate for the whole listener. /health, /ready, and /metrics are not exceptions, so every caller that cannot present a certificate loses access at the handshake. The chart adjusts what it controls:
| Caller | What the chart does |
|---|---|
| kubelet probes | Switches startup, readiness, and liveness to tcpSocket. The kubelet cannot present a client certificate, and an HTTPS probe against this listener would fail the handshake and restart the container in a loop. |
helm test hooks | Stops rendering them. The hook Pods have no certificate, so every assertion would fail at the handshake. |
| Prometheus | Nothing automatic. Put the client credentials in serviceMonitor.tlsConfig or podMonitor.tlsConfig, or scrapes fail. |
The probe change is a real loss of signal. A tcpSocket probe proves the listener accepts connections, not that /ready returns 200, so a server that is up but not ready stays in the Service endpoints. Readiness gating for anything richer than that has to come from a caller that holds a certificate. probes.mode: httpGet restores the HTTPS probes, which then fail the handshake against this listener like any other client without a certificate. It is correct only when something else terminates mTLS in front of the container.
Scrape metrics over TLS
With tls.enabled and no explicit configuration, the chart writes insecureSkipVerify: true into the scrape endpoint. Prometheus connects to the Pod IP, and a certificate issued for the Service or the public hostname carries no SAN for it, so verification would fail on every scrape. The scrape is encrypted, the target is not authenticated.
Verify the target properly by naming the CA and the hostname the certificate was issued for:
serviceMonitor:
enabled: true
tlsConfig:
ca:
secret:
name: github-sts-tls
key: ca.crt
serverName: sts.example.comUnder clientAuth, add the client credentials Prometheus should present:
serviceMonitor:
tlsConfig:
cert:
secret:
name: prometheus-client-cert
key: tls.crt
keySecret:
name: prometheus-client-cert
key: tls.keypodMonitor.tlsConfig takes the same shape. Setting either replaces the default, so insecureSkipVerify is gone as soon as you supply your own block.
Choose a version and cipher suites
tls.minVersion accepts "1.2", the default, and "1.3". Under TLS 1.3 the cipher suites are no longer configurable, and clients that cannot speak 1.3 are rejected, which includes the BusyBox image the helm test hooks run by default.
tls.cipherSuites is an allow-list of IANA names for TLS 1.2 only. Leave it empty and the server uses the Go defaults: ECDHE key exchange throughout, AES-GCM and ChaCha20-Poly1305 preferred, but AES-CBC with a SHA-1 MAC still accepted if a client insists. Pin the list when that last part is not acceptable to you. Setting it together with minVersion: "1.3" is a configuration error the server rejects at startup, so the chart fails the render instead:
tls:
minVersion: "1.2"
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256Verify
Read what the chart wrote before applying it:
helm template github-sts charts/github-sts --values values.yaml \
--show-only templates/configmap.yamlConfirm the probes match the listener:
kubectl get deployment github-sts --namespace github-sts \
-o jsonpath='{.spec.template.spec.containers[0].readinessProbe}'httpGet with "scheme":"HTTPS" is the TLS case, tcpSocket is the mTLS case, and plain httpGet means tls.enabled never took effect.
Confirm the endpoint answers over TLS from inside the cluster:
kubectl run tls-check --rm -i --restart=Never --namespace github-sts \
--image=curlimages/curl:8.11.1 -- -sS -k https://github-sts:8080/health-k skips verification, so this proves TLS is being served, not that the certificate is the one you meant to serve. To check the certificate itself, forward the port and read the chain:
kubectl port-forward --namespace github-sts svc/github-sts 8443:8080
openssl s_client -connect localhost:8443 -showcerts </dev/nullUnder clientAuth, the same command fails the handshake until it presents a certificate:
openssl s_client -connect localhost:8443 -cert client.crt -key client.key </dev/nullFinish with the chart’s own hooks, which follow the scheme automatically:
helm test github-sts --namespace github-stsKnown limitations
- Under
clientAuth, probes only prove the port accepts connections, andhelm testis unavailable. - The
helm testhooks run BusyBox, whosewgetspeaks TLS 1.2 only. WithminVersion: "1.3", pointtests.imageat a curl image such ascurlimages/curl:8.11.1. The hook script preferscurlwhen the image provides it and falls back towget. tls.mountPathcannot be a subdirectory of/etc/github-sts.- The chart mounts certificates and never creates them. Renewal belongs to cert-manager or your PKI.
- Prometheus does not verify the target unless you configure
tlsConfigyourself.
Next
- Values Reference for every
tlsvalue and its default - Rendered Resources for the mounts and ports these values change
- Networking for the route in front of the Service
- Configuration for what the server does with the
server.tlsblock this chart writes - Security Model for what the server authenticates on top of the transport