k3s ships with Traefik enabled by default — this cluster disables it and installs ingress-nginx instead. ingress-nginx is installed on k3s via a ServiceLB LoadBalancer Service, which binds ports 80 and 443 directly to the node IP. The default-ssl-certificate flag tells nginx to use wildcard-tls for every HTTPS connection whose SNI hostname isn't matched by a per-Ingress cert (which is every connection, since no per-Ingress certs exist).
01 Key configuration
controller:
ingressClassResource:
default: true # cluster-wide default IngressClass
extraArgs:
default-ssl-certificate: networking/wildcard-tls # wildcard cert for all vhosts
service:
type: LoadBalancer # fulfilled by k3s ServiceLB (klipper-lb)
externalTrafficPolicy: Local # preserve real client IPs
config:
use-forwarded-headers: "true"
ssl-protocols: "TLSv1.2 TLSv1.3" # no TLS 1.0/1.1
client-max-body-size: "100m" # needed for Nextcloud / Immich uploads
02 What an Ingress looks like
Because TLS is handled at the controller level, every Ingress in this cluster is clean and minimal — just routing rules and a redirect annotation:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
namespace: whoami
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# no cert-manager.io/cluster-issuer — not needed
spec:
ingressClassName: nginx
# no tls[] block — wildcard cert is served automatically
rules:
- host: whoami.in.alybadawy.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
03 Adding a new service
Copy the pattern above. The only required annotations are the two ssl-redirect ones. The wildcard cert covers any *.in.alybadawy.com hostname automatically.
tls[] block. If you add a tls[] section referencing a non-existent secret, nginx will fall back to a self-signed cert for that host. If you add cert-manager.io/cluster-issuer, cert-manager will try to create a redundant per-hostname cert. Both are unnecessary and break the clean architecture.04 ArgoCD special case
ArgoCD's server runs in --insecure mode because nginx handles TLS termination. Without this, you get double-TLS: nginx decrypts HTTPS, then tries to re-encrypt to the backend, which causes connection failures with ArgoCD's gRPC. The backend-protocol: HTTP annotation tells nginx to use plain HTTP to the backend:
server:
ingress:
annotations:
nginx.ingress.kubernetes.io/backend-protocol: HTTP # nginx → argocd: plain HTTP
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
tls: false # no per-ingress cert; wildcard handles it
configs:
params:
server.insecure: true # argocd-server: accept plain HTTP from nginx