Restricting IP Access to a K3s Cluster with Traefik

Posted Wed 23 April 2025   | Category : Tech Tutorial

Motivation

In my homelab, I’ve set up a K3s cluster using the default Traefik ingress controller. Some services I run shouldn't be publicly accessible. This article explains how to configure a K3s cluster to restrict access to certain services based on the client’s IP address.

Requirements

The Problem

By default, Traefik does not preserve the client’s original IP address. Instead, requests appear to come from the cluster IP of the ingress proxy (e.g., 10.42.0.1). This makes it impossible to distinguish between internal and external clients by IP.

The whoami app helps us diagnose this. It echoes headers like X-Real-IP. In this image, the X-Real-IP is reported as 10.42.0.1, Traefik’s internal IP—not the real client’s IP. Image showing X-Real-IP listed as 10.42.0.1

Step 1: Preserve the Client’s IP Address

To preserve the source IP, we need to update the externalTrafficPolicy setting on the Traefik service. This is a standard Kubernetes Service setting that, when set to Local, preserves the source IP. See the Kubernetes documentation for details.

Apply the setting with:

kubectl patch svc -n kube-system \
  -p '{"spec":{"externalTrafficPolicy":"Local"}}' traefik

Once applied, X-Real-IP will show your actual LAN or WAN IP depending on where you're connecting from.

LAN Client Example:

WAN Client Example:

Now that we can differentiate clients by IP, we can block unwanted access.

Optional: Using a YAML Patch for Persistent Configuration

For easier reproducibility, create a patch file called external-traffic-policy-local-patch.yaml:

spec:
  externalTrafficPolicy: Local

Then apply it with:

kubectl patch svc traefik \
  -n kube-system \
  --patch-file external-traffic-policy-local-patch.yaml

Same effect—just more repeatable than using CLI commands.

Step 2: Create Middleware to Restrict IP Access

Now we’ll use a Traefik middleware to allow only local clients.

Create the middleware definition in a YAML file, e.g., traefik-local-ipwhitelist.yml:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: local-ip-allowlist
spec:
  ipWhiteList:
    sourceRange:
      - 192.168.1.0/24

Apply it with:

kubectl apply -f traefik-local-ipwhitelist.yml

Verify with:

kubectl describe middleware local-ip-allowlist

You should see the middleware listed and its rules.

Step 3: Add Middleware to Your Ingress

We have created a middleware to whitelist local IP ranges, now we must update our Ingresses to use that middleware. We do this by adding an annotation to the Ingress resourcee. Let’s say you have a service like Transmission with this ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: transmission-tls-ingress
  annotations:
    spec.ingressClassName: traefik
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  rules:
    - host: transmission.[REDACTED].com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: transmission-service
                port:
                  number: 9091
  tls:
    - secretName: transmission-tls
      hosts:
        - transmission.[REDACTED].com

We configure this ingress by adding the "traefik.ingress.kubernetes.io/router.middlewares" annotation and specifying the "local-ip-allowlist" middleware we just created:

  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: >
      default-local-ip-allowlist@kubernetescrd

If you're using multiple middlewares, separate them with commas:

  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: >
      default-redirect-https@kubernetescrd,
      default-local-ip-allowlist@kubernetescrd

⚠️ Only the last router.middlewares key is used if defined multiple times. Combine all middlewares into one annotation.

Example Ingress With Middleware

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: transmission-tls-ingress
  annotations:
    spec.ingressClassName: traefik
    cert-manager.io/cluster-issuer: letsencrypt-prod
    traefik.ingress.kubernetes.io/router.middlewares: >
      default-redirect-https@kubernetescrd,
      default-local-ip-allowlist@kubernetescrd
spec:
  rules:
    - host: transmission.[REDACTED].com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: transmission-service
                port:
                  number: 9091
  tls:
    - secretName: transmission-tls
      hosts:
        - transmission.[REDACTED].com

Now just apply the ingress configuration and we are ready to test.

kubectl apply -f ingress.yml

Results: Access is Now Restricted by IP

When I access my Transmission instance from a local IP, it loads as expected: When I access it from an external IP, I get denied:

Final Thoughts

Restricting services to local IPs adds an easy layer of security to your homelab or dev environment. This method is simple to implement, repeatable, and doesn’t require additional tools or services.

Resources