Right, so you’ve got your Ingress set up and traffic is flowing. But unless you’re running an internal-only service for masochists, you’re going to want to encrypt that traffic. This is where TLS termination comes in. The Ingress controller acts as the endpoint for your TLS connections, decrypting the traffic and passing plain old HTTP to your backend pods. It’s the bouncer that checks IDs at the door so the party inside (your app) doesn’t have to.

The most straightforward, built-in way to handle your TLS certificate is by using a Kubernetes Secret of type kubernetes.io/tls. It’s not the most elegant tool in the shed (we’ll get to that), but it’s the universal joint that makes the basic setup work.

The TLS Secret: What’s Actually In There?

Don’t just wave the magic kubectl wand; know what you’re creating. This isn’t a black box. A kubernetes.io/tls Secret is just a key-value store with two specific, required data keys:

  1. tls.crt: This should contain your PEM-encoded certificate chain. That means your actual certificate and any intermediate certificates that chain up to a root your clients trust. Concatenate them all together in a single file, with your server certificate first.
  2. tls.key: This is the private key that was used to generate the certificate signing request (CSR). This is the crown jewels. If this leaks, the game is over for that certificate.

You can create this secret from your existing certificate files like so:

kubectl create secret tls my-app-tls-secret \
  --namespace=my-namespace \
  --cert=path/to/fullchain.pem \
  --key=path/to/privkey.pem

Let’s be direct: the --cert flag is poorly named. It wants the chain, not just the certificate. This trips people up constantly when they get certificate warnings because they only put the leaf cert in there. Always use the full chain file.

Hooking the Secret to Your Ingress

Once you have the secret, telling your Ingress to use it is dead simple. You add a tls section that specifies the host and the secret name. The key thing to understand here is the hosts field: it must match the hostname defined in the rules section and, crucially, the SNI (Server Name Indication) sent by the client’s browser.

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

See? The tls block is a list, allowing you to specify multiple certificates for different hosts right in the same Ingress. The controller will use SNI to pick the right one.

The Glaring, Obvious Problem: Manual Management

Here’s the part where we call out the questionable choice. Using a Secret like this is brutally manual. The certificate and key are just blobs of text stored in etcd. When your certificate is 90 days from expiring, you must:

  1. Get a new one from your CA (manually or via some script).
  2. Update the Secret with the new tls.crt and tls.key.
  3. Hope the Ingress controller reloads the new secret promptly (most do, but you should verify).

This is a terrible, non-scalable, human-error-prone process. For anything beyond a pet project, this is a hard nope. You will forget. It will expire at 2 AM on a Saturday. This is why tools like cert-manager exist—to automate this entire circus. It’s essentially mandatory for any serious setup. But you still need to understand that under the hood, cert-manager is just creating and updating these same kubernetes.io/tls Secrets on your behalf.

Best Practices and Pitfalls

  • Namespace Co-location: The TLS Secret MUST be in the same namespace as the Ingress object that references it. This is a classic “why is my certificate not working?!” gotcha. You can’t reference a secret from the default namespace in an Ingress in the staging namespace.
  • Don’t Use the Default Certificate Wildly: Many controllers let you configure a default TLS certificate that they’ll serve for requests that don’t match any of the defined SNI hosts. This is useful for a bare IP address request, but the certificate will be invalid for the hostname, causing browser warnings. It’s a debugging tool, not a feature.
  • Secret Access is a Security Risk: Remember, anyone or any service with get permissions on that Secret can read your private key. Lock down RBAC accordingly. Your CI/CD tool that deploys the app probably doesn’t need get permissions on secrets, only create and update.
  • Check Your Controller’s Reload Behavior: Most modern ingress controllers (like ingress-nginx) watch for changes to the Secret and reload their configuration automatically. But it’s not a standard, so check the docs for yours. Don’t assume.

So, in summary: TLS Secrets are the fundamental building block. Understand them intimately because they are the API that everything else plugs into. But for the love of all that is holy, use an automated tool to manage them. Your future self, who is not debugging a production outage caused by an expired cert, will thank you.