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:

FieldDefaultDescription
enabledfalseEnables the Kubernetes provider.
kubeconfigautoPath to a specific kubeconfig file. Omit to auto-discover (in-cluster ServiceAccount, otherwise $KUBECONFIG / ~/.kube/config).
namespaceallRestrict discovery to a single namespace. Omit to watch the whole cluster.
ingress_classsozuneMatch value for Ingress.spec.ingressClassName. Ingresses with a different (or missing) class are ignored.
expose_by_defaultfalseIf 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:

  1. Service watcher. Every Service in scope is inspected. If it carries at least one sozune.* annotation (or expose_by_default is set), it becomes a candidate. The annotations are parsed by the same engine used for Docker labels, so sozune validate and the runtime stay in sync.
  2. EndpointSlice watcher. Sōzune maintains a per-service cache of ready pod IPs derived from discovery.k8s.io/v1 EndpointSlice objects. 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.
  3. Ingress watcher. Standard networking.k8s.io/v1 Ingresses with spec.ingressClassName matching ingress_class are 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.ingressClassName matching the configured ingress_class are picked up. Ingresses without an ingressClassName are ignored.
  • Each (rule, path) pair becomes one entrypoint. The key is ingress_<namespace>-<name>_r<rule_idx>p<path_idx>.
  • pathType: Prefix and Exact map directly. ImplementationSpecific is treated as Prefix.
  • spec.tls[].hosts enables HTTPS termination and triggers ACME provisioning for those hostnames.
  • spec.defaultBackend is 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 Gateway API support. `tlsroutes` is
  # only consulted when the (experimental) CRD is installed; without the
  # permission Sōzune logs a warning and skips that watcher.
  - apiGroups: ["gateway.networking.k8s.io"]
    resources:
      ["httproutes", "tlsroutes", "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.x on 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 resources alongside Ingress. Five watchers run side by side: GatewayClass, Gateway, ReferenceGrant, HTTPRoute, and TLSRoute (see Gateway API (TLSRoute)). They are started automatically when the Kubernetes provider is enabled and the CRDs are installed; no extra configuration is required. The TLSRoute watcher exits quietly when its CRD — experimental-channel only — is absent.

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 honoured for route selection and hostname narrowing, but not for the actual listening port: real listening ports stay declared via proxy.http.listen_address / proxy.https.listen_address in config.yaml. A parentRef can target a specific listener with sectionName (matched against a listener name) or port, and a listener's hostname narrows which of a route's hostnames it serves. Binding traffic on the listener's own port (multiple live HTTP ports from one Gateway) is a separate, planned 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 the Host header of incoming requests, and narrowed by the bound listener's hostname (a listener serving *.example.com drops a route hostname outside that suffix).
  • parentRef.sectionName / parentRef.port — bind the route to a specific Gateway listener (by listener name or port). A ref naming a listener the Gateway doesn't have is refused; an unset sectionName and port bind to the whole Gateway (any listener), as before.
  • spec.rules[].matches[].pathPathPrefix and Exact. RegularExpression is 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 gets 404. Header and query matches support type: Exact (the default) and presence-only (empty value); type: RegularExpression on a header or query match has no faithful representation and the whole route is rejected with ResolvedRefs=False reason=UnsupportedValue rather than served too broadly. All nine standard methods are supported.
  • Multiple matches per rule — Gateway API treats them as OR, so each match becomes its own sōzune entrypoint sharing the rule's backends.
  • spec.rules[].backendRefs[]Service kind only (the default). Cross-namespace backendRefs (backendRef.namespace differs from the route's namespace) require a ReferenceGrant in 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, extensionRef are 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 Gateway appears 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 every parentRef Sōzune owns, status.parents[] carries Accepted and ResolvedRefs (controllerName: kemeter.io/sozune). Other controllers' entries are preserved untouched.
    • Gatewaystatus.conditions[] carries Accepted and Programmed, both reflecting whether Sōzune owns the Gateway's GatewayClass.
    • GatewayClassstatus.conditions[] carries Accepted on classes whose controllerName is kemeter.io/sozune.

Status conditions

HTTPRoute (status.parents[].conditions[]):

ConditionStatusReasonWhen
AcceptedTrue (Accepted)Route is in scope (parent owned), filters are OK
AcceptedFalse (UnsupportedValue)Route declares unsupported filters
ResolvedRefsTrue (ResolvedRefs)All backendRefs resolved to ready endpoints
ResolvedRefsFalse (BackendNotFound)One or more backendRefs has no ready endpoint yet — sōzune retries every 2 s
ResolvedRefsFalse (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[]):

ConditionStatusReasonWhen
AcceptedTrue (Accepted)Gateway's gatewayClassName points at a class sōzune owns
ProgrammedTrue (Programmed)Same instant as acceptance — sōzune does not stage programming

GatewayClass (status.conditions[]):

ConditionStatusReasonWhen
AcceptedTrue (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 — a parentRef's sectionName/port selects a listener and a listener hostname narrows route hostnames (both honoured), but traffic is still served on the ports from proxy.http.listen_address / proxy.https.listen_address, not on each listener's own port. Serving multiple live HTTP ports from one Gateway is a separate, planned enhancement.
  • HTTPRoute filters requestMirror and extensionRef. Declaring one of these (or a requestRedirect we can't represent, or requestRedirect combined with urlRewrite on the same rule, see below) causes Sōzune to drop the entire route with a WARN log line and surface Accepted=False reason=UnsupportedValue in 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, and urlRewrite are supported — see HTTPRoute filters.)
  • GRPCRoute.

HTTPRoute filters

FilterSupportedNotes
requestRedirectYesNative frontend redirect; replacePrefixMatch and 302 are dropped (see below)
requestHeaderModifierYesset/remove map directly; add is applied as set (see below)
responseHeaderModifierYesSame as above, on the response
urlRewriteYesTransparent path/host rewrite (no redirect); ReplaceFullPath, ReplacePrefixMatch, hostname (see below)
requestMirrorNoRoute dropped with Accepted=False reason=UnsupportedValue
extensionRefNoRoute 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 fieldMapped toNotes
scheme (http / https)redirect schemeomitted → preserve the request scheme
statusCoderedirect policy301 (default) → permanent redirect. 302 is not supported — the route is dropped rather than emit a 301
hostnamerewrite_hostrewrites the Location authority
portrewrite_portrewrites the Location port
path.replaceFullPathrewrite_pathreplaces the whole Location path with a fixed value
path.replacePrefixMatchnot 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 fieldMapped toNotes
hostnamerewrite_hostrewrites the forwarded request's Host header to a fixed authority
path.replaceFullPathrewrite_pathreplaces the whole request path with a fixed value, regardless of trailing segments (/api/users with ReplaceFullPath: /new/new)
path.replacePrefixMatchrewrite_pathswaps 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
FieldMapped toNotes
setheader setoverwrites the header with the given value
removeheader deletemaps to an empty-value frontend edit, which deletes the header
addheader setcaveat: 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.

Gateway API (TLSRoute)

TLSRoute routes TLS connections to different backends by the server name in the ClientHello, without decrypting. The client's handshake completes with the backend, against the backend's own certificate. Use it when the backend must own its certificates: end-to-end encryption to the service, a tenant terminating its own TLS, or a protocol Sōzune does not speak.

TLSRoute is v1alpha2 and ships only in the Gateway API experimental channel. Sōzune probes for the CRD at startup and skips the watcher when it is absent, so a cluster with only the standard bundle installed is unaffected.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: tlsgw
spec:
  gatewayClassName: sozune
  listeners:
    - name: passthrough
      protocol: TLS
      port: 8443
      tls:
        mode: Passthrough
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
  name: my-passthrough
spec:
  parentRefs:
    - name: tlsgw
      sectionName: passthrough
  hostnames:
    - app.example.com
  rules:
    - backendRefs:
        - name: my-service
          port: 8443

The port must already be declared

Sōzune binds TCP ports at startup from config.yaml and never opens one from a Gateway alone — the same rule that applies to Docker labels, kept so startup state stays predictable. A Gateway listener on port 8443 is therefore served only if a proxy.tcp entry already listens there:

proxy:
  tcp:
    - name: tlsgw
      listen: 8443

A TLSRoute whose listener port has no matching entry is tracked but not served, and Sōzune logs which port it wanted. Declare the listener and the route goes live on the next reload, with no need to re-apply it.

What is supported

AspectBehaviour
tls.modePassthrough only. Terminate means the Gateway decrypts, which is what HTTPS entrypoints do — such a listener is ignored here.
hostnamesEach becomes one SNI route. Exact names and a single leading *. wildcard label, per spec. A route with no hostname serves nothing: there is no name to match a ClientHello against.
Listener hostnameIntersected with the route's hostnames, exactly as for HTTPRoute.
backendRefsService-typed, resolved to ready pod IPs. Cross-namespace refs need a ReferenceGrant. weight is honoured.
parentRefssectionName and port select a listener, as for HTTPRoute. A route may bind listeners on several ports; each serves only the names its own hostname admits.
Invalid hostnamesA name Sōzu could not route (a wildcard beyond one leading *. label, /, an empty label, non-ASCII) is dropped with a warning; the route's other names still serve.
Unmatched SNIDropped. Once a listener carries SNI routes there is no catch-all — declare a route for every name you serve. See Route by SNI.

TLSRoute has no matches, filters or paths — the SNI is the whole routing decision — so none of the HTTPRoute filter machinery applies.

Gateway API (TCPRoute)

TCPRoute forwards a whole listener port to its backends — plain TCP, no SNI, no TLS, no matching. It is the raw-TCP counterpart to TLSRoute: where TLSRoute routes by the name in the ClientHello, TCPRoute routes nothing and hands the entire connection to its backend.

TCPRoute is v1alpha2 and ships only in the Gateway API experimental channel; the watcher probes for the CRD and skips quietly when it is absent.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: tcpgw
spec:
  gatewayClassName: sozune
  listeners:
    - name: raw
      protocol: TCP
      port: 5432
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
  name: my-db
spec:
  parentRefs:
    - name: tcpgw
      sectionName: raw
  rules:
    - backendRefs:
        - name: postgres
          port: 5432

The port must already be declared

Exactly as for TLSRoute: the Gateway's TCP listener port must match a proxy.tcp entry in config.yaml. Sōzune binds TCP ports at startup and never opens one from a Gateway alone. A route on an undeclared port is tracked but not served, and goes live on the next reload once the listener is declared.

proxy:
  tcp:
    - name: tcpgw
      listen: 5432

What is supported

AspectBehaviour
Listenerprotocol: TCP only. A TLS or HTTP listener is ignored by TCPRoutes.
RoutingNone — the whole port goes to the backend. One rule → one entrypoint. No hostnames, matches, or filters (a TCPRoute has none).
backendRefsService-typed, resolved to ready pod IPs. Cross-namespace refs need a ReferenceGrant. weight is honoured.
parentRefssectionName and port select a listener. A route may bind listeners on several ports; each gets its own entrypoint.

Gateway API (UDPRoute)

UDPRoute is the datagram counterpart to TCPRoute: it forwards a whole UDP listener port to its backends. Same shape, same constraints — the only difference is the listener protocol (UDP) and that it resolves against proxy.udp listeners rather than proxy.tcp.

UDPRoute is v1alpha2/experimental; the watcher probes for the CRD and skips quietly when it is absent.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: udpgw
spec:
  gatewayClassName: sozune
  listeners:
    - name: dns
      protocol: UDP
      port: 53
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: UDPRoute
metadata:
  name: my-dns
spec:
  parentRefs:
    - name: udpgw
      sectionName: dns
  rules:
    - backendRefs:
        - name: coredns
          port: 53

The Gateway's UDP listener port must match a proxy.udp entry in config.yaml, exactly as TCPRoute requires a proxy.tcp one:

proxy:
  udp:
    - name: dns
      listen: 53

Support is the same as TCPRoute — one catch-all entrypoint per rule, Service-typed backends, ReferenceGrant for cross-namespace, no hostnames or filters — on a UDP listener.

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_dir should point to a PersistentVolume so 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 Endpoints API 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 Ingress API 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 backendRefs when authorised by a ReferenceGrant.)
  • Gateway API: GRPCRoute and the requestMirror / extensionRef HTTPRoute filters are not yet implemented. GatewayClass, Gateway, HTTPRoute, TLSRoute, TCPRoute, UDPRoute and ReferenceGrant are supported, as are the requestRedirect, requestHeaderModifier, responseHeaderModifier, and urlRewrite filters. See the Gateway API section above for details.
  • TLSRoute and TCPRoute serve on a pre-declared port. The listener's port must already be bound by a proxy.tcp entry — Sōzune does not open TCP ports from a Gateway alone. TLSRoute is Passthrough only (tls.mode: Terminate is out of scope, that is what HTTPS entrypoints are for). See Gateway API (TLSRoute) and Gateway API (TCPRoute).

Environment variables

FieldEnv var
providers.kubernetes.enabledSOZUNE_PROVIDER_KUBERNETES_ENABLED
providers.kubernetes.kubeconfigSOZUNE_PROVIDER_KUBERNETES_KUBECONFIG
providers.kubernetes.namespaceSOZUNE_PROVIDER_KUBERNETES_NAMESPACE
providers.kubernetes.ingress_classSOZUNE_PROVIDER_KUBERNETES_INGRESS_CLASS
providers.kubernetes.expose_by_defaultSOZUNE_PROVIDER_KUBERNETES_EXPOSE_BY_DEFAULT