🏷️ Properly set up Ingress objects with TLS

🏷️ 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/'