The Kubernetes ingress controller
SSLB3 runs as a Kubernetes ingress controller: it watches the API for Ingress and L4Ingress objects and configures itself from them. HTTP routing comes from Ingress; TCP and UDP services come from a custom resource, because an Ingress cannot describe one.
How it is put together
The controller and the balancer run in one pod, and that is a deliberate choice rather than a convenience. The admin API configures one balancer instance. With the controller in a Deployment of its own it would have to discover every balancer pod and push to each, and a pod that started after the last reconcile would serve nothing until the next one. As a sidecar, each pod configures itself: scaling is a replica count, a new pod is ready within one reconcile, and the admin API binds loopback so the control plane never crosses the network.
The cost is one API server watch per replica. For the handful of replicas an ingress fleet runs, that is cheaper than solving the fan-out.
Installing it
The chart is published as an OCI artifact, so there is no repository to add.
shell
helm install sslb3 \ oci://docker.hacking.hu/public/sslb3-ingress \ --namespace sslb3 --create-namespace
Or apply the manifests directly, which is the same deployment written out. Use --server-side — two ports share 443 and 8443, and a client-side apply silently drops one of them.
shell
kubectl apply --server-side -k crates/sslb3-ingress-controller/kube
Full detail, including the values worth setting, is on the installation page.
HTTP, with an Ingress
An ordinary Ingress, claimed by the sslb3 IngressClass. Hosts and paths become native routing rules in the balancer's configuration rather than a script, so the request path stays in Rust.
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
annotations:
sslb3.ingress.hacking.hu/allowlist-source-range: "10.0.0.0/8"
spec:
ingressClassName: sslb3
tls:
- hosts: [shop.example.com]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: shop
port: { number: 80 }TLS comes from the Secret named in spec.tls. The controller reads it, pushes the certificate to the balancer's store, and the listener presents it by server name — no restart, and a renewed certificate needs nothing more than the Secret changing.
TCP and UDP, with an L4Ingress
An Ingress routes on hosts and paths, and neither exists in a protocol nobody has parsed. So layer 4 services come from a custom resource, declared beside the service it exposes:
l4ingress.yaml
apiVersion: sslb3.hacking.hu/v1alpha1
kind: L4Ingress
metadata:
name: postgres
namespace: shop
spec:
ingressClassName: sslb3
protocol: TCP # or UDP
listenPort: 15432
service:
name: postgres
port: 5432
# Optional. The access control available in front of a protocol nobody parses.
allowSourceRange: ["10.0.0.0/8"]
denySourceRange: ["10.1.2.3"]listenPort must be 1024 or above and the schema enforces it: these listeners are opened while the balancer is running, after it has dropped the privileges a lower port needs. Publish the port clients actually use on the Service in front, the way 80 and 443 are published onto 8080 and 8443. The L4Ingress says what to serve; the Service is what makes it reachable, and both are needed.
A port is one object across the cluster while these are namespaced, so two namespaces can claim the same one. The older wins — stable across reconciles, and a service already working is never displaced by one created later. The same number on TCP and UDP is two listeners, not a conflict.
These are the services the fast path moves
An L4Ingress produces a listener pinned to its farm with nothing parsing the traffic, which is exactly the shape the kernel fast path exists for. Where the host can forward it, the kernel does, and the source filter above travels with it as an nftables rule. Where it cannot, SSLB3 serves it and says why.
Annotations
Per-Ingress behaviour, all prefixed sslb3.ingress.hacking.hu/. The common ones:
Unknown annotations under the prefix are reported rather than ignored, so a typo does not silently do nothing.
Operating it
Status on the object
An L4Ingress carries whether it was accepted, and the reason when it was not — a port already claimed by an older object, most often. Read it with kubectl rather than the controller log.
Reconciliation is level triggered
The whole desired state is pushed each pass, so a missed event heals on the next one. There is no queue to get stuck and no incremental state to go wrong.
Metrics and a statistics page
Both off by default and both worth turning on: every listener, farm and backend with its health, plus which services the fast path moved and why the rest stayed.
The service account is narrow
It reads Secrets cluster-wide because it must, to serve the certificates Ingresses name — so its token is projected only into the controller container, never into the one running Lua.
Publishing it
The chart creates a Service in front. What type it should be is the one decision the chart cannot make for you:
LoadBalancer
The obvious answer where a cloud provider will give you an address.
ClusterIP, advertised
On bare metal with kube-router or MetalLB, the ClusterIP is what gets advertised. Pin it with service.clusterIP — deleting and recreating the Service releases the address, and anything routing to it follows.