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