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.
Having trouble? See v1.6.0 for what your setup should look like after completing this article.
Before You Begin
Prerequisites
- Tailscale Hardening completed
What We're Hardening
| Component | Before | After |
|---|---|---|
| MetalLB Helm | 0.15.x (floating) | Pinned version |
| ingress-nginx Helm | 4.x (floating) | Pinned version |
| NetworkPolicy | None | Egress 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