𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Theme

Homelab

MetalLB & Ingress Hardening

Hardening MetalLB and Ingress-NGINX: Pinned Versions and NetworkPolicies

Overview

This article hardens MetalLB and ingress-nginx by pinning Helm chart versions and adding NetworkPolicies. Both are infrastructure components that need privileged access, but we can still limit their network blast radius.

Tip

Having trouble? See v1.6.0 for what your setup should look like after completing this article.

Before You Begin

Prerequisites

What We're Hardening

ComponentBeforeAfter
MetalLB Helm0.15.x (floating)Pinned version
ingress-nginx Helm4.x (floating)Pinned version
NetworkPolicyNoneEgress limited per component

Why These Controls

Helm Pinning: Floating versions can introduce breaking changes or vulnerabilities without notice. Pinning ensures reproducible deployments.

NetworkPolicy: MetalLB speakers need to announce IPs via ARP on the local network. Ingress-nginx needs to reach backend services. Both are privileged but can still be network-constrained to limit lateral movement if compromised.

Harden MetalLB

MetalLB speakers need to send ARP announcements on the local network and communicate with the controller.

Check: MetalLB Chart Version

helm list -n metallb-system

Note the chart version (e.g., 0.15.3) - this is what you'll pin.

HelmRelease: Pin MetalLB Chart

k8s/core/metallb/helmrelease.yaml:

# ... existing header ...
spec:
    interval: 1h
    chart:
        spec:
            chart: metallb
            version: "0.15.3" # CHANGE from "0.15.x"
            sourceRef:
                kind: HelmRepository
                name: metallb
                namespace: flux-system
    # ... existing install, upgrade, values ...

Replace 0.15.3 with your current version from the helm list output.

NetworkPolicy: Restrict Speaker Egress

k8s/core/metallb/networkpolicy.yaml:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
    name: metallb
    namespace: metallb-system
spec:
    podSelector: {}
    policyTypes:
        - Egress
    egress:
        # DNS resolution
        - to:
              - namespaceSelector:
                    matchLabels:
                        kubernetes.io/metadata.name: kube-system
          ports:
              - protocol: UDP
                port: 53
        # Kubernetes API (controller needs this)
        - to:
              - ipBlock:
                    cidr: 192.168.10.30/32
              - ipBlock:
                    cidr: 192.168.10.31/32
              - ipBlock:
                    cidr: 192.168.10.32/32
          ports:
              - protocol: TCP
                port: 6443
        # Lab VLAN (L2 ARP announcements)
        - to:
              - ipBlock:
                    cidr: 192.168.10.0/24

Kustomization: Add MetalLB Policy

k8s/core/metallb/kustomization.yaml:

# ... existing header ...
resources:
    - namespace.yaml
    - helmrepository.yaml
    - helmrelease.yaml
    - config.flux.yaml
    - networkpolicy.yaml # ADD

Harden Ingress-NGINX

Ingress-nginx needs to reach backend services in app namespaces and the Kubernetes API for ingress discovery.

Check: Ingress Chart Version

helm list -n ingress-nginx

Note the chart version (e.g., 4.14.3) - this is what you'll pin.

HelmRelease: Pin Ingress Chart

k8s/core/ingress-nginx/helmrelease.yaml:

# ... existing header ...
spec:
    interval: 1h
    chart:
        spec:
            chart: ingress-nginx
            version: "4.14.3" # CHANGE from "4.x"
            sourceRef:
                kind: HelmRepository
                name: ingress-nginx
                namespace: flux-system
    # ... existing install, upgrade, values ...

Replace 4.14.3 with your current version from the helm list output.

NetworkPolicy: Allow Backend Access

k8s/core/ingress-nginx/networkpolicy.yaml:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
    name: ingress-nginx
    namespace: ingress-nginx
spec:
    podSelector: {}
    policyTypes:
        - Egress
    egress:
        # DNS resolution
        - to:
              - namespaceSelector:
                    matchLabels:
                        kubernetes.io/metadata.name: kube-system
          ports:
              - protocol: UDP
                port: 53
        # Kubernetes API (ingress discovery)
        - to:
              - ipBlock:
                    cidr: 192.168.10.30/32
              - ipBlock:
                    cidr: 192.168.10.31/32
              - ipBlock:
                    cidr: 192.168.10.32/32
          ports:
              - protocol: TCP
                port: 6443
        # Backend services (pod network)
        - to:
              - ipBlock:
                    cidr: 10.244.0.0/16

Kustomization: Add Ingress Policy

k8s/core/ingress-nginx/kustomization.yaml:

# ... existing header ...
resources:
    - namespace.yaml
    - helmrepository.yaml
    - helmrelease.yaml
    - networkpolicy.yaml # ADD

Deploy Changes

Git: Commit MetalLB and Ingress

git add k8s/core/metallb/ k8s/core/ingress-nginx/
git commit -m "feat(infra): harden MetalLB and ingress-nginx with pinned versions and NetworkPolicies"
git push

Flux: Sync MetalLB and Ingress

flux reconcile source git flux-system
flux reconcile kustomization sync

Verify: Pinned Helm Versions

kubectl get helmrelease -n metallb-system metallb -o jsonpath='{.spec.chart.spec.version}'
kubectl get helmrelease -n ingress-nginx ingress-nginx -o jsonpath='{.spec.chart.spec.version}'

Expected: Your pinned versions. HelmChart reconciliation can take a minute - if you still see floating versions, wait and check again.

Verify: NetworkPolicies Applied

kubectl get networkpolicy -n metallb-system
kubectl get networkpolicy -n ingress-nginx

Expected: metallb and ingress-nginx policies listed.

Verify: Services Still Work

kubectl get svc -A | grep LoadBalancer

Expected: Services still have external IPs assigned.

Next Steps

With MetalLB and ingress-nginx hardened, continue with Longhorn hardening.

See: Longhorn Hardening

Previous
Tailscale Hardening