Kubernetes TLS: Securing Ingress with Certificates

Managing TLS in Kubernetes is different enough from regular server TLS that it’s worth covering separately. The concepts are the same — certificates, private keys, certificate authorities — but the mechanics of how you provision, store, and rotate certificates involve Kubernetes-specific objects.

I’ve worked through this on a few clusters. Here’s the practical picture.

The Basics: TLS Secrets

Kubernetes stores TLS certificates in Secrets of type kubernetes.io/tls. These contain two pieces:

  • tls.crt — the certificate (PEM format)
  • tls.key — the private key (PEM format)

Create one manually:

kubectl create secret tls my-tls-secret \
  --cert=path/to/certificate.pem \
  --key=path/to/private.key \
  --namespace=production

Or as YAML:

apiVersion: v1
kind: Secret
metadata:
  name: my-tls-secret
  namespace: production
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded-certificate>
  tls.key: <base64-encoded-key>

The certificate file should be the full chain (cert + intermediates), not just the leaf certificate. Nginx and most other ingress controllers expect the full chain.

Ingress with TLS

A basic Ingress resource with TLS:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
  - hosts:
    - example.com
    - www.example.com
    secretName: my-tls-secret
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-service
            port:
              number: 80

The tls section references the Secret. The Ingress controller reads the certificate from the Secret and configures TLS accordingly.

Multiple hosts with different certificates:

spec:
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls-secret
  - hosts:
    - app.example.com
    secretName: app-tls-secret

cert-manager: Automating Certificate Issuance

Manual certificate management doesn’t scale. cert-manager is the standard solution for automating certificate issuance and renewal in Kubernetes. It supports Let’s Encrypt, ZeroSSL, Vault, and custom CAs.

Install with Helm:

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

ClusterIssuer for Let’s Encrypt

Create a ClusterIssuer (cluster-wide, not namespace-scoped):

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: you@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
    - http01:
        ingress:
          class: nginx

This uses HTTP-01 challenge. For wildcard certificates, use DNS-01:

    solvers:
    - dns01:
        cloudflare:
          email: you@example.com
          apiTokenSecretRef:
            name: cloudflare-api-token
            key: api-token

Create the Cloudflare API token secret:

kubectl create secret generic cloudflare-api-token \
  --from-literal=api-token=your-token \
  --namespace=cert-manager

Certificate Resource

Request a certificate explicitly:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls
  namespace: production
spec:
  secretName: example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: example.com
  dnsNames:
  - example.com
  - www.example.com

cert-manager creates a Secret with the certificate and manages renewal automatically.

Ingress with cert-manager Annotation

The simpler approach — annotate the Ingress and cert-manager handles everything:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
  - hosts:
    - example.com
    secretName: example-com-tls    # cert-manager creates and manages this
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app
            port:
              number: 80

cert-manager detects this Ingress, sees the annotation, and automatically creates a Certificate resource and manages the TLS Secret. When the cert nears expiry, cert-manager renews it automatically. No manual intervention.

Checking Certificate Status

# List all Certificate objects
kubectl get certificates --all-namespaces

# Detailed status of a certificate
kubectl describe certificate example-com-tls -n production

# Check cert-manager logs for issues
kubectl logs -n cert-manager deployment/cert-manager

Certificate conditions show the current state:

kubectl get certificate example-com-tls -n production -o jsonpath='{.status.conditions[*]}'

A healthy certificate shows Ready=True. If it’s stuck, the events section of kubectl describe usually explains why — typically DNS challenge failures, rate limits, or networking issues.

Internal Services: Private CA with cert-manager

For services that don’t need public certificates:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-keypair

First, create the CA secret:

# Create CA key and certificate (or use your existing CA)
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
  -subj "/CN=Internal CA/O=MyOrg"

kubectl create secret tls internal-ca-keypair \
  --cert=ca.crt --key=ca.key \
  --namespace=cert-manager

Then cert-manager can issue certificates signed by your internal CA. Install the CA certificate in your service trust stores for them to trust these certificates.

mTLS Between Services

For mutual TLS between services (both sides authenticate with certificates):

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: service-client-cert
  namespace: production
spec:
  secretName: service-client-tls
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  commonName: service-a
  usages:
  - client auth
  - digital signature
  - key encipherment

Services mount this Secret and present the client certificate when connecting to other services. Service meshes like Istio automate this even further by transparently injecting mTLS without application changes.

Common Issues

Certificate stuck in Pending — check if the HTTP-01 challenge endpoint is accessible from Let’s Encrypt’s servers. Also check if there are existing Ingress rules blocking /.well-known/acme-challenge/.

Rate limit errors — Let’s Encrypt rate limits are per domain. Switch to the staging server for testing: https://acme-staging-v02.api.letsencrypt.org/directory.

Wrong certificate being served — if your Ingress controller serves the wrong cert, check for conflicts in TLS section across Ingress resources for the same hostname.

Certificate expiry not caught — cert-manager renews at 2/3 of the validity period by default. But if renewals are failing silently, you won’t know until it’s almost expired. Set up monitoring for Certificate resources — a Ready=False condition means something needs attention.

For the concepts behind certificate chains and how trust works in these setups, the SSL certificate chain guide explains how root, intermediate, and end-entity certificates relate to each other.

Scroll to Top