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 createdYour 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-nginxWithout 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-tlsThe 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-tlsWith controller args (both projects):
--default-ssl-certificate=default/default-tlshelm upgrade haproxy-ingress haproxytech/kubernetes-ingress \
--set controller.defaultTLSSecret.secretNamespace=default \
--set controller.defaultTLSSecret.secret=default-tls \
-n haproxy-ingressWithout 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 createdThen 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 createdGotchas:
credentialNamemust exactly match the Kubernetes secret name. Nonamespace/nameformat 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-tlskubectl apply -f gateway.yaml
# gateway.gateway.networking.k8s.io/default-gateway createdThen 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: 80With 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-tlsHAProxy (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-tlskubectl apply -f gateway.yaml
# gateway.gateway.networking.k8s.io/default-gateway createdThat’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:
Terminatemeans the gateway decrypts traffic (most common).Passthroughforwards encrypted traffic as-is. Use this withTLSRoute, notHTTPRoute. - NGINX Gateway Fabric currently supports only a single
certificateRefper listener. If you need multiple certs, create multiple listeners.
Wrapping up #
Here’s the cheat sheet:
| Controller | Method | Format |
|---|---|---|
| Ingress NGINX (retired) | --default-ssl-certificate | namespace/secret |
| HAProxy Ingress | --default-ssl-certificate | namespace/secret |
| Istio | credentialName on Gateway server | secret name (same namespace) |
| Gateway API (any impl) | certificateRefs on listener | secret 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.