Kubernetes provider
The Kubernetes provider discovers entrypoints from two sources — Service annotations and standard Ingress resources — and routes traffic directly to ready pod IPs. Sōzune subscribes to the Kubernetes API for near-real-time updates: scaling a Deployment, rolling a pod, or deleting a Service propagates within seconds without any restart.
This is the right provider when your workloads run on a Kubernetes cluster (kind, k3s, EKS, GKE, AKS, on-prem…) and you want a Sōzu-powered ingress for them.
Configuration
The minimal block — Sōzune auto-discovers the cluster (in-cluster ServiceAccount if it runs in a Pod, otherwise $KUBECONFIG / ~/.kube/config) and watches every namespace it can read.
providers: kubernetes: enabled: true
All fields:
| Field | Default | Description |
|---|---|---|
enabled | false | Enables the Kubernetes provider. |
kubeconfig | auto | Path to a specific kubeconfig file. Omit to auto-discover (in-cluster ServiceAccount, otherwise $KUBECONFIG / ~/.kube/config). |
namespace | all | Restrict discovery to a single namespace. Omit to watch the whole cluster. |
ingress_class | sozune | Match value for Ingress.spec.ingressClassName. Ingresses with a different (or missing) class are ignored. |
expose_by_default | false | If true, every Service is a candidate even without a sozune.* annotation. |
Restrict to one namespace, point at a specific kubeconfig
providers: kubernetes: enabled: true kubeconfig: /home/me/.kube/staging.yaml namespace: production
How it works
Sōzune runs three watchers in parallel:
- Service watcher. Every
Servicein scope is inspected. If it carries at least onesozune.*annotation (orexpose_by_defaultis set), it becomes a candidate. The annotations are parsed by the same engine used for Docker labels, sosozune validateand the runtime stay in sync. - EndpointSlice watcher. Sōzune maintains a per-service cache of ready pod IPs derived from
discovery.k8s.io/v1EndpointSliceobjects. Each Service or Ingress entrypoint is populated with all ready pod IPs as backends, so Sōzu round-robins directly between pods — bypassing kube-proxy. - Ingress watcher. Standard
networking.k8s.io/v1Ingresses withspec.ingressClassNamematchingingress_classare converted into entrypoints, one per(rule, path).
When the EndpointSlice cache is empty (cold-start race, ExternalName Service, or selector-less Service), Sōzune falls back to Service.spec.clusterIP so traffic still flows.
Service annotations
Annotations follow the same schema as Docker labels. Drop the labels: prefix and put sozune.* settings under metadata.annotations instead:
apiVersion: v1 kind: Service metadata: name: api namespace: default annotations: sozune.enable: "true" sozune.http.web.host: "api.example.com" sozune.http.web.port: "8080" sozune.http.web.tls: "true" spec: selector: app: api ports: - port: 8080 targetPort: 8080
Every annotation listed under Docker labels — host, path, headers, rate limit, basic auth, redirects, sticky sessions, compression — is supported as-is on Services.
Ingress resources
Sōzune also consumes standard networking.k8s.io/v1 Ingress objects. This is the right approach when you want to use the same manifests across multiple Ingress controllers, or when you're migrating from Traefik / Nginx Ingress.
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: api namespace: default spec: ingressClassName: sozune rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api port: number: 8080 tls: - hosts: - api.example.com
- Only Ingresses with
spec.ingressClassNamematching the configuredingress_classare picked up. Ingresses without aningressClassNameare ignored. - Each
(rule, path)pair becomes one entrypoint. The key isingress_<namespace>-<name>_r<rule_idx>p<path_idx>. pathType: PrefixandExactmap directly.ImplementationSpecificis treated asPrefix.spec.tls[].hostsenables HTTPS termination and triggers ACME provisioning for those hostnames.spec.defaultBackendis supported as a catch-all entrypoint with no host filter.- Backend Services must live in the same namespace as the Ingress (Kubernetes spec).
- Ingresses cannot express middleware (auth, rate-limit, headers, compression…). Use Service annotations when you need those.
Deploying Sōzune in-cluster
Run as a Deployment with a ServiceAccount bound to a ClusterRole that grants read access to Services, EndpointSlices, Ingresses, and Namespaces.
Minimum RBAC
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: sozune rules: - apiGroups: [""] resources: ["services", "namespaces"] verbs: ["get", "list", "watch"] - apiGroups: ["discovery.k8s.io"] resources: ["endpointslices"] verbs: ["get", "list", "watch"] - apiGroups: ["networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch"] # Optional: only needed if you want HTTPRoute support. - apiGroups: ["gateway.networking.k8s.io"] resources: ["httproutes", "gateways", "gatewayclasses", "referencegrants"] verbs: ["get", "list", "watch"] # Optional: only needed for Gateway API status reporting (kubectl # describe will show sōzune's conditions: Accepted/ResolvedRefs on # HTTPRoutes, Accepted/Programmed on Gateways, Accepted on GatewayClasses). - apiGroups: ["gateway.networking.k8s.io"] resources: ["httproutes/status", "gateways/status", "gatewayclasses/status"] verbs: ["update", "patch"]
namespaces is only used for the start-up sanity check; if you want a strictly minimal role, drop it (Sōzune logs a warning instead of an info line).
Reaching Sōzune from outside
Expose the Sōzune pod with a Service of type LoadBalancer (cloud) or NodePort (kind, on-prem). Sōzune itself listens on the ports declared under proxy.http.listen_address and proxy.https.listen_address.
Running outside the cluster
Useful for local development. Sōzune uses the current context from $KUBECONFIG or ~/.kube/config automatically; set kubeconfig: only if you need a specific file.
Heads up: pod IPs (
10.244.x.xon most clusters) are not routable from outside the cluster's CNI. Out-of-cluster Sōzune can therefore discover services correctly but cannot actually reach the backends. Use this mode for testing the discovery pipeline; deploy Sōzune in-cluster for real traffic.
Gateway API (HTTPRoute)
Sōzune watches gateway.networking.k8s.io/v1 resources alongside Ingress. Four watchers run side by side: GatewayClass, Gateway, ReferenceGrant, and HTTPRoute. They are started automatically when the Kubernetes provider is enabled and the CRDs are installed; no extra configuration is required.
Prerequisites
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
If the CRDs are missing, Sōzune logs HTTPRoute CRD not installed once and skips the Gateway watchers — Ingress alone keeps working.
Opting in: declare a GatewayClass
Sōzune only serves HTTPRoutes whose chain of parentRefs → Gateway → GatewayClass ends at a GatewayClass it owns. Multi-controller clusters depend on this — without it, Sōzune would hijack routes meant for Traefik, Envoy Gateway, NGINX Gateway, and friends.
Declare a GatewayClass whose spec.controllerName is kemeter.io/sozune:
apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: sozune spec: controllerName: kemeter.io/sozune
Then a Gateway that references it:
apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: gw namespace: default spec: gatewayClassName: sozune listeners: - name: http port: 80 protocol: HTTP
The listener block is required by the Gateway API schema, but Sōzune currently ignores it: real listening ports stay declared via
proxy.http.listen_address/proxy.https.listen_addressinconfig.yaml. Wiring listeners to live ports is a planned post-MVP enhancement.
Finally, the HTTPRoute references the Gateway via parentRefs:
apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: web namespace: default spec: parentRefs: - name: gw # same namespace; add `namespace:` for cross-ns parents hostnames: - app.example.com rules: - matches: - path: type: PathPrefix value: /api backendRefs: - name: api-svc port: 8080 weight: 100 - matches: - path: type: PathPrefix value: / backendRefs: - name: web-svc port: 80
Routes whose parentRefs point to a Gateway Sōzune does not own are silently ignored — you'll see them in kubectl get httproute but they will not produce any sōzune entrypoint. Routes with no parentRefs at all are also rejected (the Gateway API spec requires every Route to declare its parent).
What's supported
- Three watchers (
GatewayClass,Gateway,HTTPRoute) running cluster-wide and reacting to apply/delete events live. - Multi-controller scoping via
controllerName: kemeter.io/sozune(above). spec.hostnames— matched against theHostheader of incoming requests.spec.rules[].matches[].path—PathPrefixandExact.RegularExpressionis silently skipped.spec.rules[].matches[].headers[],matches[].queryParams[],matches[].method— a match can additionally require specific request headers, query parameters, and/or an HTTP method. Within one match these are ANDed with the path (and with each other); a request that fails any condition gets404. Header and query matches supporttype: Exact(the default) and presence-only (empty value);type: RegularExpressionon a header or query match has no faithful representation and the whole route is rejected withResolvedRefs=False reason=UnsupportedValuerather than served too broadly. All nine standard methods are supported.- Multiple
matchesper rule — Gateway API treats them as OR, so each match becomes its own sōzune entrypoint sharing the rule's backends. spec.rules[].backendRefs[]—Servicekind only (the default). Cross-namespacebackendRefs(backendRef.namespacediffers from the route's namespace) require aReferenceGrantin the target namespace; without one the backend is dropped (see Cross-namespace backends below).backendRef.weight— propagated to the load balancer.spec.rules[].filters[]—requestRedirect,requestHeaderModifier,responseHeaderModifier,urlRewrite(see HTTPRoute filters below).requestMirror,extensionRefare not supported yet.- Live reconciliation — apply/update/delete of any of the three resources is reflected in routing within seconds, including when the target Service's pods come up after the route was created, or when a
Gatewayappears after the routes that depend on it. - Status reporting — Sōzune writes standard conditions back onto the resources it owns, visible via
kubectl describe/kubectl get:HTTPRoute— for everyparentRefSōzune owns,status.parents[]carriesAcceptedandResolvedRefs(controllerName: kemeter.io/sozune). Other controllers' entries are preserved untouched.Gateway—status.conditions[]carriesAcceptedandProgrammed, both reflecting whether Sōzune owns the Gateway'sGatewayClass.GatewayClass—status.conditions[]carriesAcceptedon classes whosecontrollerNameiskemeter.io/sozune.
Status conditions
HTTPRoute (status.parents[].conditions[]):
| Condition | Status | Reason | When |
|---|---|---|---|
Accepted | True (Accepted) | Route is in scope (parent owned), filters are OK | |
Accepted | False (UnsupportedValue) | Route declares unsupported filters | |
ResolvedRefs | True (ResolvedRefs) | All backendRefs resolved to ready endpoints | |
ResolvedRefs | False (BackendNotFound) | One or more backendRefs has no ready endpoint yet — sōzune retries every 2 s | |
ResolvedRefs | False (UnsupportedValue) | Route was rejected because of unsupported filters |
Routes whose parent is not sōzune-owned receive no status entry from sōzune (per Gateway API spec — implementations only report on parents they own).
Gateway (status.conditions[]):
| Condition | Status | Reason | When |
|---|---|---|---|
Accepted | True (Accepted) | Gateway's gatewayClassName points at a class sōzune owns | |
Programmed | True (Programmed) | Same instant as acceptance — sōzune does not stage programming |
GatewayClass (status.conditions[]):
| Condition | Status | Reason | When |
|---|---|---|---|
Accepted | True (Accepted) | spec.controllerName is kemeter.io/sozune |
Gateways and GatewayClasses that belong to another controller receive no status entry from sōzune.
Cross-namespace backends (ReferenceGrant)
An HTTPRoute may reference a Service in another namespace via backendRef.namespace. Per the Gateway API spec, the target namespace must opt in with a ReferenceGrant: without one, the reference is a potential privilege escalation (any route author could route traffic to any Service cluster-wide), so Sōzune drops the backend and logs a WARN. Same-namespace references never need a grant.
The grant lives in the namespace that owns the Service and names the route's namespace as trusted:
apiVersion: gateway.networking.k8s.io/v1beta1 kind: ReferenceGrant metadata: name: allow-frontend-routes namespace: backend # the Service's namespace spec: from: - group: gateway.networking.k8s.io kind: HTTPRoute namespace: frontend # the HTTPRoute's namespace to: - group: "" kind: Service
With this applied, an HTTPRoute in frontend may target a Service in backend. Removing the grant revokes access on the next reconcile (the route falls back to ResolvedRefs=False once the backend is dropped). Grants naming a non-Service to kind (e.g. Secret) do not authorise backendRefs.
What's not supported (yet)
- Listener-driven port binding — the
listenersblock onGatewayis parsed but ignored; ports are still configured viaproxy.http.listen_address/proxy.https.listen_address. parentRef.sectionNameandparentRef.port— the route binds to the wholeGateway, not a specific listener.- HTTPRoute
filtersrequestMirrorandextensionRef. Declaring one of these (or arequestRedirectwe can't represent, orrequestRedirectcombined withurlRewriteon the same rule, see below) causes Sōzune to drop the entire route with aWARNlog line and surfaceAccepted=False reason=UnsupportedValuein the route status. Routing it as if the filter weren't there would silently rewrite user intent. Use Service or Ingress annotations until support lands. (requestRedirect,requestHeaderModifier,responseHeaderModifier, andurlRewriteare supported — see HTTPRoute filters.) GRPCRoute,TCPRoute,UDPRoute,TLSRoute.
HTTPRoute filters
| Filter | Supported | Notes |
|---|---|---|
requestRedirect | Yes | Native frontend redirect; replacePrefixMatch and 302 are dropped (see below) |
requestHeaderModifier | Yes | set/remove map directly; add is applied as set (see below) |
responseHeaderModifier | Yes | Same as above, on the response |
urlRewrite | Yes | Transparent path/host rewrite (no redirect); ReplaceFullPath, ReplacePrefixMatch, hostname (see below) |
requestMirror | No | Route dropped with Accepted=False reason=UnsupportedValue |
extensionRef | No | Route dropped with Accepted=False reason=UnsupportedValue |
A rule may combine a header modifier with either a requestRedirect or a urlRewrite (e.g. a requestRedirect together with a requestHeaderModifier); all are honoured together. requestRedirect and urlRewrite conflict — one redirects the client, the other transparently rewrites the forwarded request, and both target the same frontend rewrite — so a rule carrying both is rejected. A rule carrying any unsupported filter drops the whole route.
requestRedirect
Sōzune maps the requestRedirect filter onto Sōzu's native frontend redirect — no extra middleware hop. A rule may declare a redirect alongside its backendRefs:
rules: - matches: - path: type: PathPrefix value: / filters: - type: RequestRedirect requestRedirect: scheme: https # http→https is the common case hostname: new.example.com # optional port: 8443 # optional backendRefs: - name: web-svc port: 80
requestRedirect field | Mapped to | Notes |
|---|---|---|
scheme (http / https) | redirect scheme | omitted → preserve the request scheme |
statusCode | redirect policy | 301 (default) → permanent redirect. 302 is not supported — the route is dropped rather than emit a 301 |
hostname | rewrite_host | rewrites the Location authority |
port | rewrite_port | rewrites the Location port |
path.replaceFullPath | rewrite_path | replaces the whole Location path with a fixed value |
path.replacePrefixMatch | — | not supported: keeping the request's trailing segments needs a path capture Sōzune doesn't set up for redirect rules, so the route is dropped |
A requestRedirect declaring only supported fields is honoured; one using replacePrefixMatch or a 302 status is treated as unsupported and the whole route is dropped (Accepted=False reason=UnsupportedValue).
urlRewrite
Unlike requestRedirect, the urlRewrite filter is transparent: the backend receives the rewritten request and the client sees no redirect. It maps onto Sōzu's native frontend rewrite_path / rewrite_host — no extra middleware hop.
rules: - matches: - path: type: PathPrefix value: /api filters: - type: URLRewrite urlRewrite: hostname: internal.svc # optional: rewrites the Host header path: type: ReplacePrefixMatch # or ReplaceFullPath replacePrefixMatch: /v2 backendRefs: - name: api-svc port: 80
urlRewrite field | Mapped to | Notes |
|---|---|---|
hostname | rewrite_host | rewrites the forwarded request's Host header to a fixed authority |
path.replaceFullPath | rewrite_path | replaces the whole request path with a fixed value, regardless of trailing segments (/api/users with ReplaceFullPath: /new → /new) |
path.replacePrefixMatch | rewrite_path | swaps the matched prefix and keeps the trailing segments (/api/users with prefix /api, ReplacePrefixMatch: /v2 → /v2/users; /api exactly → /v2). Only meaningful with a PathPrefix match |
When set, urlRewrite takes precedence over the strip_prefix / add_prefix Service-annotation knobs. urlRewrite cannot be combined with requestRedirect on the same rule (the route is dropped).
requestHeaderModifier / responseHeaderModifier
Header modifiers map onto Sōzu's native frontend header edits — no extra middleware hop. requestHeaderModifier edits the request before it reaches the backend; responseHeaderModifier edits the response on the way back.
rules: - filters: - type: RequestHeaderModifier requestHeaderModifier: set: - name: X-Env value: prod remove: - X-Debug - type: ResponseHeaderModifier responseHeaderModifier: set: - name: X-Powered-By value: sozune backendRefs: - name: web-svc port: 80
| Field | Mapped to | Notes |
|---|---|---|
set | header set | overwrites the header with the given value |
remove | header delete | maps to an empty-value frontend edit, which deletes the header |
add | header set | caveat: Sōzu has no frontend append-without-replace, so add is applied as set (existing values are replaced, not appended) and a WARN is logged. The route is not dropped — set-semantics is what most add users want |
How resolution works
When an HTTPRoute is applied, Sōzune resolves each backendRef to the ready pod IPs from the matching Service's EndpointSlices. Sōzu requires IpAddr backends and refuses cluster-DNS hostnames, so a route that targets a Service with no ready endpoints registers no entrypoint and is retried every 2 seconds until the endpoints appear. Once at least one ready endpoint exists the route becomes live without any user intervention.
ACME / Let's Encrypt
When a Service is annotated with sozune.http.<svc>.tls=true, or an Ingress declares spec.tls[].hosts, Sōzune provisions a certificate for the declared hostnames. The HTTP-01 challenge responder runs inside the Sōzune pod, so:
- Sōzune must be reachable on port 80 from the public internet for the challenge to succeed.
acme.certs_dirshould point to aPersistentVolumeso issued certs survive pod restarts.
Refer to ACME / Let's Encrypt for the full setup.
Limitations
- Multi-cluster. A single Sōzune instance watches a single API server. Run one Sōzune per cluster.
- EndpointSlice required. Kubernetes ≥ 1.21 only — the deprecated
EndpointsAPI is not consumed. - UDP entrypoints are recognised at the annotation level but not yet proxied (same caveat as the Docker provider).
- Ingress middleware not supported. The
IngressAPI has no portable way to express auth, rate-limit, or headers. Use Service annotations when you need middleware. - Cross-namespace backends not supported on Ingress. Backends must live in the same namespace as the Ingress, per the Kubernetes spec. (HTTPRoute supports cross-namespace
backendRefswhen authorised by aReferenceGrant.) - Gateway API:
GRPCRoute,TCPRoute,UDPRoute,TLSRoute, and therequestMirror/extensionRefHTTPRoute filters are not yet implemented.GatewayClass,Gateway,HTTPRoute, andReferenceGrantare supported, as are therequestRedirect,requestHeaderModifier,responseHeaderModifier, andurlRewritefilters. See the Gateway API section above for details.
Environment variables
| Field | Env var |
|---|---|
providers.kubernetes.enabled | SOZUNE_PROVIDER_KUBERNETES_ENABLED |
providers.kubernetes.kubeconfig | SOZUNE_PROVIDER_KUBERNETES_KUBECONFIG |
providers.kubernetes.namespace | SOZUNE_PROVIDER_KUBERNETES_NAMESPACE |
providers.kubernetes.ingress_class | SOZUNE_PROVIDER_KUBERNETES_INGRESS_CLASS |
providers.kubernetes.expose_by_default | SOZUNE_PROVIDER_KUBERNETES_EXPOSE_BY_DEFAULT |