Why Gateway API?

Kubernetes Ingress provides a small, portable API for exposing HTTP services. More advanced behavior often depends on controller-specific annotations or custom resources, which makes configurations harder to understand and move between implementations.

Gateway API is a family of Kubernetes APIs for L4 and L7 routing. It is role-oriented, protocol-aware, and expressive enough to describe common routing behavior without hiding it in annotations.

This website repository uses Gateway API together with kubenix. The Kubernetes resources are written as Nix attribute sets and rendered into manifests by the flake.

The Gateway API Resource Model

For HTTP traffic, three resources form the core model:

  • GatewayClass selects the controller that implements a class of gateways.
  • Gateway creates an instance of that infrastructure and defines its listeners.
  • HTTPRoute attaches routing rules to a listener and forwards matching requests to backends.

This repository owns the application-level resources. The cluster already provides a Traefik-managed Gateway named default-http-gateway, so the website only needs to define its routes instead of its own GatewayClass and Gateway.

The relationship looks like this:

Declaring HTTPRoute in kubenix

kubenix knows the built-in Kubernetes resource types, but Gateway API resources are installed as custom resource definitions (CRDs). The flake therefore registers HTTPRoute as a custom type:

kubernetes = {
  customTypes = [
    {
      group = "gateway.networking.k8s.io";
      version = "v1";
      kind = "HTTPRoute";
      attrName = "httpRoutes";
    }
  ];

  resources = {
    # ...
  };
};

The attrName makes routes available below kubernetes.resources.httpRoutes. kubenix can then render them alongside the other resources in the module.

Attaching a Route to a Gateway

An HTTPRoute requests attachment to a Gateway through parentRefs. This website selects the https listener of a Gateway in another namespace:

parentRefs = [
  {
    name = "default-http-gateway";
    namespace = "traefik";
    sectionName = "https";
  }
];

sectionName targets one specific listener instead of attaching the route to every compatible listener. The Gateway must also permit routes from the application’s namespace. This two-sided attachment model allows cluster operators to own shared entry points while application teams own their routes.

Redirecting Alternate Hostnames

The first route handles alternate domains:

"${name}-redirects" = {
  metadata = {
    inherit namespace;
  };
  spec = {
    parentRefs = [
      {
        name = "default-http-gateway";
        namespace = "traefik";
        sectionName = "https";
      }
    ];
    hostnames = [
      "www.bashlover.de"
      # ...
    ];
    rules = [
      {
        matches = [
          {
            path = {
              type = "PathPrefix";
              value = "/";
            };
          }
        ];
        filters = [
          {
            type = "RequestRedirect";
            requestRedirect = {
              scheme = "https";
              hostname = "bashlover.de";
              statusCode = 301;
            };
          }
        ];
      }
    ];
  };
};

The route matches every path for the listed hostnames. Its RequestRedirect filter returns a permanent redirect to the canonical HTTPS hostname. It does not need a backend because the filter produces the response.

Matching and Forwarding Requests

The main route accepts only GET and HEAD requests for bashlover.de:

hostnames = [ "bashlover.de" ];

rules = [
  {
    matches = [
      {
        inherit path;
        method = "GET";
      }
      {
        inherit path;
        method = "HEAD";
      }
    ];

    backendRefs = [
      {
        inherit name;
        port = listenerPort;
      }
    ];
  }
];

Each item in matches is an alternative, so either HTTP method matches. The shared path is defined as a PathPrefix for /, which covers the whole website.

backendRefs sends matching requests to the bashlover-web Service on port 8080. The Service then selects the application’s Pods:

Adding Response Headers

Gateway API filters can modify traffic as part of a route. The main route uses the portable ResponseHeaderModifier filter to set security headers:

filters = [
  {
    type = "ResponseHeaderModifier";
    responseHeaderModifier = {
      set = [
        {
          name = "X-Frame-Options";
          value = "DENY";
        }
        {
          name = "X-Content-Type-Options";
          value = "nosniff";
        }
        {
          name = "Strict-Transport-Security";
          value = "max-age=31536000; includeSubDomains; preload";
        }
        {
          name = "Content-Security-Policy";
          value = builtins.concatStringsSep "; " [
            "default-src 'self'"
            "object-src 'none'"
            "frame-ancestors 'none'"
            "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net"
            "upgrade-insecure-requests"
          ];
        }
      ];
    };
  }
];

Previously, this behavior required a Traefik Middleware resource and an ExtensionRef. Using a standard Gateway API filter keeps the application route independent of that controller-specific CRD, provided that the selected Gateway controller supports the filter.

Building the Manifests

The flake evaluates the kubenix module and exposes the result as a package:

src =
  (inputs.kubenix.evalModules.${system} {
    module = { kubenix, ... }: {
      imports = [ kubenix.modules.k8s ];
      kubernetes = {
        # custom types and resources
      };
    };
  }).config.kubernetes.result;

Build the Kubernetes manifests with:

nix build .#k8s

Nix pins kubenix and the other inputs in flake.lock, while kubenix turns the Nix module into Kubernetes manifests. The result is a reproducible deployment definition containing the Namespace, ServiceAccount, Service, Deployment, and both HTTPRoute resources.

Operational Checks

Creating an HTTPRoute does not guarantee that a controller accepted it. Check the route status after deployment:

kubectl get httproute -n bashlover-web
kubectl describe httproute bashlover-web -n bashlover-web

The route’s parent status should report an Accepted=True condition. A ResolvedRefs=True condition confirms that references such as the backend Service could be resolved.

Final Thoughts

Gateway API makes the traffic path explicit: routes attach to named listeners, match requests, apply standard filters, and select backends. In this repository, kubenix adds declarative composition, and Nix makes the generated deployment reproducible.

The result is a route definition that is easier to review than annotation-heavy Ingress resources and less coupled to a specific Gateway controller.

Learn more: