Exposing Kubernetes Services Over the Internet Using MetalLB, NGINX Ingress, and Pangolin

DevOps & Cloud Engineer — building scalable, automated, and intelligent systems. Developer of sorts | Automator | Innovator
Search for a command to run...

DevOps & Cloud Engineer — building scalable, automated, and intelligent systems. Developer of sorts | Automator | Innovator
Artificial Intelligence has become one of the biggest talking points in software engineering. Every week, a new tool promises to automate deployments, fix production incidents, or replace developers a

You spin up a Kubernetes workload. You connect to PostgreSQL. You add sslmode=require because security matters. And then PostgreSQL replies with: server does not support SSL, but SSL was required Wai

When you first approach Tencent Cloud, it feels familiar if you come from AWS or Azure. There are VPCs, Kubernetes clusters, load balancers, object storage, IAM, everything you would expect. But once

When I first automated Docker builds and deployments, I thought the hard part would be writing the YAML. It was not. The real challenges were versioning, preventing accidental rollbacks, handling envi

For a long time, Docker Compose felt like the perfect solution. Simple YAML, fast local setup, predictable behavior. For a single service or even a small stack, it works beautifully. But at some point, reality catches up. As the application grew, tra...

CodeOps Studies
40 posts
Simple write-ups on day to day code or devops experiments, tests etc.
This guide focuses on exposing applications running inside a Kubernetes cluster to the public internet in a controlled and secure way. The components used are MetalLB, the NGINX Ingress Controller, and Pangolin. The cluster itself is already provisioned and functioning, so we begin from the network exposure layer.
We will walk through the following steps:
Installing and configuring MetalLB to provide LoadBalancer IPs on bare metal
Deploying the NGINX Ingress Controller
Creating ingress resources for applications
Routing public traffic using Pangolin
The domain used in this configuration is nyzex.in, and the tunnel endpoint is mytunnel.nyzex.in.
The full flow of traffic will be:

MetalLB provides the LoadBalancer IP on the private network. The NGINX Ingress Controller watches for Ingress objects and routes traffic accordingly. Pangolin exposes the ingress to the public internet securely.
Public Internet
│
│ (DNS: A record → 150.x.x.x)
│
nyzex.in / *.nyzex.in
│
▼
Pangolin (Cloud VM)
TLS Termination Enabled
│
│ (HTTPS Proxy)
│
┌──────────┴──────────┐
│ │
│ │
proxmox.nyzex.in *.nyzex.in app endpoints
(Proxmox Web UI 8006) │
│
▼
MetalLB LoadBalancer IP Range
192.168.29.240-192.168.29.250
│
│
▼
NGINX Ingress Controller (DaemonSet)
Running on Kubernetes Worker Nodes
│
│ (Ingress Rules Route Traffic)
│
┌──────────────────────────────┴───────────────────────────────┐
│ │
▼ ▼
kubenav Service (ClusterIP) argocd-server Service (ClusterIP)
Namespace: kubenav Namespace: argocd
Exposed via Ingress Exposed via Ingress
HTTPS handled by Pangolin HTTPS handled by Pangolin
| Layer | Role |
| DNS (Cloudflare) | Routes external requests to Pangolin. |
| Pangolin | Terminates TLS and forwards HTTP traffic to the MetalLB public IP. |
| MetalLB | Assigns a local LAN IP to the NGINX ingress controller. |
| NGINX Ingress Controller | Matches incoming requests based on domain and path. |
| Ingress Resources | Define which service responds to which domain. |
| ClusterIP Services | Internal-only services that receive traffic forwarded from NGINX. |
TLS is terminated once at Pangolin, reducing complexity in Kubernetes.
MetalLB assigns real IPs, so all nodes can expose services consistently.
Ingress provides routing, path control, and a cleaner security boundary.
Kubernetes remains internal, while exposure is brokered through Pangolin.
MetalLB allows the cluster to assign external IP addresses to LoadBalancer services, even in environments without a native cloud load balancer.
First, install MetalLB:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
After MetalLB is running, define the IP address pool. This range should be within your LAN network and unused by DHCP.
Create a file called metallb-config.yaml:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 192.168.29.240-192.168.29.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-advertisement
namespace: metallb-system
Apply it:
kubectl apply -f metallb-config.yaml
This makes MetalLB assign IPs from this range whenever a LoadBalancer service is created.
When installing the NGINX Ingress Controller using Helm, ensure that the service type is configured correctly to LoadBalancer at installation time. To allow external access via the MetalLB assigned address, the service type must be changed to LoadBalancer:
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.hostNetwork=true \
--set controller.kind=DaemonSet \
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set controller.hostPort.enabled=true \
--set controller.hostPort.ports.http=80 \
--set controller.hostPort.ports.https=443 \
--set controller.service.type=LoadBalancer
This will allow MetalLB to assign an IP from the configured address pool, making the ingress reachable from outside the cluster.
Verify the assigned IP:
kubectl get svc -n ingress-nginx
Example output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
ingress-nginx-controller LoadBalancer 10.101.220.246 192.168.29.240 80:30080/TCP,443:30443/TCP
The EXTERNAL-IP is from MetalLB. This is what Pangolin will connect to.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kubenav
namespace: kubenav
spec:
ingressClassName: nginx
rules:
- host: kubenav.nyzex.in
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kubenav
port:
number: 14122
Apply:
kubectl apply -f kubenav-ingress.yaml
If ArgoCD is configured with TLS redirect enabled, disable it first:
kubectl patch configmap argocd-cmd-params-cm -n argocd --type merge -p '{"data": {"server.insecure": "true"}}'
kubectl rollout restart deployment argocd-server -n argocd
Similar to kubenav we create the argoCD ingress too!
Pangolin will expose these services securely from the public internet.
Create a new Resource in Pangolin
Set the domain to match the ingress host, for example:
kubenav.nyzex.in or argocd.nyzex.inTarget backend address should be:
http://192.168.29.240:80
This is the NGINX Ingress Controller LoadBalancer IP and port.
Enable TLS termination in Pangolin (Pangolin handles HTTPS at the edge).
Save and activate.
At this point, DNS for the domain must point to Pangolin.
Once that is complete, browsing to:
https://kubenav.nyzex.in
will load Kubenav.
https://argocd.nyzex.in
will load ArgoCD.
With MetalLB, we are able to assign IP addresses to LoadBalancer services on bare metal. With NGINX Ingress Controller, routing is handled cleanly inside the cluster. Pangolin exposes the traffic securely to the public internet.
This architecture balances internal simplicity with external security and scale flexibility.