L4Ingress reference
The custom resource for TCP and UDP services: every field, which ones you must set, and what happens to the ones you leave out.
An Ingress routes on hosts and paths, and neither exists in a protocol nobody has parsed. So a plain TCP or UDP service gets a resource of its own, declared in the same namespace as the service it exposes.
The smallest one that works
Four fields. No access control, no TLS, nothing optional — this is a complete, valid L4Ingress and it is what most of them look like.
postgres.yaml
apiVersion: sslb3.hacking.hu/v1alpha1
kind: L4Ingress
metadata:
name: postgres
namespace: shop
spec:
ingressClassName: sslb3
protocol: TCP
listenPort: 15432
service:
name: postgres
port: 5432Fields
Everything under spec. The API server enforces the types and the ranges, so a malformed object is refused at kubectl apply rather than accepted and then logged about.
The two optional fields
allowSourceRange and denySourceRange are the whole of the optional surface, and both default to empty. Empty means admit anything not denied — the minimal example above serves every client that can reach the port, which is what you want for a database already behind a private network.
They are schema fields rather than annotations on purpose. An annotation is the right shape for something layered onto a resource somebody else defined; this is a kind I own, so a control this important belongs where the API server validates it and kubectl explain describes it.
postgres-restricted.yaml
spec:
ingressClassName: sslb3
protocol: TCP
listenPort: 15432
service:
name: postgres
port: 5432
# Both optional, both empty by default.
allowSourceRange:
- 10.0.0.0/8
- 192.168.4.0/24
denySourceRange:
- 10.1.2.3The two lists are resolved together by specificity, not by order or by which list a rule came from: 10.1.2.3 above is denied even though 10.0.0.0/8 admits it, because a rule about one host is more specific than a rule about its network. An exact tie goes to deny. Where the kernel fast path takes the service, the same rules travel with it as nftables rules with that ordering preserved.
Ports
listenPort must be 1024 or above and the schema enforces it. These listeners are opened while the balancer is already running, after it has dropped the privileges a lower port would need. 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. Both are needed, and neither implies the other.
Two objects, one port
A port is one thing across the whole cluster while these resources are namespaced, so two namespaces can ask for the same number. The oldest wins, with the namespace and name breaking an exact tie so the outcome is the same on every reconcile. A service already working is never displaced by one created later; the loser is not served and says so in its own status.
The same number on TCP and on UDP is two listeners rather than a conflict — they are separate address spaces.
Status
The controller writes back what it made of the object, which is worth reading because the answer is not always yes.
The resource ships print columns, so the common question is answered without reaching for a JSON path:
shell
kubectl get l4ing -A NAMESPACE NAME PROTOCOL PORT SERVICE ACCEPTED AGE shop postgres TCP 15432 postgres True 6d shop syslog UDP 15140 syslog True 6d
UDP
The same kind with one word changed. There is no second resource for it because everything about the two is identical except which protocol goes in that field.
syslog.yaml
apiVersion: sslb3.hacking.hu/v1alpha1
kind: L4Ingress
metadata:
name: syslog
namespace: shop
spec:
ingressClassName: sslb3
protocol: UDP
listenPort: 15140
service:
name: syslog
port: 514An L4Ingress produces a listener with nothing parsing the traffic, which is the exact shape the kernel fast path exists for. Where the host can forward it, the kernel does and SSLB3 supervises instead. HTTP services are configured with an ordinary Ingress and its annotations.