<kubernetes/> ·Saturday, April 4, 2026· 7 min· 1319 words

Kubernetes for Cloud Engineers: Default TLS Certificates

This is the first post in a series for new operations and cloud engineers getting started with Kubernetes. Whether you’re running K3s on a Raspberry Pi, EKS in AWS, AKS in Azure, or anything in between, the concepts are the same. Let’s get into it.


The problem #

You’ve got a Kubernetes cluster. You’ve got an ingress controller routing traffic. You hit your app over HTTPS and… you get a browser warning about an untrusted self-signed certificate. Or worse, you’ve got ten services and you’re copy-pasting the same TLS secret into every single Ingress resource.

The fix: set a default TLS certificate on your ingress controller. One cert to rule them all. Any request that doesn’t match a more specific TLS config falls back to this default. Clean, simple, done.

Every controller does this slightly differently. Let’s walk through each one.


First, create your TLS secret #

This part is the same regardless of which controller you’re running. You need a Kubernetes TLS secret containing your certificate and private key.

# Create the TLS secret from your cert and key files
kubectl create secret tls default-tls \
  --cert=tls.crt \
  --key=tls.key \
  -n default

# secret/default-tls created

Your cert file should contain the full chain: leaf → intermediate → root. If you’re using cert-manager or Let’s Encrypt, this is handled for you. If you’re doing it by hand, get the order right or you’ll be debugging trust chain errors at 2am.


Ingress Controllers (Legacy Ingress API) #

Heads up: Ingress NGINX is retired. The kubernetes/ingress-nginx project, the one most of us grew up on, officially reached end-of-life on March 24, 2026. The repository is now read-only. No more releases, no bug fixes, no security patches. Your existing deployments won’t break overnight, but you’re flying without a safety net.

The Kubernetes Steering Committee and Security Response Committee issued a joint statement in January 2026 urging migration. The recommended path forward is Gateway API, which has been GA since October 2023 and now has 20+ conformant implementations.

If you’re starting fresh, skip to the Gateway API section below. If you’re migrating, check out ingress2gateway, the official migration tool that now supports 30+ annotation conversions.

Note: NGINX Inc.’s commercial ingress controller (F5/NGINX) is a completely separate codebase and remains actively maintained. Don’t confuse the two.

Ingress NGINX (kubernetes/ingress-nginx) #

Even though it’s retired, you’ll encounter this in the wild for a while. The default cert is set via a controller argument.

With Helm:

# values.yaml
controller:
  extraArgs:
    default-ssl-certificate: "default/default-tls"
helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
  -f values.yaml \
  -n ingress-nginx

Without Helm, patch the controller deployment directly:

# Add to the container args in the ingress-nginx-controller Deployment
spec:
  containers:
  - name: controller
    args:
    - /nginx-ingress-controller
    - --default-ssl-certificate=default/default-tls

The format is namespace/secret-name. Without this flag, NGINX generates a self-signed certificate for the catch-all server. That’s the browser warning you’re seeing.

Gotcha: If your Ingress resource has a tls: section but no secretName, NGINX uses this default cert and forces an HTTPS redirect. If the tls: section is missing entirely, it still serves the default cert but does not redirect. Use force-ssl-redirect: "true" in the ConfigMap if you want to enforce HTTPS everywhere.


HAProxy Ingress #

There are two HAProxy ingress projects: the community haproxy-ingress and the one from HAProxy Technologies. Both support the same flag pattern.

With Helm (HAProxy Technologies):

# values.yaml
controller:
  defaultTLSSecret:
    secretNamespace: default
    secret: default-tls

With controller args (both projects):

--default-ssl-certificate=default/default-tls
helm upgrade haproxy-ingress haproxytech/kubernetes-ingress \
  --set controller.defaultTLSSecret.secretNamespace=default \
  --set controller.defaultTLSSecret.secret=default-tls \
  -n haproxy-ingress

Without a default cert configured, both HAProxy implementations generate a self-signed fake certificate. Same story as NGINX.

Note: HAProxy Technologies controller v1.11+ automatically enables QUIC (HTTP/3) when you set a default TLS cert. If you don’t want that yet, add --disable-quic.


Istio Ingress Gateway #

Istio doesn’t use the Kubernetes Ingress API at all. It has its own Gateway CRD (not the same as Gateway API, I know, the naming is painful).

Important: The TLS secret must live in the same namespace as the Istio ingress gateway pod, typically istio-system.

kubectl create secret tls default-tls \
  --cert=tls.crt \
  --key=tls.key \
  -n istio-system

# secret/default-tls created

Then create the Gateway resource:

# istio-gateway.yaml
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: default-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: default-tls
    hosts:
    - "*.example.com"
kubectl apply -f istio-gateway.yaml
# gateway.networking.istio.io/default-gateway created

Gotchas:

  • credentialName must exactly match the Kubernetes secret name. No namespace/name format here. It just looks in its own namespace.
  • Istio doesn’t have a single “default certificate” concept. You configure TLS per-server block on the Gateway. To make it act as a default, use a wildcard host like *.example.com.
  • Wrong namespace is the #1 debugging headache. If TLS isn’t working, check the secret namespace first.

Gateway API #

Gateway API is the future. It’s the Kubernetes-native standard from SIG-Network, GA since October 2023, and every major ingress controller is building an implementation. If you’re starting fresh, start here.

The big difference: there’s no --default-ssl-certificate flag. TLS is configured declaratively on the Gateway resource itself, per-listener. Each HTTPS listener explicitly references a certificate via certificateRefs.

NGINX Gateway Fabric #

NGINX Gateway Fabric is NGINX’s Gateway API implementation, completely separate from the retired Ingress NGINX project.

# gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: default-gateway
spec:
  gatewayClassName: nginx
  listeners:
  - name: http
    port: 80
    protocol: HTTP
  - name: https
    hostname: "*.example.com"
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
      - kind: Secret
        name: default-tls
kubectl apply -f gateway.yaml
# gateway.gateway.networking.k8s.io/default-gateway created

Then attach your HTTPRoutes to the HTTPS listener:

# route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
spec:
  parentRefs:
  - name: default-gateway
    sectionName: https
  hostnames:
  - "app.example.com"
  rules:
  - backendRefs:
    - name: my-app
      port: 80

With cert-manager, add an annotation and cert-manager handles the rest:

# gateway.yaml: cert-manager will create and manage the secret
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: default-gateway
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: nginx
  listeners:
  - name: https
    hostname: "app.example.com"
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
      - kind: Secret
        name: default-tls

HAProxy (Gateway API) #

The beauty of Gateway API is that it’s a standard. The Gateway resource looks almost identical regardless of the underlying implementation. You just swap the gatewayClassName.

# gateway.yaml: same pattern, different class
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: default-gateway
spec:
  gatewayClassName: haproxy
  listeners:
  - name: https
    hostname: "*.example.com"
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
      - kind: Secret
        name: default-tls
kubectl apply -f gateway.yaml
# gateway.gateway.networking.k8s.io/default-gateway created

That’s it. Same spec. Same structure. The gatewayClassName tells Kubernetes which controller reconciles the resource. This is exactly why Gateway API is the future. You can swap implementations without rewriting your config.

Gotchas for both implementations:

  • Gateway API doesn’t have a global “default certificate” flag. Each HTTPS listener must explicitly reference a cert via certificateRefs. To cover everything, use a wildcard hostname.
  • The TLS secret must be in the same namespace as the Gateway by default. For cross-namespace references, create a ReferenceGrant.
  • TLS modes: Terminate means the gateway decrypts traffic (most common). Passthrough forwards encrypted traffic as-is. Use this with TLSRoute, not HTTPRoute.
  • NGINX Gateway Fabric currently supports only a single certificateRef per listener. If you need multiple certs, create multiple listeners.

Wrapping up #

Here’s the cheat sheet:

ControllerMethodFormat
Ingress NGINX (retired)--default-ssl-certificatenamespace/secret
HAProxy Ingress--default-ssl-certificatenamespace/secret
IstiocredentialName on Gateway serversecret name (same namespace)
Gateway API (any impl)certificateRefs on listenersecret reference

If you’re starting fresh, go straight to Gateway API. Pick an implementation (NGINX Gateway Fabric, HAProxy, Envoy Gateway, whatever fits your stack), define your Gateway with a TLS listener, and move on.

If you’re migrating from Ingress NGINX, take a breath. Your cluster won’t explode tomorrow. But start planning the move. The ingress2gateway tool can automate most of the conversion, and the Gateway API spec is stable and well-documented.

Next up in this series: cert-manager and automated TLS. Creating secrets by hand is so 2024.

kubernetesk8stlsingressgateway-apicloud-native