Configuration reference

Every setting the configuration file understands, what it accepts, and the value it takes when you leave it out.

An INI-style file of named sections. The configuration guide covers how the pieces fit together and is the better place to start; this page is the exhaustive list to come back to.

A setting that is present in the file can be overridden by an environment variable, which is what makes a container image configurable without rewriting the file inside it. The name is SSLB3_, then the section and the key joined by an underscore and upper-cased — SSLB3_SYSTEM_MAXCONNECTIONS, SSLB3_THREAT_MODE. The key has to be in the file already: an override replaces a value, it cannot add a setting the file does not mention. More on overriding.

[System]

Process-wide settings. One of these sections.

SettingAcceptsUnsetWhat it does

User

a user name

empty

Drop to this user after binding. Empty stays as started.

Group

a group name

empty

Drop to this group after binding.

Daemon

true, false

false

Fork into the background. Leave off under systemd, which wants the process in the foreground.

PidFile

a path

empty

Where to write the process id. Empty writes none.

CommunicationModule

tcp

tcp

The transport module used for connections.

Worker

threadpool

threadpool

The worker model.

Threads

integer

0

Worker threads. Zero means one per available core.

StartSSLB

true, false

true

Read so that an SSLBv2 configuration still parses, and otherwise unused: setting it to false does not stop SSLB3 starting.

MaxConnections

integer

0

Concurrent connections across the process. Zero derives a ceiling from the open file limit — which SSLB3 raises to its hard limit at startup — and logs what it settled on, roughly half the descriptors because a proxied connection costs two. Unlimited was never a policy, only a promise the kernel breaks at a time of its choosing by failing an accept and every health check at once.

MaxAuthConcurrency

integer

0

How many basic-auth password verifications may run at once; zero picks half the cores. Verifying is meant to be expensive and a wrong password is never cached, so without a cap a handful of guesses can starve the proxying. Requests past the cap get 503, not 401 — busy is not the same as wrong.

WorkerBacklog

integer

128

Accept queue depth per worker.

ShutdownTimeout

seconds

30

How long established clients get to finish once shutdown starts.

HeaderTimeout

seconds

30

How long a client has to finish sending its request head; zero disables it. The Slowloris bound — a connection that sends half a header line and stops holds a socket, a connection slot and a worker slot, and pays nothing for it. IdleTimeout does not catch it: a connection dribbling a byte a minute is never idle. Head only, so a slow body is left to the backend.

KeepAliveInterval

seconds

30

How often an idle HTTP/2 client connection is pinged, and how long the answer is waited for; zero disables the pings. An h2 connection can sit with no streams open, so nothing notices a peer that has gone — a closed laptop, a NAT that forgot the flow — and TCP will not say so for hours.

TcpKeepalive

seconds

900

Turns on SO_KEEPALIVE for accepted sockets and for connections to backends, and how long one may be idle before the first probe; zero turns it off. The layer below KeepAliveInterval: a peer whose machine vanished closes nothing, and the kernel calls a dead socket healthy until something is written to it.

KeepHeaderNameCase

true, false

true

Forward HTTP/1.1 header names to the backend as the client wrote them. The parser lower-cases everything internally, which is correct by the RFC and wrong for a backend comparing names literally. HTTP/1.1 only — h2 and h3 define field names as lower case on the wire. What a Lua script sees is unaffected.

RouteHeader

true, false

true

Add X-SSLB3-Route to every response served by the HTTP module, saying which path decided where the request went: native, lua or default. On by default because a route quietly falling off the native path is otherwise invisible. Turn it off where telling clients how the balancer is configured is unwanted.

MaxAffinitySessions

integer

100000

How many cookie affinity sessions may be held at once across every farm. A client that discards cookies mints a new session per request, so the store needs a ceiling as well as an expiry. Reaching it evicts the least recently used.

SessionStoreFile

a path

empty

Where to keep affinity sessions across a restart. Empty disables it. Only a clean shutdown writes the file — a SIGKILL leaves the last one and clients are redistributed, deliberately, so that affinity costs no I/O while serving.

[Log]

Where records go and how much is written. Access logging is off globally and turned on per route with the enable-access-log annotation, or per listener in the file.

SettingAcceptsUnsetWhat it does

LogName

a name

SSLB

The name records are written under.

DebugLevel

0–5

3

How much is written. Higher is more.

LogDir

a path

empty

Directory for log files. Empty logs to standard output, which is what a container wants. The file is opened as root and handed to System.User, so rotate with SIGUSR1 and have the rotator create the replacement as that user — or use copytruncate, which needs no signal. A log path that is a symlink, a hard link or anything but a regular file is refused, since that first open happens as root in a directory System.User may own.

UseSyslog

true, false

false

Send records to syslog instead.

LogSocket

a path

empty

The syslog socket, when the default is not right.

RemoteLogging

true, false

false

Send records to a remote collector.

SyncLogfile

true, false

false

Flush every record to disk as it is written. Safer across a crash, slower in the ordinary case.

AccessLog

true, false

false

Write an access record per request. Off globally; also settable per route by annotation.

AccessLogFormat

text, json, combined

text

How a record is rendered. combined is the Apache/nginx format most log tooling already parses.

AccessLogLevel

0–5

3

The level access records are emitted at. Matches the default DebugLevel, so turning access logging on is enough to see it.

[Listener-name]

One section per bound socket, named after the hyphen — [Listener-https] is a listener called https. As many as you need.

SettingAcceptsUnsetWhat it does

ListenPort

1–65535

0

The port to bind. Required — a listener without one binds nothing.

BindAddress

an address

::

The address to bind. The default is every interface, both families.

BindIPv4

true, false

true

Whether to bind the IPv4 family.

BindIPv6

true, false

true

Whether to bind the IPv6 family.

BindAddressIPv4

an address

empty

A distinct IPv4 address, where one socket per family is wanted.

BindAddressIPv6

an address

empty

The same for IPv6.

Protocol

tcp, tls, udp, uds, quic

tcp

What this listener speaks. tls terminates; quic serves HTTP/3; uds listens on a Unix socket.

Serverfarm

a farm name

unset

The farm this listener sends to, when it is not decided by matching.

Layer7Module

http, lua

unset

What parses the traffic. Unset copies bytes without parsing, which is what makes a service eligible for the kernel fast path.

CertificateFile

a path

empty

The certificate chain, for a TLS or QUIC listener.

PrivateKeyFile

a path

empty

The matching private key.

PrivateKeyPassword

a password

unset

For an encrypted private key.

RedirectToTLS

true, false

false

Answer everything on this listener with a redirect to https. The usual shape of a plaintext listener that exists only to send people to the encrypted one.

Advertise

true, false

true

Whether responses over TCP invite clients to this QUIC listener at all. On by default, because HTTP/3 is reached only by following Alt-Svc and a listener nobody is told about receives nothing. Turn it off to run HTTP/3 without advertising it — how you bring it up behind a load balancer and prove it with a client aimed straight at the address before any browser can find it — and to stop HTTP/3 later without dropping the connections already on it, which removing the listener would.

AdvertisePort

1–65535

0

The port named in Alt-Svc for HTTP/3, when it differs from the bound one.

AdvertiseHost

a hostname

empty

The host named in Alt-Svc, when HTTP/3 is not reached on the same name as HTTP/1.1 and HTTP/2 — behind an AWS NLB it cannot be, since a QUIC listener sits on neither a dualstack load balancer nor one carrying a security group. This says where the service is found, not what it is called: the client still sends the origin in SNI and :authority and still wants a certificate for the origin, so this name needs neither a certificate nor a route, only an address record.

AdvertiseMaxAge

seconds

86400

How long clients may remember that answer. Worth lowering while HTTP/3 is being rolled out: a client that has cached the alternative keeps trying it for this long, falling back to TCP each time.

ConnectionIdFormat

random, quic-lb

random

The shape of the connection IDs this listener issues. Random says nothing about this instance and is right wherever nothing in front reads them. quic-lb encodes a server ID, in the layout of draft-ietf-quic-load-balancers, so a load balancer that has lost the address it hashed on — because the client moved — can still find the instance holding the connection.

ServerId

0x and 16 hex digits

empty

The server ID the load balancer allocated this instance. It has to match what the balancer registered, or every packet after the handshake is dropped.

ServerIdEnvironment

a variable name

empty

Where to read that server ID from instead, for a deployment that hands it over at startup because it differs per instance. The AWS Load Balancer Controller injects AWS_LBC_QUIC_SERVER_ID.

ServerIdEncoding

hex, base64

hex

How that value is written. AWS documents hex; its controller injects base64.

ConnectionIdLength

2–20

17

Total connection ID length for a quic-lb listener. 17 is the only length an AWS NLB accepts — it drops any other without a word. With ConnectionMigration on it also has to leave room for the worker index and a nonce after the server ID: the server ID length plus four, at least.

ConnectionMigration

true, false

false

Keep an HTTP/3 connection when the client changes network. Linux only. HTTP/3 gets its throughput from one socket per worker sharing a port, and the kernel picks between them by hashing the address, so a client that moves lands on a worker that has never seen its connection. This attaches a socket filter that steers by the connection ID instead, which is why it needs ConnectionIdFormat = quic-lb. It settles the question inside one instance only: where several sit behind a load balancer, the balancer has to route by connection ID too. The worker index takes two bytes of the connection ID, so up to 65536 workers; an instance with more sockets than that refuses to start rather than steering to the wrong one. Turning it off also tells QUIC that migration is refused, so a connection stays on the path it was established on instead of being accepted onto one nothing can serve.

StatelessResetKey

any string

empty

A key every instance shares, so one can tell a client another one’s connection is gone. Only consulted while ConnectionMigration is off — which is exactly when a moved client is expected to arrive somewhere that has never heard of it: another worker, or another instance behind a balancer that cannot route on the connection ID. QUIC’s answer there is the stateless reset, the one message an endpoint with no state can send, and a client believes it only if the token matches the one issued with the connection. That token is an HMAC of the connection ID under this key, so an instance can speak for another’s connections precisely when both hold the same key. Left empty, each endpoint generates its own: right for a single process and wrong for a group, because every reset is then rejected as unauthenticated and the client waits out the idle timeout instead — a minute of a dead connection before it falls back to TCP. The string is hashed, so length and alphabet do not matter, and nothing about serving depends on it: a wrong or missing key costs a slow failure, never a wrong answer.

StatelessResetKeyEnvironment

a variable name

empty

Where to read that key from instead, which is the better way to supply it wherever the configuration file is less protected than a secret — in Kubernetes it always is, since sslb.conf lives in a ConfigMap. Takes precedence when both are set. A variable that is named and not set is a startup error rather than a quiet fall back to a random key: a shared key nobody notices is missing is the case this exists to prevent.

ForwardedPort

1–65535

0

The port backends are told in X-Forwarded-Port, when clients reach this listener on a different one than it binds. 0 means the bound port, or AdvertisePort on a QUIC listener. A PROXY protocol header outranks it.

HandshakeTimeout

seconds

10

How long a TLS handshake may take before it is abandoned.

AllowSourceRange

CIDRs, comma separated

empty

Addresses admitted. Empty admits anything not denied.

DenySourceRange

CIDRs, comma separated

empty

Addresses refused. Resolved against the allow list by specificity, with an exact tie going to deny.

ProxyProtocol

v2, no

no

Expect a PROXY protocol v2 header on every connection, so the real client address survives a load balancer in front. Version 2 only. Mandatory when set, never sniffed: a listener either always expects the header or never reads it, since deciding per connection would let the sender choose which rules apply to it. A listener that reads it never believes X-Forwarded-For.

ProxyProtocolFrom

CIDRs

empty

Which senders may speak it, checked against the real socket address before anything is read. Required whenever ProxyProtocol is set — "believe anyone" has to be typed out as 0.0.0.0/0, ::/0 rather than arrived at by leaving it empty, because a PROXY header from an unchecked sender is an address-spoofing primitive.

TrustXForwardedForFrom

CIDRs

empty

Which senders may set X-Forwarded-For and be believed. Empty trusts nobody, and an untrusted chain is replaced rather than forwarded. Ignored on a listener that reads PROXY protocol: a layer 4 proxy passes the client’s bytes through untouched, so an X-Forwarded-For arriving that way was written by the client.

MaxDatagram

bytes

4096

Largest UDP datagram accepted.

MaxSessions

integer

8192

Concurrent UDP sessions tracked for this listener.

Socket

a path

empty

Serve on a Unix socket instead of a port.

listeners.conf

[Listener-http]
ListenPort = 8080
Serverfarm = web
RedirectToTLS = true

[Listener-https]
ListenPort = 8443
Protocol = tls
CertificateFile = /etc/sslb3/tls/fullchain.pem
PrivateKeyFile = /etc/sslb3/tls/privkey.pem
Serverfarm = web
Layer7Module = http

[Listener-postgres]
ListenPort = 15432
Serverfarm = postgres
AllowSourceRange = 10.0.0.0/8

HTTP/3 behind a load balancer

The settings, in short. HTTP/3 behind an AWS network load balancer is the whole deployment: why QUIC needs a load balancer of its own, what the connection ID carries, and what the kernel-assisted steering costs.

A QUIC connection is identified by its connection ID rather than by the address it came from — that is what lets a client change network and keep it. A load balancer in front has the same problem in reverse: the address it hashed on has changed, so it has to read the connection ID to find the instance holding the connection. It can only do that if the instance puts something recognisable in there, which is what ConnectionIdFormat = quic-lb does.

h3-behind-an-nlb.conf

[Listener-h3]
Protocol = quic
ListenPort = 8443
AdvertisePort = 443
# HTTP/3 lives on its own name here, because an AWS NLB will not carry a QUIC
# listener if it is dualstack or has a security group.
AdvertiseHost = h3.example.com
AdvertiseMaxAge = 300
# 1 byte of config rotation, the 8-byte server ID, then a nonce: 17 in total,
# which is the only length an NLB accepts.
ConnectionIdFormat = quic-lb
ServerIdEnvironment = AWS_LBC_QUIC_SERVER_ID
ServerIdEncoding = base64
# Linux: steer datagrams to the worker that owns the connection, so a client
# that changes network keeps it instead of reconnecting.
ConnectionMigration = yes

The two halves are separate, and either is useful alone. ConnectionIdFormat settles which instance a moved client reaches, and is what a load balancer in front needs; ConnectionMigration settles which worker inside that instance, and is what the sockets sharing a port need. Turning on only the second one still breaks the moment there is more than one replica, which is why it is off by default.

And when it is off, plan for the client that moves anyway — a laptop changing network, a NAT rebinding. It arrives at a worker or an instance holding no state for its connection, and the only thing an endpoint with no state can send is QUIC's stateless reset. A client believes one only if its token matches the one issued with the connection, and that token is an HMAC of the connection ID under StatelessResetKey — so instances can speak for each other's connections exactly when they share it. Without a shared key every reset is rejected as unauthenticated and the client waits out the idle timeout instead, which is a minute of a dead connection before it falls back to TCP. It fixes nothing about routing; it turns a stall into an immediate fallback.

Bring a setup like this up unadvertised. Advertise = no runs the listener while no response carries Alt-Svc, so no browser can find it — and a client aimed at an address directly needs no invitation, so curl --http3-only --connect-to origin:443:h3-host:443 https://origin/ still exercises the whole path, load balancer included. Advertise once that works. The same setting is the way to stop HTTP/3 afterwards without dropping the connections already on it, which removing the listener would.

[Serverfarm]

The defaults every farm inherits. One of these sections, and every setting in it can be overridden per farm below.

SettingAcceptsUnsetWhat it does

Balancer

round-robin, weighted, least-connections, lua

round-robin

How a backend is chosen. There are four and no others; an unknown name fails the parse.

HealthCheck…

every health check setting

see Health checks

The health check every farm and real server inherits unless it sets its own. All ten settings are listed under Health checks below.

Layer7Module

http, lua

empty

What parses the traffic for farms that do not say.

Selector

default, match, lua

default

How a farm is chosen for a request. match is the native host/path routing; lua hands the decision to a script.

ThreatDetection

off, detect, block

empty

What the intrusion detector does. Empty follows the balancer-wide [Threat] Mode.

ImpersonationEnforcement

true, false

true

Whether the impersonation signal refuses as well as reports.

ConnectTimeout

seconds

10

How long to wait dialling a backend.

ResponseTimeout

seconds

60

How long to wait for a backend to start answering an HTTP request; zero waits forever, and exceeding it answers 504. The failure ConnectTimeout cannot see: a backend that accepts the connection and then never replies — a full thread pool, a stuck query — looks healthy to a connect timeout while holding the request forever. Counted from when the request body has finished sending, so a slow upload is never cut off by it — IdleTimeout is what bounds the upload itself. Upgrades are exempt, since a WebSocket is meant to sit quiet.

IdleTimeout

seconds

0

How long a connection may be silent. Zero disables. Also how long a request body may make no progress before the request is abandoned with a 408 — reset by every chunk that arrives, so an upload that is still moving is never cut off. Zero does not switch that part off; it falls back to a minute. A long poll that is legitimately quiet is a different thing from a half-sent request, which holds a worker slot, a connection permit and a backend connection for as long as the client keeps the socket open.

MaxConnectionDuration

seconds

0

How long a connection may last at all. Zero disables. The one worth setting: an idle timeout does nothing against a connection trickling a byte to look busy.

MaxIdleConnections

integer

256

Backend connections kept open for reuse, per farm and per worker.

FailedBackendRetries

integer

3

How many further backends to try when the chosen one refuses a connection. Each one that failed is excluded from the next choice, so this is failover rather than retrying the same server.

FailedBackendRetryInterval

seconds

1

Delay between those attempts. Zero tries the next backend immediately.

SessionAffinity

none, cookie

none

Whether a returning client goes back to the backend that served it.

SessionCookieName

a cookie name

SSLB3_AFFINITY

The affinity cookie.

SessionTimeout

seconds

3600

How long a session survives unused.

ForwardedHeaders

true, false

true

Add X-Forwarded-For and its companions to backend requests.

RealServers

integer

unset

A count, and only a check: every [RealServer] section present is used whether or not this is set, but when it is set and disagrees with the number of sections, startup fails rather than ignoring the mismatch.

[Serverfarm-name]

One per farm. Anything left out here falls back to [Serverfarm] above — that is the whole point of the split, so a farm section usually holds two or three lines rather than a copy of the defaults.

The Match* settings are the exception: they exist only here, because they are what distinguishes one farm from another. They are how routing happens natively in Rust rather than through a script.

SettingAcceptsUnsetWhat it does

MatchHost

host names, comma separated

empty

Hosts this farm answers for. The core of native routing.

MatchPath

path prefixes, comma separated

empty

Path prefixes, combined with MatchHost by AND. Unset matches any path for those hosts.

MatchScheme

http, https

empty

Restrict to requests that arrived one way. Matches how the request reached this balancer, not X-Forwarded-Proto — a header a client can set is not something to route on.

AllowSourceRange

CIDRs, comma separated

empty

Client addresses admitted to the requests MatchHost claims. Empty admits anything not denied. Checked per request against the resolved client — a PROXY header or trusted X-Forwarded-For — and refused with 403 before a backend is involved. Per route, where a listener’s AllowSourceRange is per connection.

DenySourceRange

CIDRs, comma separated

empty

Client addresses refused from those requests. The more specific rule wins either way, and a tie goes to deny.

MatchListener

listener names

empty

Only requests arriving on these listeners.

MatchSNI

server names

empty

Only connections whose TLS handshake asked for these names.

MatchAddress

local addresses

empty

Only requests that arrived on these local addresses. How one balancer serves a different pool per address, as a web server does.

Balancer

as above

inherits

Overrides [Serverfarm] for this farm.

HealthCheck…

every health check setting

inherits

Overrides [Serverfarm] for every member of this farm that does not set its own. See Health checks below.

Layer7Module

http, lua

inherits

Overrides [Serverfarm] for this farm.

ThreatDetection

off, detect, block

inherits

Overrides [Serverfarm] for this farm.

ImpersonationEnforcement

true, false

inherits

Overrides [Serverfarm] for this farm.

ConnectTimeout

seconds

inherits

Overrides [Serverfarm] for this farm.

ResponseTimeout

seconds

inherits

Overrides [Serverfarm] for this farm.

IdleTimeout

seconds

inherits

Overrides [Serverfarm] for this farm.

MaxConnectionDuration

seconds

inherits

Overrides [Serverfarm] for this farm.

SessionAffinity

none, cookie

inherits

Overrides [Serverfarm] for this farm.

SessionCookieName

a cookie name

inherits

Overrides [Serverfarm] for this farm.

SessionTimeout

seconds

inherits

Overrides [Serverfarm] for this farm.

farms.conf

[Serverfarm]
Balancer = round-robin
HealthCheck = true
ConnectTimeout = 10

[Serverfarm-web]
MatchHost = shop.example.com, www.shop.example.com
Layer7Module = http

[Serverfarm-api]
MatchHost = shop.example.com
MatchPath = /api/
Balancer = least-connections
Layer7Module = http

[Serverfarm-postgres]
Balancer = least-connections
HealthCheckType = tcp

Both farms above answer for the same host; the one with MatchPath is more specific, so /api/orders goes to the API farm and everything else to the web farm. Host and path are combined by AND.

[RealServer-n]

A backend. One section each, numbered — [RealServer-1], [RealServer-2] — and assigned to a farm by name. The suffix has to be a number: it keeps the order of the sections meaningful, and a section named [RealServer-web1] stops the configuration loading rather than being quietly skipped. Two sections with the same number are refused too.

SettingAcceptsUnsetWhat it does

IPAddress

an address

empty

Where the backend is. Required.

Port

1–65535

0

Its port. Required.

Serverfarm

a farm name

default

Which farm it belongs to. A backend belongs to exactly one.

Protocol

tcp, tls

tcp

How to reach it. tls speaks TLS to the backend. Only these two — a UDP listener reaches its backends without one being declared here.

Weight

integer

1

Relative share of traffic, proportioned by the weighted balancer; round-robin and least-connections ignore the size of it. Zero is special and honoured everywhere: it drains the backend, taking it out of rotation under every balancer and withholding it from the kernel fast path, without removing it from the configuration.

HealthCheck…

every health check setting

inherits

This server’s own health check, overriding its farm setting by setting. The most specific level — see Health checks below.

SNI

a server name

unset

The name presented in the handshake to this backend, and the name an https health check presents too.

VerifyCertificate

true, false

true

Check the backend's certificate. Set false for an internal certificate nothing trusts.

CertificateFile

a path

unset

A client certificate to present to this backend.

backends.conf

[RealServer-1]
IPAddress = 10.0.1.11
Port = 8080
Serverfarm = web
Weight = 2

[RealServer-2]
IPAddress = 10.0.1.12
Port = 8080
Serverfarm = web

# Drained: configured, but taking no traffic. Weight zero is honoured
# by every balancer, and such a backend is not handed to the kernel
# fast path either.
[RealServer-3]
IPAddress = 10.0.1.13
Port = 8080
Serverfarm = web
Weight = 0

Health checks

A health check belongs to a real server. Every setting below can be written in three places, and the most specific one wins — setting by setting, not section by section:

[RealServer-name]

this server only

[Serverfarm-name]

every member of that farm that does not say otherwise

[Serverfarm]

every farm that does not say otherwise

So a pool whose members are all alike says it once on the farm, and a member that differs — a different port, a different path — overrides just that and keeps the rest. Checks run on their own schedule, never on the request path, so a request never waits for one. The Unset column below is what applies when no level sets a value.

SettingAcceptsUnsetWhat it does

HealthCheck

true, false

true

Whether to check at all. A server that is not checked is always considered healthy.

HealthCheckType

tcp, http, https, smtp, lua, noop

tcp

tcp only connects. http and https send a GET and compare the status with HealthCheckStatus. smtp expects a 220 greeting, sends QUIT and expects 221. lua runs a script against the open connection. noop passes without testing anything.

HealthCheckPort

1–65535

the server’s Port

Where the check connects. Members listening on different ports need nothing extra; this is for a separate management port.

HealthCheckPath

a path

/

What an http or https check requests. Must start with /.

HealthCheckHost

a host name

empty

The Host header sent. Empty sends the address being checked, never an empty header. An https check presents the server’s SNI in its handshake, then this host, then the address.

HealthCheckStatus

codes and ranges

200-399

Status codes that pass an http or https check, comma separated: 200,401 or 200-399. The default is any success or redirect.

HealthCheckInterval

seconds

10

How often each server is checked. At least 1. A lua check still running when the next tick comes is not started twice, and that tick does not count as a failure — a check that is slow but right should not take its own server out of rotation. It warns instead; the fix is a longer interval or a shorter timeout.

HealthCheckTimeout

seconds

3

How long one check may take before it counts as failed. For a lua check it bounds the whole check, every read and write included, rather than each call — a server trickling a byte just inside a per-call deadline cannot keep the check going.

HealthCheckFailTolerance

1–255

3

Consecutive failures before a server is taken out of rotation. A success resets the count.

HealthCheckScript

a script name

[LUA] HealthChecker

The script a lua check runs, relative to ScriptDirectory. Configuration only — the admin API cannot name one, since that would let the key holder choose which file on the balancer’s disk runs.

health.conf

[Serverfarm]
# Every farm checks /health over http unless it says otherwise.
HealthCheckType = http
HealthCheckPath = /health

[Serverfarm-api]
# This farm's application answers its health path with 401
# when unauthenticated, and that is healthy.
HealthCheckPath = /actuator/health/liveness
HealthCheckStatus = 200,401

# Two members of one farm on different ports. Each is checked on
# its own port, with nothing extra configured.
[RealServer-11]
IPAddress = 127.0.0.1
Port = 8080
Serverfarm = api

[RealServer-12]
IPAddress = 127.0.0.1
Port = 8081
Serverfarm = api

# A member whose health endpoint is on a management port.
[RealServer-13]
IPAddress = 10.0.1.13
Port = 8080
Serverfarm = api
HealthCheckPort = 9090

[AdminAPI]

The configuration API. TLS-only and authenticated, always — it changes configuration, so there is no plaintext mode and no anonymous one. This is what the ingress controller talks to.

SettingAcceptsUnsetWhat it does

Enabled

true, false

false

Whether the API listens. The ingress controller needs it.

BindAddress

an address

127.0.0.1

Loopback by default, unlike the metrics and stats listeners — this one changes configuration, so it should be reached deliberately.

ListenPort

1–65535

6443

The port.

AuthType

basic, apikey

basic

How callers authenticate. There is no anonymous mode.

ApiKey

a key

empty

The key, for apikey auth. Preferred over a username and password.

Username

a name

sslb

For basic auth.

Password

a password

empty

For basic auth.

CertificateFile

a path

empty

The API is TLS-only, so this is required for it to start.

PrivateKeyFile

a path

empty

The matching key.

PrivateKeyPassword

a password

unset

For an encrypted key.

ProbePort

1–65535

0

A plain HTTP port serving only /healthz and /readyz, for a health checker that cannot speak to the TLS loopback listener above — a Kubernetes kubelet, most obviously, which probes the pod address. Zero disables it. Nothing but those two handlers is mounted, so it exposes no configuration and needs no credential. /healthz says the process is alive; /readyz answers 503 until there is a configuration to serve — backends in the file, or a first write through the API, an empty one included. A PROXY protocol v2 header in front of a probe is stepped over, because an AWS NLB with PROXY protocol on its target group sends one on its health checks too.

BindIPv4 / BindIPv6

true, false

true

Which families to bind.

[Threat]

Intrusion detection and prevention. Every rate below is a count over Window seconds from a single source address, and every one of them is zero by default, meaning "do not watch this" — a rate that is abusive for one deployment is ordinary for the next, so there is no default I could pick that would be right for yours.

SettingAcceptsUnsetWhat it does

Mode

off, detect, block

off

block refuses, detect counts and reports without refusing anyone, off does not look.

Window

seconds

10

The window every rate below is counted over.

Interval

milliseconds

500

How often detectors flush and the aggregator decides — also the worst-case delay between crossing a threshold and being refused.

BlockDuration

seconds

300

How long a source stays blocked, refreshed while it keeps offending. It has to expire: a permanent list eventually blackholes a whole CGNAT range on the strength of one bad client behind it.

MaxTrackedSources

integer

65536

Sources counted at once. Beyond this, addresses not already tracked are ignored.

ConnectionRate

integer

0

Connections per window from one address. The only signal needing no request, so the one that works in front of a protocol SSLB3 does not parse. Inert on a listener reading PROXY protocol: it is counted at accept, where the proxy is the only address there is.

RequestRate

integer

0

Requests per window from one address. Needs the HTTP module.

NotFoundRate

integer

0

404s per window. A source producing these at a rate is enumerating paths.

DeniedRate

integer

0

401s and 403s per window. At a rate, that is credential stuffing.

MalformedRate

integer

0

Requests per window that were not valid HTTP. The strongest signal and the cheapest, because the parser has already judged. A browser does not send malformed HTTP. Worth a far lower threshold than the rest.

ProbeRate

integer

0

Requests per window for something no legitimate client asks a frontend for. /.env is a request for your credentials and /.git/config for your source history; neither has an innocent reading, so 1 is the sensible setting. Only the path is inspected, never a payload. .well-known is excluded, ACME living there.

ImpersonationRate

integer

0

Requests per window from a client that claims a browser in its User-Agent and then sends neither Sec-Fetch-* nor Accept-Language, one of which every current browser sends. The headers accuse; the TLS handshake only grades how hard it lands and can never accuse on its own, because GREASE says “browser” when it is present and says nothing when it is absent — Firefox sends none. That rule arrived in 0.8.1; on anything older, set this to 0, which skips the check entirely rather than just raising the bar.

HoneypotRate

integer

0

Requests per window for HoneypotPath. One is conclusive, so 1 is the sensible setting.

HoneypotPath

a path

empty

A path nothing legitimate would ever request.

HoneypotRobots

true, false

false

Publish the honeypot in a robots.txt of SSLB3’s own, answered only to clients that reached the balancer by address rather than by name — no SNI, and a Host that is an address or absent. That is a scanner sweeping a range, and no real visit looks like it; a client that reads the file and then fetches the one path it was told to avoid has convicted itself. Gated on naming no host rather than on which serverfarm was reached, because the default farm carries real traffic in a standalone deployment and a route with the wrong ingress class falls through to it. The file names the honeypot and allows everything else. Off by default: it discloses the path to anyone probing the address.

BlocklistSources

true, false

true

Whether a source that crosses a threshold is blocked as a source. Turn off where traffic arrives SNATed and an address stands for a crowd.

EnforceAtConnection

true, false

true

Whether a blocked source is refused at accept rather than per request. Same caveat behind a SNATing ingress.

ExemptSourceRange

CIDRs

empty

Addresses never blocked — monitoring, health checkers, your own offices.

threat.conf

[Threat]
# Count and report, refuse nobody. Where every rollout should start.
Mode = detect
Window = 10
BlockDuration = 300

# One request for /.env or /.git/config has no innocent reading,
# so one is the threshold.
ProbeRate = 1

# A browser does not send malformed HTTP. Low threshold, high confidence.
MalformedRate = 5

# Path enumeration and credential stuffing.
NotFoundRate = 40
DeniedRate = 20

[LUA]

The scripts, and the limits they run under. Every hook is optional and unset by default — Lua is available, not required, and a deployment that never names a script never loads one.

SettingAcceptsUnsetWhat it does

ScriptDirectory

a path

etc/scripts

Where scripts are loaded from.

Request

a script name

unset

Runs per request, for per-request logic.

Session

a script name

unset

Owns a connection outright, for a protocol you want to speak yourself.

Selector

a script name

unset

Chooses the farm for a request, when Selector = lua.

Balancer

a script name

unset

Chooses the backend, when Balancer = lua.

HealthChecker

a script name

unset

The script for every lua health check that names no HealthCheckScript of its own.

Startup

a script name

unset

Runs once as the balancer starts.

Shutdown

a script name

unset

Runs once as it stops.

InstructionLimit

integer

10000000

Instructions a script may execute before it is stopped — the guard against a script that never returns.

MemoryLimit

bytes

0

Memory ceiling for the Lua state. Zero is unlimited. The released binaries are built against LuaJIT, which allocates outside any ceiling I could set — a non-zero value warns once at startup and does nothing, so bound scripts with InstructionLimit and UserDataMaxEntries instead.

Sandbox

true, false

true

Remove io, os, package, ffi, require, dofile, loadfile and load from every script. On by default since 0.10.0 — it was off before. Every bundled script runs under it, and what a script used io and os for has a sandboxed replacement: the file library for its own data, time for clocks. A script that still reaches for a removed global fails with an error naming it.

DataDirectory

a path

unset

The one directory the file library may read and write. Unset, no script has it. Only the Startup and Shutdown scripts and a request script’s admin handler get it — never anything on the request or connection path. It takes file names, not paths, refuses symbolic links, and writes through a rename so a crash leaves the old content or the new.

DataFileMaxBytes

bytes

67108864

The largest file the file library reads or writes.

UserDataMaxEntries

integer

100000

Entries in the shared store scripts use to keep state between requests. Zero is unbounded, and the API gateway requires it: a store that evicts would drop a contract.

UserDataTTL

seconds

0

How long an entry survives. Zero never expires.

What a script can call, and what each hook receives, is the Lua interface reference.

[ApiGateway]

Read by api_gateway.lua, and only when it is the request script — the section means nothing to any other role. Every setting is optional, and a value the gateway does not understand stops it starting, with the setting named. How they fit together is on the API gateway page.

SettingAcceptsUnsetWhat it does

RequestValidation

off, detect, enforce

enforce

The request check: the operation exists and allows the method, every parameter, body presence and media type.

RequestBodyValidation

off, detect, enforce

enforce

The request body against its schema.

ResponseValidation

off, detect, enforce

detect

The response check: a declared status, a declared media type, required and valid headers. Detect by default, because enforcing turns a backend drifting from its contract into an outage.

ResponseBodyValidation

off, detect, enforce

detect

The response body against its schema.

RequestFailureStatus

400–499

400

A request that fails its contract.

NotFoundStatus

400–499

404

A request no contract or operation describes.

MethodNotAllowedStatus

400–499

405

A path the contract has, with a method it does not. Sent with Allow.

UnsupportedMediaTypeStatus

400–499

415

A Content-Type the operation does not accept, a compressed body that would be validated, or a charset other than UTF-8.

PayloadTooLargeStatus

400–499

413

A body larger than MaxBodyBytes, in enforce mode.

ResponseFailureStatus

500–599

502

What replaces a response that fails an enforced check — with a body that says only that, never what the backend sent.

OverloadStatus

500–599

503

A body arriving while the worker already holds MaxHeldBytes.

MaxBodyBytes

bytes

1048576

The largest body validated, either direction. Sized against the instruction limit: a 1 MiB body costs about 42% of the default budget.

MaxHeldBytes

bytes

67108864

Everything one worker holds at once, across every body it is validating.

FormatAssertion

yes, no

yes

Whether format is checked — date-time, email, uuid, int64 and the rest — or only annotates.

AllowBacktrackingPatterns

yes, no

no

Patterns run on a linear-time engine. One needing lookaround or a backreference is refused at upload unless this lets it run on a backtracking engine instead.

BacktrackingMaxInput

characters

1024

The longest value a backtracking pattern is ever run against — what keeps one from being a denial of service.

ErrorDetail

yes, no

yes

Whether a refused request is told where and why. Never the value, and never for responses.

MaxErrors

1–100

10

How many problems one refusal lists.

UnmatchedServerfarm

a farm name

unset

Where a request no contract describes goes, unchecked. Unset, it is answered NotFoundStatus. This is how a gateway goes in front of an existing estate one API at a time.

StatsInterval

seconds

5

How often each worker publishes its metrics — to the Prometheus endpoint, the statistics page and /api/v1/script/stats. Publishing is once per interval, never per request.

Persist

yes, no

yes

Write every accepted change through to LUA.DataDirectory, so the startup script can restore it.

AccessLog

yes, no

no

An access record per request.

[Prometheus]

The metrics endpoint: its own listener, off until you turn it on. See metrics and the statistics page.

SettingAcceptsUnsetWhat it does

Enabled

true, false

false

Whether the endpoint listens. Off because it is unauthenticated and describes your whole deployment.

ListenPort

1–65535

9101

The port. 9090 is Prometheus itself; exporters conventionally sit in the 9100+ range.

BindAddress

an address

::

Every interface, because a scraper is on another machine by definition.

Path

a path

/metrics

Where the metrics are served.

BindIPv4 / BindIPv6

true, false

true

Which families to bind.

[Stats]

The live statistics page, off on the same terms and for the same reason.

SettingAcceptsUnsetWhat it does

Enabled

true, false

false

Whether the page is served. Off for the same reason as the metrics.

ListenPort

1–65535

9102

The port, next to the metrics endpoint.

BindAddress

an address

::

Every interface, for the same reason.

Path

a path

/

Where the page is served. The WebSocket sits directly beneath it, so /stats puts it at /stats/ws.

RefreshInterval

seconds

2

Seconds between pushes. The whole cost of the feature: one pass over the backend list per interval per connected browser.

BindIPv4 / BindIPv6

true, false

true

Which families to bind.

observability.conf

[Prometheus]
Enabled = true
ListenPort = 9101
Path = /metrics

[Stats]
Enabled = true
ListenPort = 9102
Path = /
RefreshInterval = 2

Both bind every interface when enabled, because a scraper or a browser is on another machine by definition and a loopback default would start, log that it is listening, and never be reachable. Both are unauthenticated and describe your whole deployment, so put them somewhere deliberate.

[FastPath]

Whether the kernel may forward eligible layer 4 services instead of SSLB3 copying their bytes. How eligibility is decided is its own page.

SettingAcceptsUnsetWhat it does

Mode

userspace, auto, kernel

auto

userspace serves everything and programs nothing. auto hands over what can be handed over, and a service staying behind is ordinary. kernel is the same but reports a service that should have moved and did not as a fault.

Backend

auto, nftables, ipvs

auto

Which facility to use. auto asks the host what it can actually do rather than assuming.

Table

a name

sslb3

The nftables table created for these rules.

HelperSocket

a path

/run/sslb3/netlink-helper.sock

Where the privileged netlink helper listens. The helper is a separate process with no Lua and no listener — it is the only component needing CAP_NET_ADMIN.

SourceNat

true, false

true

Rewrite the source address on forwarded connections. Correct nearly everywhere; set false only on a network that can route replies back without it, DSR-style. Without a rewrite and without such a network, connections hang.

fastpath.conf

[FastPath]
# auto: hand over what can be handed over, and a service staying
# behind is ordinary rather than a fault.
Mode = auto
Backend = auto

# Set false only where the network can route replies back without it --
# DSR-style. Leaving it on is correct nearly everywhere.
SourceNat = true

Kubernetes deployments set most of this through the Helm chart rather than by editing a file. The install page covers the values, and the annotation reference covers what an individual Ingress can override.