External access to Kubernetes services is handled by three components working together:
| Setting | Value |
|---|---|
| Chart | metallb v0.15.2 |
| Namespace | metallb-system |
| Sync Wave | 1 |
| Mode | Layer 2 |
# IPAddressPool
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
spec:
addresses:
- 192.168.88.10-192.168.88.30
autoAssign: true
# L2Advertisement
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default-l2adv
MetalLB assigns IPs from the pool 192.168.88.10-192.168.88.30 to Kubernetes LoadBalancer services. The primary consumer is Traefik at 192.168.88.12.
| Setting | Value |
|---|---|
| Chart | traefik v34.4.1 |
| Namespace | traefik |
| Sync Wave | 2 |
| External IP | 192.168.88.12 (MetalLB) |
| Traffic Policy | externalTrafficPolicy: Local — preserves real client IPs for CrowdSec |
| TLS Min Version | TLS 1.3 |
| Resources | Requests: 100m / 128Mi, Limits: 500m / 512Mi |
| Security | runAsNonRoot, runAsUser 65532 |
| Port | External | Internal | Purpose |
|---|---|---|---|
| web | 80 | 8000 | HTTP (redirects to HTTPS) |
| websecure | 443 | 8443 | HTTPS (TLS enabled) |
| traefik | — | 9000 | Dashboard (not exposed externally) |
192.168.88.0/24, 10.244.0.0/16)The Traefik dashboard is available at https://traefik.homelab.vyanh.uk but restricted to local network IPs only via the local-only middleware.
| Setting | Value |
|---|---|
| Chart | cert-manager v1.18.2 |
| Namespace | cert-manager |
| Sync Wave | 0 (first to deploy) |
| Issuer | Challenge | Provider | Use Case |
|---|---|---|---|
letsencrypt-dns01 |
DNS-01 | Cloudflare API | Primary — supports wildcards |
letsencrypt-prod |
HTTP-01 | Traefik ingress | Backup — simpler but no wildcards |
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-dns01"
spec:
ingressClassName: traefik
rules:
- host: myapp.homelab.vyanh.uk
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80
tls:
- secretName: myapp-tls
hosts:
- myapp.homelab.vyanh.uk
All HTTPS traffic on the websecure entrypoint passes through the CrowdSec bouncer middleware before reaching any backend service. This is enforced globally via:
--entryPoints.websecure.http.middlewares=traefik-crowdsec-bouncer@kubernetescrd
| Setting | Value |
|---|---|
| Mode | stream — syncs ban list every 30 s |
| Default ban duration | 4 h |
| HTTP timeout | 10 s |
| AppSec | Enabled (fail-open) |
| AppSec host | crowdsec-appsec-service.crowdsec.svc.cluster.local:7422 |
LAN bypass (clientTrustedIPs) |
192.168.88.0/24, 192.168.20.0/24, 192.168.100.0/24 |
| Trusted forwarder IPs | 10.244.0.0/16, 192.168.88.0/24 |
| API key source | /etc/crowdsec/api_key (file mount from crowdsec-bouncer-key secret via VSO) |
The bouncer plugin is fail-closed at startup: if the CrowdSec LAPI is unreachable when Traefik starts, it returns 403 for ALL traffic. Always ensure CrowdSec is healthy before restarting Traefik:
kubectl get pods -n crowdsec # all Running
kubectl get secret -n traefik crowdsec-bouncer-key # exists
kubectl exec -n crowdsec deploy/crowdsec-lapi -- cscli bouncers list
externalTrafficPolicy: Local is Required for CrowdSecTraefik's LoadBalancer service must use externalTrafficPolicy: Local. With the default Cluster policy, kube-proxy SNATs all external traffic to an internal pod-network IP (10.244.x.x) before it reaches Traefik. CrowdSec's whitelist-good-actors collection whitelists 10.0.0.0/8, so every Traefik log line gets silently dropped — HTTP detection never fires.
With Local, MetalLB L2 announces the VIP only from the node running Traefik. Traffic arrives without SNAT and Traefik logs the real client IP.
See Security (CrowdSec IDS/IPS) for full documentation.
| Setting | Value |
|---|---|
| Sync Wave | 13 |
| Namespace | nas-ingress |
| Type | Raw manifests (Service + Endpoints + Ingress) |
The nas-ingress component creates Kubernetes resources that proxy traffic through Traefik to Docker apps running on the Synology NAS (192.168.88.19). Each NAS app gets:
| Middleware | Applied To | Effect |
|---|---|---|
default-security-headers |
All NAS apps | HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy |
local-only |
Portainer only | IP whitelist: 192.168.88.0/24, 10.244.0.0/16 |
| App | NAS Port | Local URL | Public URL |
|---|---|---|---|
| Immich | 2283 | immich.homelab.vyanh.uk |
— |
| Vaultwarden | 8843 | vaultwarden.homelab.vyanh.uk |
vault.vyanh.uk |
| Uptime Kuma | 3001 | uptime.homelab.vyanh.uk |
status.vyanh.uk |
| ntfy | 2586 | ntfy.homelab.vyanh.uk |
ntfy.vyanh.uk |
| Portainer | 9443 (HTTPS) | portainer.homelab.vyanh.uk (local-only) |
— |
| Homepage | 3000 | homepage.homelab.vyanh.uk |
— |
| Paperless-ngx | 8010 | paperless.homelab.vyanh.uk |
— |
| Wiki.js | 3080 | wikijs.homelab.vyanh.uk |
wiki.vyanh.uk |
Portainer uses a Traefik IngressRoute (CRD) instead of standard Ingress because it requires HTTPS backend (skip-verify ServersTransport for self-signed cert).
Services with a Public URL have a second TLS entry in the same Ingress resource (e.g. secretName: vault-public-tls) so cert-manager issues a separate certificate for the public hostname via the letsencrypt-dns01 ClusterIssuer. Split DNS in Technitium ensures LAN clients hit Traefik directly rather than going through Cloudflare. See Network Topology → Split DNS.