Centinel AnalyticaCentinel Analytica
Platforms

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

Install the module
curl -o /usr/local/openresty/site/lualib/centinel-nginx.lua \
  https://docs.centinelanalytica.com/downloads/centinel-nginx.lua

Install the lua-resty-http dependency:

opm get ledgetech/lua-resty-http

No 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
Configure nginx.conf
# 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.

Set your secret key and reload
export CENTINEL_SECRET_KEY="sk_live_your_key_here"
nginx -t && nginx -s reload

For 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-image

In 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 Centinel

Configuration reference

OptionTypeDefaultDescription
secret_keystringAPI key from the dashboard. Required.
validator_urlstringhttps://validator.centinelanalytica.com/validateValidator endpoint.
timeout_msnumber100Request timeout (ms).
connect_timeout_msnumber100Connection timeout (ms).
fail_openbooleantrueAllow requests when API is unreachable.
ssl_verifybooleantrueVerify SSL certificates.
debugbooleanfalseVerbose logging.
log_enabledbooleantrueEnable all logging.
protected_pathstable{}Lua patterns to protect. Empty = protect all.
unprotected_pathstable[static assets]Lua patterns to skip.

Changelog

v1.1.1

  • Response headers from /validate are now applied to all outgoing responses (block, redirect, and allow). Headers like Content-Type are no longer hardcoded — they come from the validator.
  • Validator API requests now include a User-Agent header 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.

On this page