Varnish
Bot protection for Varnish Cache 6.0 LTS using a VCL config and a small helper VMOD.
Overview
Centinel integrates with Varnish through VCL. Protected requests are routed to the Centinel validator as a normal Varnish backend fetch, so Varnish's backend connection pool keeps the connection warm. The decision comes back in x-centinel-* response headers, so VCL never parses JSON. A small helper module (libvmod_centinel) base64-decodes the block or challenge page for synth delivery and appends the validator's Set-Cookie without overwriting cookies from your origin. Requests are blocked, challenged, or restarted to your origin based on the decision.
If the validator is unreachable, times out, or returns an error, requests fail open to your origin.
Backend TLS required
Open-source Varnish has no native backend TLS, and the validator is HTTPS. You must run a small local TLS proxy in front of validator.centinelanalytica.com and point the validator backend at it over plain HTTP on localhost. Reuse an nginx/HAProxy you already run, or add a ghostunnel sidecar — the install steps show both. Varnish Enterprise users can use a native TLS backend and skip the proxy. (hitch can't be used here — it only terminates inbound TLS, not the outbound TLS this leg needs.)
Prerequisites
- Secret key from your dashboard
- Varnish Cache 6.0 LTS (open source) or Varnish Enterprise 6, plus the matching
varnish-devheaders. Distro-stock Varnish 6.5 and 7.x are not supported: the VMOD is built with a strict ABI, so rebuild it after any Varnish upgrade. - Build tools:
autoconf,automake,libtool,pkg-config,python3-docutils - A local TLS proxy for the HTTPS validator (an existing nginx/HAProxy, or a ghostunnel sidecar)
- A configured origin backend
Install
Download centinel-varnish.zip and extract it:
curl -O https://docs.centinelanalytica.com/downloads/centinel-varnish.zip
unzip centinel-varnish.zip -d centinel-varnish/
cd centinel-varnish/The zip contains:
centinel.vcl— the VCL config templatesrc/— thelibvmod_centinelVMOD source (base64 decoding for block pages)autogen.sh,configure.ac,Makefile.am— autotools build files
Install the build dependencies (Debian/Ubuntu example), then build:
Debian and Ubuntu ship a newer Varnish and no varnish-dev package, so add the 6.0 LTS
repository first:
curl -s https://packagecloud.io/install/repositories/varnishcache/varnish60lts/script.deb.sh \
| sudo bash
sudo apt-get install -y varnish varnish-dev \
build-essential automake autoconf libtool pkg-config python3-docutils
sh autogen.sh
./configure --disable-dependency-tracking
make
sudo make installmake install puts libvmod_centinel.so into Varnish's vmod directory.
Why --disable-dependency-tracking
The module includes the vmodtool-generated header, which doesn't exist when configure's dependency bootstrap runs. The flag defers that to the build step.
Open-source Varnish can't speak TLS to a backend, so a local proxy accepts plain HTTP on 127.0.0.1:8443 and originates TLS (with SNI) to the validator. All three options keep the validator connection warm under load; pick by what's already on the host.
If the host already runs nginx (or HAProxy), add a TLS-originating proxy — no extra daemon, and the most resource-efficient option in our benchmark. The keepalive + proxy_ssl_session_reuse + Connection "" lines keep the upstream TLS connection warm. The proxy_ssl_verify + proxy_ssl_name lines are not optional: without them nginx sends the upstream group name as SNI and accepts any certificate, so an on-path attacker could impersonate the validator and capture your API key:
# nginx.conf
upstream centinel_validator { server validator.centinelanalytica.com:443; keepalive 16; }
server {
listen 127.0.0.1:8443;
location / {
proxy_pass https://centinel_validator;
proxy_ssl_server_name on; # SNI
proxy_ssl_name validator.centinelanalytica.com; # SNI defaults to the upstream name, not the host
proxy_ssl_verify on; # verify the validator's cert
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_ssl_verify_depth 3;
proxy_ssl_session_reuse on;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host validator.centinelanalytica.com;
}
}HAProxy equivalent: a backend with server centinel validator.centinelanalytica.com:443 ssl sni str(validator.centinelanalytica.com) verify required ca-file /etc/ssl/certs/ca-certificates.crt plus http-reuse always.
ghostunnel is a maintained single-binary TLS proxy (Prometheus metrics, cert hot-reload, graceful restart):
ghostunnel client \
--listen 127.0.0.1:8443 \
--target validator.centinelanalytica.com:443 \
--disable-authentication # present no client cert; the server cert is still verifiedThe connection stays warm as long as Varnish keeps its (pooled) front-side connection open.
stunnel still works (it just uses the most CPU of the options we measured):
# /etc/stunnel/centinel.conf
foreground = no
[centinel]
client = yes
accept = 127.0.0.1:8443
connect = validator.centinelanalytica.com:443
sni = validator.centinelanalytica.com
verifyChain = yes # sni alone does not verify the cert
checkHost = validator.centinelanalytica.com
CAfile = /etc/ssl/certs/ca-certificates.crtstunnel /etc/stunnel/centinel.confEdit the bundled centinel.vcl:
- Point the
originbackend at your app. - Point the
validatorbackend at the TLS proxy (127.0.0.1:8443). - Set your secret key in
vcl_backend_fetch(set bereq.http.X-API-Key = "...";).
backend origin { .host = "127.0.0.1"; .port = "8080"; }
backend validator {
.host = "127.0.0.1";
.port = "8443";
.max_connections = 200;
.connect_timeout = 3s;
.first_byte_timeout = 5s;
.between_bytes_timeout = 2s;
}Keep the three timeouts. Varnish defaults first_byte_timeout to 60s, so without them a hung
validator stalls each client request for a minute instead of failing open in five seconds.
varnishd -f /etc/varnish/centinel.vcl -a :80 -s malloc,256m \
-p workspace_client=256k -p http_resp_hdr_len=96kOr on a running instance:
varnishadm vcl.load centinel /etc/varnish/centinel.vcl && varnishadm vcl.use centinelConfigure
How it works
On every request, the VCL:
- Bypasses static assets (configurable regex in
vcl_recv) and lets Varnish cache them normally. - Reads the decision from
x-centinel-*response headers.centinel.b64decode()decodes the block or challenge page. - Applies the decision:
allow— restart to your originblock— 403 plus the block pageredirect— the challenge page
- Forwards the validator's first
Set-Cookieand a whitelisted set of response headers (Content-Type,Cache-Control,X-Content-Type-Options,X-Frame-Options,Content-Security-Policy,Referrer-Policy).
Protected responses are not cached
Allowed and fail-open requests pass to your origin after validation. Block and redirect responses use a synthetic response and do not reach your origin. Only the static-asset bypass path is cached as usual. Each protected request also costs one internal restart. Plan capacity before you put Centinel in front of an HTML cache.
Allow only GET and HEAD through the validator
The downloadable centinel.vcl validates every non-static method. The validator hop restarts the request, which is not safe after Varnish streams a request body. Before the validator code in vcl_recv, add this gate:
if (req.method != "GET" && req.method != "HEAD") {
set req.backend_hint = origin;
return (pass);
}This sends POST, PUT, PATCH, and other methods to your origin without validation.
Pure VCL cannot modify a response body, so this integration does not inject the browser script. Add the script tag to your HTML yourself, or use Fastly Compute if you want it injected for you.
Configuration reference
| Setting | Where | Default |
|---|---|---|
| Secret key | vcl_backend_fetch → X-API-Key | — (required) |
| Origin backend | backend origin | your app |
| Validator backend | backend validator | TLS proxy → validator |
| Protected paths | vcl_recv static-asset regex | static assets bypass |
| Block/challenge page | from the validator (x-centinel-response-html-b64) | served via synth |
Keep-alive & tuning
- Keep the validator connection warm. Varnish pools the localhost hop to the proxy automatically (
.max_connectionson thevalidatorbackend). The proxy must keep its upstream TLS connection warm so the handshake is paid once, not per request — nginx does this withkeepalive+proxy_ssl_session_reuse, HAProxy withhttp-reuse always, and the stunnel/ghostunnel tunnels stay warm as long as Varnish keeps its front-side connection pooled. - Low-traffic sites. Varnish closes idle pooled backends after
backend_idle_timeout(default 60s). Raise it (e.g.-p backend_idle_timeout=300) so a quiet period doesn't force a fresh TLS handshake on the next request. - Large block/challenge pages. The page is delivered in a response header (
x-centinel-response-html-b64), so a large page can exceed Varnish's header limit, which defaults to 8k. Exceeding it fails the fetch and the request is allowed through, so raise-p http_resp_hdr_len(and-p workspace_clientfor the decoded page) as shown in the launch command above. The validator also caps the HTML it sends at 64k and substitutes a short generic page beyond that. - Cookie attributes are fixed. The VCL forwards the first cookie only, with
Path=/; HttpOnly; SameSite=Lax; Max-Age=86400, and does not setSecure. The validator's own attributes are not preserved on this platform.
Verify
Confirm the VCL loaded, watch the validator hop, then send a request that should be denied:
varnishadm vcl.list
varnishlog -g request -q 'ReqURL eq "/validate"'
curl -s -o /dev/null -w '%{http_code}\n' -A 'python-requests/2.31' http://localhost/If the VMOD will not load, it was built against a different Varnish version; rebuild it. If
everything is allowed, check that the validator backend is healthy and that
http_resp_hdr_len is large enough for your block page, since an oversized header fails the fetch
and the request is let through.
Changelog
- v1.0.1 — TLS-proxy and keep-alive guidance
- v1.0.0 — Initial release