🏷️ Properly set up Ingress objects with TLS#
Ingress is a Layer 7 routing API for HTTP/HTTPS traffic. It defines rules to route external requests to internal services based on hostnames, paths, TLS termination, and more. It consolidates multiple services under a single external IP, supports advanced routing, and works with an Ingress controller (e.g., NGINX, Traefik). It’s ideal for web applications needing cost-effective, scalable, and secure access.
Tip
Key decision flow
Use ClusterIP for internal-only communication.
Use NodePort only for temporary, non-production, or edge use.
Use LoadBalancer for dedicated external IPs or non-HTTP traffic.
Use Ingress for HTTP/HTTPS routing with multiple services, path/host-based rules, TLS, and centralized edge control.
Create an Ingress#
Create 2 deployments in default namespace.
ᐅ kubectl create deployment app1 --image=nginx --replicas=3 --port=80 --dry-run=client -o yaml
ᐅ kubectl create deployment app1 --image=nginx --replicas=3 --port=80
ᐅ kubectl create deployment app2 --image=httpd --replicas=3 --port=80 --dry-run=client -o yaml
ᐅ kubectl create deployment app2 --image=httpd --replicas=2 --port=80
Create 2 services.
ᐅ k expose deployment app1
ᐅ k expose deployment app2
ᐅ k get deploy,svc
Install NGINX Ingress Controller.
ᐅ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
ᐅ helm install ingress-nginx ingress-nginx/ingress-nginx \
--create-namespace \
--namespace ingress-nginx \
--set controller.replicaCount=2 \
--set controller.service.type=NodePort \
--set controller.service.nodePorts.http=30080 \
--set controller.service.nodePorts.https=30443
Create ingress for apps.
ᐅ cat <<EOF | k apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: apps-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: "/"
spec:
ingressClassName: nginx
rules:
- host: "app1.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: app1
port:
number: 80
- host: "app2.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: app2
port:
number: 80
EOF
Get node IP addresses.
ᐅ k get no -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.addresses[0].address}{"\n"}'
controlplane-01 172.16.0.73
worker-01 172.16.0.74
Get ingress controller service nodeport.
ᐅ k get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{range .spec.ports[*]}{.name}{"\t"}{.nodePort}{"\n"}'
http 30080
https 30443
Check ingress routing.
ᐅ curl -H "Host: app1.com" -sI http://172.16.0.73:30080 | awk 'NR==1'
HTTP/1.1 200 OK
ᐅ curl -H "Host: app2.com" -sI http://172.16.0.74:30080 | awk 'NR==1'
HTTP/1.1 200 OK
ᐅ curl -sI http://172.16.0.73:30080 | awk 'NR==1'
HTTP/1.1 404 Not Found
Create an Ingress with TLS#
Delete previous Ingress.
ᐅ k delete ingress apps-ingress
Create a self-signed certificate.
ᐅ openssl req -x509 -nodes -newkey rsa:4096 -keyout tls.key \
-out tls.crt -subj="/CN=myapp.com" -addext "subjectAltName = DNS:myapp.com"
Create a tls secret with the key and the certificate.
ᐅ k create secret tls myapp-tls --key tls.key --cert tls.crt
ᐅ k get secret myapp-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text
Create a new Ingress
ᐅ k create ingress app-ingress-tls \
--rule "myapp.com/app1=app1:80,tls=myapp-tls" \
--rule "myapp.com/app2=app2:80,tls=myapp-tls" \
--class nginx --annotation nginx.ingress.kubernetes.io/rewrite-target="/\$1" \
--dry-run=client -o yaml > app-ingress-tls.yam
ᐅ sed -i 's/Exact/Prefix/g' app-ingress-tls.yaml
Deploy the new Ingress.
ᐅ k apply -f app-ingress-tls.yaml
ᐅ k get deploy,svc,ingress
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/app1 3/3 3 3 14h
deployment.apps/app2 2/2 2 2 14h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/app1 ClusterIP 10.96.2.254 <none> 80/TCP 14h
service/app2 ClusterIP 10.96.236.43 <none> 80/TCP 14h
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 89d
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/app-ingress-tls nginx myapp.com 80, 443 4s
Then check the Ingress.
ᐅ curl -sk -H "Host: myapp.com" https://172.16.0.74:30443/app1 | awk '/<title>/'
<title>Welcome to nginx!</title>
ᐅ curl -sk -H "Host: myapp.com" https://172.16.0.73:30443/app2 | awk '/<title>/'
<title>It works! Apache httpd</title>
ᐅ curl -skvI -H "Host: myapp.com" https://172.16.0.73:30443/app2
* Trying 172.16.0.73:30443...
* Connected to 172.16.0.73 (172.16.0.73) port 30443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
* subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
* start date: Mar 18 19:42:01 2026 GMT
* expire date: Mar 18 19:42:01 2027 GMT
* issuer: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
* SSL certificate verify result: self-signed certificate (18), continuing anyway.
* Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://172.16.0.73:30443/app2
* [HTTP/2] [1] [:method: HEAD]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: myapp.com]
* [HTTP/2] [1] [:path: /app2]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> HEAD /app2 HTTP/2
> Host: myapp.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
HTTP/2 200
< date: Thu, 19 Mar 2026 09:55:17 GMT
date: Thu, 19 Mar 2026 09:55:17 GMT
< content-type: text/html
content-type: text/html
< content-length: 191
content-length: 191
< last-modified: Fri, 07 Nov 2025 08:23:08 GMT
last-modified: Fri, 07 Nov 2025 08:23:08 GMT
< etag: "bf-642fce432f300"
etag: "bf-642fce432f300"
< accept-ranges: bytes
accept-ranges: bytes
< strict-transport-security: max-age=31536000; includeSubDomains
strict-transport-security: max-age=31536000; includeSubDomains
<
* Connection #0 to host 172.16.0.73 left intact
ᐅ openssl s_client -showcerts -connect 172.16.0.74:30443 < /dev/null | openssl x509 -noout -dates
Can't use SSL_get_servername
depth=0 O = Acme Co, CN = Kubernetes Ingress Controller Fake Certificate
verify error:num=18:self-signed certificate
verify return:1
depth=0 O = Acme Co, CN = Kubernetes Ingress Controller Fake Certificate
verify return:1
DONE
notBefore=Mar 18 19:42:01 2026 GMT
notAfter=Mar 18 19:42:01 2027 GMT
Tip
Cilium logs
ᐅ cilium hubble port-forward
ℹ️ Hubble Relay is available at 127.0.0.1:4245
ᐅ hubble observe -n default -f
[...]
Mar 19 09:55:22.760: ingress-nginx/ingress-nginx-controller-579c674b5d-jstfh:40576 (ID:5321) -> default/app2-6c66f57cdf-p96vf:80 (ID:17516) to-endpoint FORWARDED (TCP Flags: ACK, FIN)
Troubleshooting Ingress access#
Check Ingress Controller resources
ᐅ k get deploy,po,svc,endpointslice -n ingress-nginx
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/ingress-nginx-controller 2/2 2 2 14h
NAME READY STATUS RESTARTS AGE
pod/ingress-nginx-controller-579c674b5d-8l7zm 1/1 Running 0 14h
pod/ingress-nginx-controller-579c674b5d-jstfh 1/1 Running 0 14h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx-controller NodePort 10.96.211.154 <none> 80:30080/TCP,443:30443/TCP 14h
service/ingress-nginx-controller-admission ClusterIP 10.96.179.233 <none> 443/TCP 14h
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
endpointslice.discovery.k8s.io/ingress-nginx-controller-admission-22nlk IPv4 8443 10.0.1.27,10.0.0.120 14h
endpointslice.discovery.k8s.io/ingress-nginx-controller-jgkth IPv4 443,80 10.0.1.27,10.0.0.120 14h
Check Ingress Controller logs
ᐅ k -n ingress-nginx logs deployments/ingress-nginx-controller --since=2m --follow
Found 2 pods, using pod/ingress-nginx-controller-579c674b5d-8l7zm
10.0.1.169 - - [19/Mar/2026:10:07:01 +0000] "GET /app1 HTTP/2.0" 200 896 "-" "curl/8.5.0" 32 0.004 [default-app1-80] [] 10.0.0.143:80 896 0.004 200 dea0ea772a61b13ce923e52fef01b03b
Check Ingress
ᐅ k describe ingress app-ingress-tls
Name: app-ingress-tls
Labels: <none>
Namespace: default
Address: 10.96.211.154
Ingress Class: nginx
Default backend: <default>
TLS:
myapp-tls terminates myapp.com
Rules:
Host Path Backends
---- ---- --------
myapp.com
/app1 app1:80 (10.0.0.143:80,10.0.1.175:80,10.0.1.190:80)
/app2 app2:80 (10.0.1.32:80,10.0.0.81:80)
Annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 25m (x2 over 26m) nginx-ingress-controller Scheduled for sync
Normal Sync 25m (x2 over 26m) nginx-ingress-controller Scheduled for sync
Check Nginx configuration
ᐅ k exec -n ingress-nginx \
$(k -n ingress-nginx get po -l app.kubernetes.io/instance=ingress-nginx -o name | tail -1) \
-- cat /etc/nginx/nginx.conf
Check myapp.com pods and services
ᐅ k get deploy,po,svc -l app=app1
ᐅ k get deploy,po,svc -l app=app2
ᐅ k describe svc app2
Check the TLS secret used in Ingress
ᐅ k get secret myapp-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -tex
Check key and cert in the TLS secret
ᐅ k get secret myapp-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -modulus -noout | sha256sum
066d09be1a147a31ed96f7308a5e5726fc9681cef0589cdd3459f6c298b06075 -
ᐅ k get secret myapp-tls -o jsonpath='{.data.tls\.key}' | base64 -d | openssl rsa -modulus -noout | sha256sum
066d09be1a147a31ed96f7308a5e5726fc9681cef0589cdd3459f6c298b06075 -
Use curl verbose option
ᐅ curl -skvI -H "Host: myapp.com" https://172.16.0.73:30443/app2
Use openssl s_client command
ᐅ openssl s_client -showcerts -connect 172.16.0.74:30443 --servername myapp.com < /dev/null 2>&1 | openssl x509 -noout -text
Check Nginx clusterrole and clusterrolebinding
ᐅ k get clusterrole,clusterrolebinding | awk '/ingress-nginx/'