Annotation reference
Every annotation SSLB3's ingress controller reads, what it accepts, and what it does when you leave it alone. All of them, not a selection.
Put these on an Ingress, each prefixed sslb3.ingress.hacking.hu/. Every one of them is optional: an Ingress with no annotations at all is a valid Ingress, and it is the one you should start from.
An annotation under the prefix that I do not recognise is logged and ignored. A value that will not parse is logged and dropped rather than quietly becoming a default, because a setting that silently did nothing is worse than one that was refused. Another controller's annotations — anything under nginx.ingress.kubernetes.io/, say — are not mine to interpret and are left alone.
Applied to the serverfarm
These are enforced by the balancer itself, on the farm behind the rule. The important consequence is in the note under the table: a farm is one object per service, so two Ingresses pointing at the same service share these settings whether they meant to or not.
Examples
Health checking, with a probe rather than trusting pod readiness alone. Kubernetes already knows whether the container is up; this is SSLB3 forming its own opinion about whether the backend is answering.
health.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/health-check: "true"
sslb3.ingress.hacking.hu/health-check-type: http
sslb3.ingress.hacking.hu/connect-timeout: "3"Timeouts for a service with long-lived connections — a websocket endpoint, say, where an idle timeout would be wrong but an unbounded connection is worse:
timeouts.yaml
metadata:
annotations:
# Silence is expected here, so do not close on it.
sslb3.ingress.hacking.hu/idle-timeout: "0"
# But nothing gets to live forever. This is the one worth setting: an idle
# timeout does nothing against a connection trickling a byte to look busy.
sslb3.ingress.hacking.hu/max-connection-duration: "3600"Cookie affinity, for a backend that keeps per-user state in memory. Worth knowing before you reach for it: sessions live in the balancer's own process, so with more than one replica a client that reaches a different pod is balanced afresh whatever the cookie says.
affinity.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/affinity: cookie
sslb3.ingress.hacking.hu/session-cookie-name: shop_backend
sslb3.ingress.hacking.hu/session-cookie-max-age: "3600"Talking TLS to the backends, for a service that terminates its own certificate. The verify switch is there for the common case of an internal certificate nothing in the cluster trusts:
backend-tls.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/serverfarm-sni: shop.internal
sslb3.ingress.hacking.hu/serverfarm-ssl-verify: "false"And the rollout this design exists for. Leave the balancer on block with the serverfarm default off, then move services across one at a time — watch first, enforce when the numbers look right:
threat-rollout.yaml
# The service you are still watching. Counted and reported, nobody refused.
metadata:
annotations:
sslb3.ingress.hacking.hu/threat-detection: detect
---
# The service you are confident about.
metadata:
annotations:
sslb3.ingress.hacking.hu/threat-detection: block
---
# The service with a mobile SDK that spoofs a browser User-Agent on purpose.
# Enforcing everywhere except that one signal, which would turn it away.
metadata:
annotations:
sslb3.ingress.hacking.hu/threat-detection: block
sslb3.ingress.hacking.hu/impersonation-enforcement: "false"Applied per request
These are carried on the route and applied to each request as it is served, so unlike the farm settings above they really are per-Ingress.
Examples
Basic auth, end to end. The Secret is an ordinary htpasswd file — the same shape ingress-nginx expects, so an existing one can be moved across unchanged. An unqualified name is read from the Ingress's own namespace; write namespace/name to point elsewhere.
shell
htpasswd -nbB alice 'correct horse battery staple' > auth htpasswd -nbB bob 'hunter2' >> auth kubectl -n shop create secret generic staging-users --from-file=auth
auth.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/auth-type: basic
sslb3.ingress.hacking.hu/auth-secret: staging-users
sslb3.ingress.hacking.hu/auth-realm: "Staging"
# By default the header is consumed here. Set this if the backend
# wants to see it too -- to know which user it is serving, say.
sslb3.ingress.hacking.hu/auth-keep-authorization: "true"Both halves are required. auth-secret on its own is logged and applied to nothing, rather than leaving a route that was meant to be protected quietly open — that is the worst outcome available here, so it is the one I made impossible.
An internal-only service, admitting the private ranges and cutting out one host inside them. Note the specificity rule doing the work: the deny is narrower than the allow, so it wins without needing to be ordered first.
restricted.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/allowlist-source-range: "10.0.0.0/8,192.168.0.0/16"
sslb3.ingress.hacking.hu/denylist-source-range: "10.1.2.3"A browser API with CORS. Turning it on is the one annotation that matters; the six cors- settings only narrow what the headers say, and each has a working default.
cors.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/enable-cors: "true"
sslb3.ingress.hacking.hu/cors-allow-origin: "https://app.example.com"
sslb3.ingress.hacking.hu/cors-allow-methods: "GET, POST, OPTIONS"
sslb3.ingress.hacking.hu/cors-expose-headers: "X-Request-Id"
sslb3.ingress.hacking.hu/cors-max-age: "86400"Leave the rest out and you get * for the origin, the ingress-nginx method and header lists, credentials allowed, and a twenty-day preflight cache.
A backend that routes on the Host header and does not know the public name, plus a couple of headers tidied on the way past:
vhost.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/serverfarm-vhost: shop.internal
sslb3.ingress.hacking.hu/request-headers: |
X-Forwarded-Host: shop.example.com
X-Environment: production
sslb3.ingress.hacking.hu/response-headers: |
Strict-Transport-Security: max-age=31536000; includeSubDomains
sslb3.ingress.hacking.hu/response-headers-remove: Server, X-Powered-ByNarrowing the HTTP versions a route answers. Rarely needed, but a client library that negotiates HTTP/2 badly is a real thing and this is cheaper than fixing it everywhere:
protocols.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/allowed-protocols: "http1,http2"
# When the port clients should retry QUIC on is not the one this listener
# is bound to -- behind a Service that publishes 443 onto 8443, say.
sslb3.ingress.hacking.hu/http3-advertise-port: "443"And redirect plus per-route logging, for a public site where you want a record of what was served without turning access logging on for everything:
public.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/force-ssl-redirect: "true"
sslb3.ingress.hacking.hu/enable-access-log: "true"How values are read
Booleans
Anywhere a table says true or false, I also accept yes/no, on/off and 1/0. That is partly convenience and partly so a manifest carried over from ingress-nginx, which writes on and off, does not need editing to keep working.
Header lists
The four header annotations take one Name: value per line. The -remove pair takes bare header names, one per line or comma separated.
headers.yaml
metadata:
annotations:
sslb3.ingress.hacking.hu/request-headers: |
X-Forwarded-Host: shop.example.com
X-Environment: production
sslb3.ingress.hacking.hu/response-headers: |
Strict-Transport-Security: max-age=31536000
X-Frame-Options: DENY
sslb3.ingress.hacking.hu/response-headers-remove: Server, X-Powered-ByNames with two spellings
The source range annotations each answer to two names. allowlist-source-range and whitelist-source-range are the same setting, as are denylist-source-range and blocklist-source-range. The second spelling of each is what ingress-nginx calls it, so a manifest moved across keeps working; the first is what I would write today.
Allow and deny are resolved together by specificity, not by list order: a rule about one host beats a rule about its network whichever list it came from, and an exact tie goes to deny. On a layer 4 service the same rules travel into the kernel as nftables rules with that ordering preserved — see the fast path.
One Ingress with all of it
The pieces above in a single object, so the shape is clear. This is more annotation than any real service needs — it is a reference, not a recommendation. A production Ingress usually carries two or three of these, and plenty carry none.
everything.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
annotations:
cert-manager.io/cluster-issuer: letsencrypt
# --- serverfarm: shared by every Ingress pointing at this service ---
sslb3.ingress.hacking.hu/load-balance: least-connections
sslb3.ingress.hacking.hu/health-check: "true"
sslb3.ingress.hacking.hu/health-check-type: http
sslb3.ingress.hacking.hu/connect-timeout: "3"
sslb3.ingress.hacking.hu/idle-timeout: "60"
sslb3.ingress.hacking.hu/max-connection-duration: "3600"
sslb3.ingress.hacking.hu/threat-detection: block
# --- route: this Ingress only ---
sslb3.ingress.hacking.hu/force-ssl-redirect: "true"
sslb3.ingress.hacking.hu/enable-access-log: "true"
sslb3.ingress.hacking.hu/allowlist-source-range: "10.0.0.0/8"
sslb3.ingress.hacking.hu/serverfarm-vhost: shop.internal
sslb3.ingress.hacking.hu/request-headers: |
X-Forwarded-Host: shop.example.com
sslb3.ingress.hacking.hu/response-headers-remove: Server
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 }Deliberately absent
Three things people look for here and do not find, with the reason in each case. These are decisions, not gaps.
Layer 4 services are not annotated at all — they are a resource of their own, with the equivalent controls as schema fields. See the L4Ingress reference.