Kubernetes Gateway API Explained with kubenix
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:
GatewayClassselects the controller that implements a class of gateways.Gatewaycreates an instance of that infrastructure and defines its listeners.HTTPRouteattaches 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: