𝔩𝔢𝔩𝕠𝔭𝔢𝔷
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