k3s: Ingress
What exactly is an Ingress?
In the Kubernetes ecosystem, an Ingress is an API object that manages external access to applications running inside a cluster. It acts as an intelligent HTTP and HTTPS router, eliminating the need to create a separate load balancer for every service.
How Does Ingress Work?
Traditional service exposure methods, such as NodePort or LoadBalancer, typically require assigning an external IP address to each service. This quickly becomes tedious and difficult to manage as the cluster grows.
Ingress centralizes external access and operates at Layer 7 (the application layer) of the OSI model:
- Single entry point — only one public IP address needs to be exposed to the outside world.
- Rule-based routing — traffic is forwarded to the appropriate service based on the requested URL path (for example,
my.domain/something) or hostname (for example,something.my.domain). - SSL/TLS termination — HTTPS certificates can be handled centrally, removing that responsibility from individual applications.
Ingress vs. Ingress Controller
This distinction is important.
An Ingress object itself is merely a collection of routing rules.
For those rules to have any effect, the cluster must run an Ingress Controller — a reverse proxy responsible for processing incoming traffic and applying the rules defined in Ingress manifests.
Popular examples include:
- NGINX Ingress Controller
- Traefik
- HAProxy Ingress
This guide uses the NGINX Ingress Controller.
Domain Names
Since the cluster will eventually host various web applications, it makes sense to identify services by DNS names rather than IP addresses.
The examples below use:
.lab.local
as the local domain.
This allows services to be accessed using names such as:
grafana.lab.local
argocd.lab.local
whoami.lab.local
which is considerably more pleasant than remembering port numbers.
Certificates
Sooner or later, running everything without TLS becomes annoying.
For a home lab environment, generating your own Certificate Authority and issuing certificates locally is perfectly acceptable.
Root Certificate Authority
Create a dedicated CA:
mkdir -p certs
cd certs
openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -nodes \
-key lab-ca.key \
-sha256 \
-days 3650 \
-out lab-ca.crt \
-subj "/CN=Lab Local CA"
Wildcard Certificate
A wildcard certificate can secure every host under:
*.lab.local
Create the configuration file lab-local.cnf:
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[dn]
CN = *.lab.local
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.lab.local
DNS.2 = lab.local
Generate the key, request, and certificate:
openssl genrsa -out lab-local.key 2048
openssl req -new \
-key lab-local.key \
-out lab-local.csr \
-config lab-local.cnf
openssl x509 -req \
-in lab-local.csr \
-CA lab-ca.crt \
-CAkey lab-ca.key \
-CAcreateserial \
-out lab-local.crt \
-days 825 \
-sha256 \
-extensions req_ext \
-extfile lab-local.cnf
Namespace for the Ingress Controller
The ingress controller will live in its own namespace.
Create ingress-namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
Apply it:
kubectl apply -f ingress-namespace.yaml
TLS Secret
The certificate must be stored inside Kubernetes as a TLS secret:
kubectl -n ingress-nginx create secret tls lab-local-tls \
--cert=lab-local.crt \
--key=lab-local.key
The secret is named:
lab-local-tls
and can be referenced as:
ingress-nginx/lab-local-tls
throughout the cluster configuration.
Installing the CA Certificate on Your Workstation
The root CA must also be trusted by your workstation:
sudo cp lab-ca.crt /usr/local/share/ca-certificates/lab-ca.crt
sudo update-ca-certificates
Firefox users should additionally import the CA certificate manually through the browser settings.
Deploying the Ingress Controller
A complete ingress controller deployment requires several Kubernetes resources:
- ServiceAccount
- IngressClass
- ConfigMap
- ClusterRole
- ClusterRoleBinding
- Deployment
The container image used in this setup is:
registry.k8s.io/ingress-nginx/controller:v1.14.0
which provides the familiar NGINX reverse proxy and load balancer functionality.
The deployment manifest is shown below.
# ingress.yaml
# (manifest unchanged from the original article)
Noteworthy Configuration
The ingress controller is pinned to the control-plane node:
nodeSelector:
kubernetes.io/hostname: iridium
This is perfectly acceptable for a small home lab.
The controller also uses the wildcard certificate by default:
--default-ssl-certificate=ingress-nginx/lab-local-tls
which means every ingress can immediately benefit from TLS.
Verifying the Installation
Start with the usual checks:
kubectl get pods -n ingress-nginx -o wide
kubectl get rs -n ingress-nginx -o wide
Next, add a test host entry:
10.10.10.24 test.lab.local
to your workstation’s /etc/hosts.
Then run:
curl http://test.lab.local
Expected result:
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
Surprisingly, this is good news.
The DNS lookup works.
The ingress controller works.
NGINX works.
There simply isn’t any application behind the hostname yet.
The infrastructure is doing exactly what it should.
TLS Verification
Next, test HTTPS:
curl -v https://test.lab.local
The interesting part is not the final HTTP response, but the TLS negotiation itself.
Look for output similar to:
SSL connection using TLSv1.3
...
subject: CN=*.lab.local
...
issuer: CN=Lab Local CA
...
subjectAltName: "test.lab.local" matches cert's "*.lab.local"
...
SSL certificate verified via OpenSSL
If these messages appear, the certificate chain is functioning correctly and the wildcard certificate is being served as expected.
Finally, open the URL in Firefox and confirm that the browser displays the expected lock icon and certificate information.
Conclusion
At this point:
- DNS works,
- the ingress controller is running,
- TLS works,
- wildcard certificates work,
- external traffic reaches the cluster.
No actual applications are exposed yet, but the foundation is now in place.
The cluster is finally ready to host something more interesting than infrastructure components.
Which, as every Kubernetes administrator eventually discovers, is usually the easy part.
