From nginx to traefik (and solving X-Forwarded-For)

I recently switched my home setup from nginx to traefik. I had to get used to the new configuration styles and weird doc style of traefik, but suffice to say, I am happy at the end results.

Traefik ships with SNI, allowing me to snoop the connection and prevent unauthorized access to my file server. (One must have the correct hostname to see my shared photos.) My first action was to replace my edge nginx reverse proxy with traefik. If this was a success, then I would move onto replacing nginx in my k8s cluster. I was surprized that after zero OS tuning changes, traefik was able to serve pages faster for my k8s cluster. I initially thought that traefik would perform marginally slower than nginx due to additional HostSNI checks, but that was totally disproved by the first page load.

After switching to traefik inside my k8s cluster, performance improved even more. However, a new problem emerged: I was getting the IPs of my internal traffic logged instead of the real origin IP. While I would not normally care, malicious bots were attempting to brute force my WordPress installations, and I need to block their IPs.

I was able to get the X-Forwarded-For header populated with the true ip by enabling the Proxy Protocol on my edge traefik, setting externalTrafficPolicy: Local on my traefik k8s service, and finally telling traefik to accept the proxyProtocol information from the edge server.

# Edge Router providers.yaml
...
tcp:
  routers:
    to-web:
      service: web
      rule: HostSNIRegexp(`www.andrewwippler.com`, `andrewwippler.com`, ...)
      entryPoints:
        - web
    to-websecure:
      service: websecure
      rule: HostSNIRegexp(`www.andrewwippler.com`, `andrewwippler.com`, ...)
      entryPoints:
        - websecure
      tls: 
        passthrough: true
services:
    web:
      loadBalancer:
        proxyProtocol:
          version: 2
        servers:
        - ...
    websecure:
      loadBalancer:
        proxyProtocol:
          version: 2
        servers:
        - ...
...
# traefik k8s deployment and service
# missing service account definition and CRDs
apiVersion: apps/v1
kind: Deployment
metadata:
  name: traefik
spec:
  replicas: 3 #<-- one for each of my nodes, for failover
  strategy: #<-- forces k8s to update pods on configuration change
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  selector:
    matchLabels:
      app: traefik
  template:
    metadata:
      labels:
        app: traefik
    spec:
      affinity: #<-- one replica per node
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - traefik
            topologyKey: "kubernetes.io/hostname"
      serviceAccountName: traefik-ingress-controller
      containers:
        - name: traefik
          image: traefik:v2.10.3
          args:
            - --entrypoints.web.address=:80
            - --entrypoints.websecure.address=:443
            - --entrypoints.websecure.http.tls
            - --entrypoints.websecure.http3
            - --experimental.http3=true
            - --providers.kubernetesingress
            - --providers.kubernetescrd
            - --log.level=DEBUG
            - --entrypoints.web.http.redirections.entrypoint.scheme=https
            - --entrypoints.web.http.redirections.entrypoint.to=websecure
            # - --accesslog
            - --entryPoints.web.proxyProtocol.trustedIPs=127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #<-- local CIDRs
            - --entryPoints.websecure.proxyProtocol.trustedIPs=127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #<-- local CIDRs
          ports:
            - name: web
              containerPort: 80
            - name: websecure
              containerPort: 443
---
apiVersion: v1
kind: Service
metadata:
  name: traefik
spec:
  type: NodePort
  selector:
    app: traefik
  ports:
    - protocol: TCP
      port: 80
      name: web
      targetPort: 80
    - protocol: TCP
      port: 443
      name: websecure
      targetPort: 443
  externalTrafficPolicy: Local # <--- changed