Ingress for my local kind cluster

Posted in linux, kubernetes on May 29, 2026 by Adrian Wyssmann ‐ 4 min read

My Kubernetes playground is built primarily around local kind clusters. To make working with them more seamless, I configured custom DNS forwarding so that specific domains—such as *.cluster to automatically resolve to the respective kind cluster. This setup allows me to simulate real-world networking scenarios while keeping everything lightweight and self-contained.

What is kind?

kind is a tool for running local Kubernetes clusters using Docker container “nodes”. kind was primarily designed for testing Kubernetes itself, but may be used for local development or CI.

Configuration

ExtraPortMappings

I ensure to have a dedicate interface e.g. 127.0.0.2 and bind it to hostPort 80 (and 443):

extraPortMappings:
- containerPort: 30080 # Kubernetes NodePort values must be in the range 30000–32767 by default.
  hostPort: 80
  listenAddress: "127.0.0.2" # Use this dedicated IP for .cluster resolution
  protocol: TCP

In addition, we need to map this to a containerPort. This port reflects a nodePort, see below.

DNS Resolution Path

As I use dnscrypt-proxy I have to configure 2 things

  1. Add alias for local interface

    systemd.services."lo-alias" = {
      description = "Add 127.0.0.2 loopback alias for kind cluster";
      wantedBy = [ "network.target" ];
      after = [ "network-pre.target" ];
      serviceConfig = {
        Type = "oneshot";
        RemainAfterExit = true;
        ExecStart = pkgs.writeShellScript "lo-alias-up" ''
          if ! ${pkgs.iproute2}/bin/ip addr show lo | grep -q "127.0.0.2"; then
            ${pkgs.iproute2}/bin/ip addr add 127.0.0.2/8 dev lo
          fi
        '';
        ExecStop = pkgs.writeShellScript "lo-alias-down" ''
          ${pkgs.iproute2}/bin/ip addr del 127.0.0.2/8 dev lo 2>/dev/null || true
        '';
        # Ignore error if address already exists
        ExecStartPre = "${pkgs.bash}/bin/bash -c '${pkgs.iproute2}/bin/ip addr show lo | grep -q 127.0.0.2 && exit 0 || true'";
      };
    };
  2. dnscrypt-proxy’s job is to encrypt your DNS traffic so your ISP cannot see what websites you are visiting. By default, dnscrypt-proxy sends every single request out to an encrypted public resolver on the internet. The forwaringd-rules instructs the proxy to take that specific dns request and “hand it off” to dnsmasq listening on 127.0.0.1:5353.

    environment.etc = {
      # Creates /etc/forwarding-rules.txt
      "forwarding-rules.txt" = {
        text = ''
          calico           127.0.0.1:5353
          cluster          127.0.0.1:5353
          envoy            127.0.0.1:5353
      };
    }
  3. dnsmasq is lightweight dns resolver which does the internal routing. dnscrypt-proxy forwards the requests to dnsmasq. dnsmasq will then ensure requests are forwarde to the kind cluster.

    services.dnsmasq = {
      enable = true;
      settings = {
        port = 5353;
        listen-address = "127.0.0.1";
        bind-interfaces = true;
        no-resolv = true;          # don't touch upstream DNS
        address = [
          "/cluster/127.0.0.2"
          "/calico/127.0.0.2"
          "/envoy/127.0.0.2"
        ];
      };
    };

Thats it.

Testing

For testing I create a new cluster with envoy proxy as ingress. I will not explain the details about the envoy setup here. The same can be achieved with another gateway as well.

  1. Create the kind cluster mapping port host port

    systemd-run --scope --user kind create cluster --name=envoy-cluster --config=envoy-cluster.yaml
    kind export kubeconfig --name envoy-cluster
    kubectl config use-context kind-envoy-cluster
  2. Install envoyproxy and gateway. For that purpose I use my custom helm-chart

    helm install helm-envoy-we 
    envoy-we ./helm-envoy-we  -n envoy-we  --create-namespace
    • installs envoy

    • installs envoy gateway – needed for the ingress

      hostnames:
        - "*.envoy"
        - "*.cluster"
      nodePort: 30080
    • installs backend - a echo server that is listeing

      image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
      hostnames:
        - www.envoy
        - backend.cluster
      port: 3000
  3. Allow shared-gateway-access

    kubectl label namespace envoy-we shared-gateway-access=true

Now everything is running. As the echo server is listening on www.cluster I can run

 curl -v www.cluster
* Host www.cluster:80 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.2
*   Trying 127.0.0.2:80...
* Established connection to www.cluster (127.0.0.2 port 80) from 127.0.0.1 port 37880 
* using HTTP/1.x
> GET / HTTP/1.1
> Host: www.cluster
> User-Agent: curl/8.19.0
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 404 Not Found
< date: Fri, 29 May 2026 13:33:09 GMT
< content-length: 0
< 
* Connection #0 to host www.cluster:80 left intact

Conclusion

By removing the need for manual port-forwarding, the setup provides a cleaner and more realistic way to access services. One limitation is that the setup currently focuses on HTTP only—TLS/HTTPS integration is not yet covered and will be explored in a follow-up blog post.