NGINX Ingress on Rocky Linux 10 Kubernetes (kubeadm)

nginx ingress controller rocky linux, kubernetes ingress rocky linux 10

If you followed our guide to setting up a Kubernetes cluster on Rocky Linux 10 with kubeadm, you now have a working control plane and worker nodes — but no way to route external traffic into your Pods. That’s where an Ingress Controller comes in.

This guide walks through deploying the NGINX Ingress Controller on that same Rocky Linux 10 cluster, covering the firewalld rules, SELinux considerations, and bare-metal external IP problem that trip up most first attempts.

Table of Contents

  1. Why You Need an Ingress Controller
  2. Prerequisites
  3. Ingress Options Compared
  4. Architecture Overview
  5. Step 1: Open Required firewalld Ports
  6. Step 2: Install ingress-nginx via Helm
  7. Step 3: Solve the Bare-Metal External IP Problem
  8. Step 4: Deploy a Sample App and Ingress Resource
  9. Step 5: Add TLS with cert-manager (Optional)
  10. Troubleshooting Table
  11. FAQ
  12. Conclusion

Why You Need an Ingress Controller

Without an Ingress, exposing a service outside the cluster means either a NodePort (ugly high-numbered ports) or a cloud LoadBalancer (which doesn’t exist on bare metal). An Ingress Controller gives you:

  • Name-based virtual hosting (multiple domains/services on ports 80/443)
  • Centralized TLS termination
  • Path-based routing (/api, /app, etc.) to different backend Services

Prerequisites

  • A working kubeadm cluster on Rocky Linux 10 (control plane + at least one worker), per the previous guide
  • kubectl configured against the cluster
  • helm v3 installed on your admin machine
  • firewalld active on all nodes (default on Rocky Linux 10)

Ingress Options Compared

ControllerMaturityConfig StyleBest Fit
ingress-nginxVery mature, most widely deployedAnnotations on Ingress resourcesGeneral-purpose, largest community, this guide
TraefikMatureCRDs (IngressRoute)Teams wanting built-in dashboard and dynamic config reload
HAProxy IngressMatureAnnotations + ConfigMapHigh-throughput environments already standardized on HAProxy
Kong IngressMatureCRDs + pluginsAPI-gateway features (rate limiting, auth) built in

ingress-nginx remains the default choice for most bare-metal clusters because of its documentation depth and predictable annotation-based configuration — which is why it’s the one used here.

Architecture Overview

                 Internet / LAN


┌─────────────────────────────┐
│ Node (firewalld: 80/443) │
│ ┌─────────────────────────┐ │
│ │ ingress-nginx-controller│ │ (DaemonSet / hostNetwork
│ │ (Pod) │ │ or NodePort)
│ └───────────┬─────────────┘ │
└──────────────┼───────────────┘
│ routes by Host/Path

┌─────────────────────────────┐
│ Ingress resource (rules) │
└─────────────┬───────────────┘

┌─────────────────────────────┐
│ ClusterIP Service(s) │
└─────────────┬───────────────┘

┌─────────────────────────────┐
│ Application Pods │
└─────────────────────────────┘

Step 1: Open Required firewalld Ports

Run on every node that will run an ingress-nginx Pod (with hostNetwork: true, that’s effectively every worker):

sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --permanent --add-port=8443/tcp   # admission webhook
sudo firewall-cmd --reload

This follows the same explicit-port-table approach used in the cluster setup guide — no blanket firewalld disable.

Step 2: Install ingress-nginx via Helm

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.hostNetwork=true \
  --set controller.kind=DaemonSet \
  --set controller.service.type=ClusterIP

hostNetwork=true + DaemonSet binds the controller directly to ports 80/443 on each node — the simplest working pattern on bare metal, avoiding the external-IP problem for the controller itself (though individual Services behind it still resolve internally).

Verify:

kubectl get pods -n ingress-nginx
kubectl get ds -n ingress-nginx

Step 3: Solve the Bare-Metal External IP Problem

If you instead installed with controller.service.type=LoadBalancer (the Helm chart default), you’ll see:

NAME                                 TYPE           EXTERNAL-IP   PORT(S)
ingress-nginx-controller             LoadBalancer   <pending>     80:xxxxx/TCP,443:xxxxx/TCP

That <pending> state is expected — there’s no cloud controller on bare metal to assign an IP. Two ways to resolve it:

Option A — MetalLB (recommended for production-like setups):

helm install metallb metallb/metallb -n metallb-system --create-namespace

Then define an IPAddressPool covering a free range on your LAN so LoadBalancer Services get a real, reachable IP.

Option B — hostNetwork/DaemonSet (used in Step 2): simplest for homelab/single-cluster setups — traffic reaches the controller directly via each node’s IP on ports 80/443, no external IP needed at all.

Step 4: Deploy a Sample App and Ingress Resource

kubectl create deployment demo-app --image=nginxdemos/hello --port=80
kubectl expose deployment demo-app --port=80 --type=ClusterIP
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: demo.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: demo-app
                port:
                  number: 80

Apply it, then add demo.local to your admin machine’s /etc/hosts pointing at a worker node’s IP to test:

kubectl apply -f demo-ingress.yaml
curl -H "Host: demo.local" http://<worker-node-ip>

Step 5: Add TLS with cert-manager (Optional)

For a homelab with no public DNS, a self-signed ClusterIssuer is the pragmatic option:

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set installCRDs=true
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}

For internet-facing production domains, switch to a letsencrypt-prod ClusterIssuer with an HTTP-01 or DNS-01 solver instead.

Troubleshooting Table

SymptomLikely CauseFix
EXTERNAL-IP stuck at <pending>No cloud LB on bare metalInstall MetalLB, or switch to hostNetwork/DaemonSet (Step 3)
502 Bad Gateway from ingressBackend Service selector doesn’t match Pod labelskubectl describe svc demo-app and confirm selector/labels align
Ingress rule ignored, default backend served insteadMissing or wrong ingressClassNameSet ingressClassName: nginx explicitly in the Ingress spec
curl: (7) Failed to connect to node IP on port 80firewalld port not open on that nodeRe-run the firewall-cmd --add-port commands from Step 1 on that specific node
Admission webhook timeout during helm installPort 8443 blocked by firewalldOpen 8443 per Step 1; webhook Pod needs it to validate Ingress objects
SELinux denial in /var/log/audit/audit.log for controller PodMissing container-selinux policy contextsudo ausearch -m avc -ts recent | audit2allow -M ingress-fix && semodule -i ingress-fix.pp — same pattern used for Docker in the earlier guide

FAQ

Why does my Ingress external IP stay <pending> on Rocky Linux?
On bare-metal kubeadm clusters there’s no cloud load balancer to assign an IP automatically. Install MetalLB for a real external IP, or use hostNetwork/NodePort mode and reach the controller via a node’s own IP instead.

Does SELinux need to be disabled to run ingress-nginx on Rocky Linux?
No — keep it enforcing and resolve specific denials with audit2allow, or make sure container-selinux and the correct volume :z/:Z flags are in place, the same approach used for Docker on Rocky Linux.

Can I use firewalld and ingress-nginx together?
Yes. Open the specific ports the controller needs (80, 443, and the 8443 webhook port) on each node rather than disabling firewalld entirely.

Conclusion

With the NGINX Ingress Controller running, your Rocky Linux 10 kubeadm cluster can now route real HTTP(S) traffic into workloads instead of relying on raw NodePorts. From here, the natural next step is layering in cert-manager for automated Let’s Encrypt certificates on a public domain, or adding MetalLB for a fully self-contained bare-metal load-balancing setup.

Related reading: How to Install Docker on Rocky Linux 10 with SELinux · How to Set Up a Kubernetes Cluster on Rocky Linux 10 with kubeadm · Managing Kubernetes Applications with Helm Charts

(Visited 1 times, 1 visits today)

You may also like