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

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**.

---

## 1\. Understanding the Networking Flow

The full flow of traffic will be:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1762606069464/2ba00b7a-396d-4a2e-8322-9f1892803814.png align="center")

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.

## The Architecture

```yaml
                   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
```

### Explanation of the Flow

| 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. |

### Why This Architecture Works Well

1. TLS is terminated **once** at Pangolin, reducing complexity in Kubernetes.
    
2. MetalLB assigns real IPs, so all nodes can expose services consistently.
    
3. Ingress provides routing, path control, and a cleaner security boundary.
    
4. Kubernetes remains internal, while exposure is brokered through Pangolin.
    

---

## 2\. Installing and Configuring MetalLB

MetalLB allows the cluster to assign external IP addresses to LoadBalancer services, even in environments without a native cloud load balancer.

First, install MetalLB:

```yaml
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`:

```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:

```yaml
kubectl apply -f metallb-config.yaml
```

This makes MetalLB assign IPs from this range whenever a LoadBalancer service is created.

---

## 3\. Deploying the NGINX Ingress Controller

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`:

```yaml
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:

```yaml
kubectl get svc -n ingress-nginx
```

Example output:

```yaml
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.

---

## 4\. Creating Ingress Resources

### Example: Exposing Kubenav

```yaml
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:

```yaml
kubectl apply -f kubenav-ingress.yaml
```

### Note:

If ArgoCD is configured with TLS redirect enabled, disable it first:

```yaml
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!

---

## 5\. Configuring Pangolin

Pangolin will expose these services securely from the public internet.

### Steps:

1. Create a new Resource in Pangolin
    
2. Set the domain to match the ingress host, for example:
    
    * `kubenav.nyzex.in` or `argocd.nyzex.in`
        
3. Target backend address should be:
    

```yaml
http://192.168.29.240:80
```

This is the NGINX Ingress Controller LoadBalancer IP and port.

4. Enable TLS termination in Pangolin (Pangolin handles HTTPS at the edge).
    
5. Save and activate.
    

At this point, DNS for the domain must point to Pangolin.

Once that is complete, browsing to:

```yaml
https://kubenav.nyzex.in
```

will load Kubenav.

```yaml
https://argocd.nyzex.in
```

will load ArgoCD.

---

## Conclusion

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.
