Nginx / OpenResty
Bot protection for Nginx and OpenResty using a Lua module.
Prerequisites
- Secret key from your dashboard
- OpenResty 1.19+ or nginx with lua-nginx-module
Installation
curl -o /usr/local/openresty/site/lualib/centinel-nginx.lua \
https://docs.centinelanalytica.com/downloads/centinel-nginx.luaInstall the lua-resty-http dependency:
opm get ledgetech/lua-resty-httpNo opm? Install manually:
mkdir -p /usr/local/openresty/site/lualib/resty
curl -o /usr/local/openresty/site/lualib/resty/http.lua \
https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http.lua
curl -o /usr/local/openresty/site/lualib/resty/http_headers.lua \
https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http_headers.lua
curl -o /usr/local/openresty/site/lualib/resty/http_connect.lua \
https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http_connect.lua# At the top, before events {} — required for os.getenv() to work in OpenResty
env CENTINEL_SECRET_KEY;
env CENTINEL_VALIDATOR_URL;
events {
worker_connections 1024;
}
http {
lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
lua_ssl_verify_depth 3;
resolver 8.8.8.8 valid=30s ipv6=off;
lua_shared_dict centinel_cache 10m;
lua_package_path '/usr/local/openresty/site/lualib/?.lua;;';
init_by_lua_block {
centinel = require("centinel-nginx")
centinel.init({
secret_key = os.getenv("CENTINEL_SECRET_KEY"),
validator_url = os.getenv("CENTINEL_VALIDATOR_URL")
or "https://validator.centinelanalytica.com/validate",
timeout_ms = 1000,
connect_timeout_ms = 500
})
}
server {
listen 80;
location / {
access_by_lua_block { centinel.access_handler() }
proxy_pass http://your-backend;
}
}
}Do not use return as your content directive
return 200 runs in the rewrite phase, before access_by_lua_block. Centinel will never execute. Use proxy_pass or content_by_lua_block instead.
export CENTINEL_SECRET_KEY="sk_live_your_key_here"
nginx -t && nginx -s reloadFor systemd:
[Service]
Environment="CENTINEL_SECRET_KEY=sk_live_your_key_here"Docker
FROM openresty/openresty:alpine
RUN apk add --no-cache ca-certificates
RUN mkdir -p /usr/local/openresty/site/lualib/resty && \
wget -q -O /usr/local/openresty/site/lualib/resty/http.lua \
https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http.lua && \
wget -q -O /usr/local/openresty/site/lualib/resty/http_headers.lua \
https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http_headers.lua && \
wget -q -O /usr/local/openresty/site/lualib/resty/http_connect.lua \
https://raw.githubusercontent.com/ledgetech/lua-resty-http/master/lib/resty/http_connect.lua
RUN wget -q -O /usr/local/openresty/site/lualib/centinel-nginx.lua \
https://docs.centinelanalytica.com/downloads/centinel-nginx.lua
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
EXPOSE 80
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]docker run -e CENTINEL_SECRET_KEY="sk_live_xxx" -p 80:80 your-imageIn a Docker Compose network, set resolver 127.0.0.11 valid=30s ipv6=off; instead of 8.8.8.8. Docker's embedded DNS at 127.0.0.11 handles both container names and external hostnames.
Advanced configuration
All paths are protected by default except static assets. Override in centinel.init():
centinel.init({
secret_key = os.getenv("CENTINEL_SECRET_KEY"),
protected_paths = { "^/api/", "^/admin" }, -- empty = protect all
unprotected_paths = { "^/health$", "^/metrics$" }
})Excluded by default: images (.gif, .ico, .jpg, .jpeg, .png, .svg, .webp, .avif, .bmp), fonts (.eot, .otf, .ttf, .woff, .woff2), styles (.css, .less), scripts (.js, .map), media (.mp3, .mp4, .webm, .wav, .flac, and others), archives (.gz, .zip), and .json, .xml.
Uses nginx's native proxy to call the validator. Connections to the upstream are pooled and reused, so each request skips the TLS handshake overhead.
Add to the http {} block:
upstream centinel_validator {
server validator.centinelanalytica.com:443;
keepalive 16;
keepalive_requests 1000;
keepalive_timeout 60s;
}Add to the server {} block:
set $centinel_h2_enabled "1";
location = /_centinel_validate {
internal;
rewrite_by_lua_block {
ngx.req.set_header("x-api-key", ngx.ctx.centinel_api_key or "")
ngx.req.set_header("x-origin-module", "nginx")
ngx.req.set_header("x-origin-version", ngx.ctx.centinel_version or "")
}
proxy_pass https://centinel_validator/validate;
proxy_http_version 1.1;
proxy_set_header Host "validator.centinelanalytica.com";
proxy_set_header Content-Type "application/json";
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_name validator.centinelanalytica.com;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_ssl_session_reuse on;
proxy_connect_timeout 1s;
proxy_send_timeout 2s;
proxy_read_timeout 2s;
}When $centinel_h2_enabled is set, the module uses this location. Without it, it falls back to lua-resty-http.
centinel.init({
secret_key = os.getenv("CENTINEL_SECRET_KEY"),
timeout_ms = 1000, -- default: 100 (increase if not using the keepalive pool)
connect_timeout_ms = 500, -- default: 100
fail_open = true -- default: true (allow requests if API is down)
})On API failure the module backs off exponentially: 1s → 2s → 4s → 8s → 5min max. Requests pass through during backoff.
centinel.init({
secret_key = os.getenv("CENTINEL_SECRET_KEY"),
debug = true
})Or set CENTINEL_DEBUG=true as an environment variable (requires env CENTINEL_DEBUG; in nginx.conf).
Check the error log for [Centinel] entries:
tail -f /var/log/nginx/error.log | grep CentinelConfiguration reference
| Option | Type | Default | Description |
|---|---|---|---|
secret_key | string | — | API key from the dashboard. Required. |
validator_url | string | https://validator.centinelanalytica.com/validate | Validator endpoint. |
timeout_ms | number | 100 | Request timeout (ms). |
connect_timeout_ms | number | 100 | Connection timeout (ms). |
fail_open | boolean | true | Allow requests when API is unreachable. |
ssl_verify | boolean | true | Verify SSL certificates. |
debug | boolean | false | Verbose logging. |
log_enabled | boolean | true | Enable all logging. |
protected_paths | table | {} | Lua patterns to protect. Empty = protect all. |
unprotected_paths | table | [static assets] | Lua patterns to skip. |
Changelog
v1.1.1
- Response headers from
/validateare now applied to all outgoing responses (block, redirect, and allow). Headers likeContent-Typeare no longer hardcoded — they come from the validator. - Validator API requests now include a
User-Agentheader identifying the integration name and version.
1.1.0 - Updated cookie format, response handling, and nginx proxy path; fixed OpenResty environment variable access.
1.0.0
- Initial release.