Centinel AnalyticaCentinel Analytica
Platforms

Apache HTTP Server

Bot protection for Apache HTTP Server using a Lua module with mod_lua.

Prerequisites

  • Secret key from your dashboard
  • Apache 2.4+ with mod_lua enabled
  • Lua 5.1 libraries: lua-curl, lua-socket, lua-cjson

Installation

Download and extract the module

Download centinel-apache.zip and extract both files to your Lua module directory:

curl -O https://docs.centinelanalytica.com/downloads/centinel-apache.zip
unzip centinel-apache.zip -d /usr/local/lib/lua/

The zip contains:

  • centinel-apache.lua - core module
  • centinel-handler.lua - Apache hook entry point
Install dependencies

On Alpine Linux:

apk add apache2-lua lua5.1-curl lua5.1-socket lua5.1-cjson

On Debian/Ubuntu:

apt-get install libapache2-mod-lua lua-curl lua-socket lua-cjson
a2enmod lua
Configure httpd.conf
# Load mod_lua
LoadModule lua_module modules/mod_lua.so

# Lua package paths (adjust for your distro)
LuaPackagePath /usr/share/lua/5.1/?.lua
LuaPackagePath /usr/share/lua/5.1/?/init.lua
LuaPackageCPath /usr/lib/lua/5.1/?.so

# Centinel module path
LuaPackagePath /usr/local/lib/lua/?.lua

# Pass environment variables to Lua
PassEnv CENTINEL_SECRET_KEY
PassEnv CENTINEL_VALIDATOR_URL
PassEnv CENTINEL_DEBUG

# Persist Lua state across requests (required for connection reuse)
LuaScope server

# Centinel access checker — runs on every request
LuaHookAccessChecker /usr/local/lib/lua/centinel-handler.lua access_check early

LuaScope server is required

Without LuaScope server, Apache creates a fresh Lua state per request. The module reuses a persistent HTTP/2 connection to the validator API. Dropping that state on every request forces a new TLS handshake per validation.

Set your secret key and restart
export CENTINEL_SECRET_KEY="sk_live_your_key_here"
apachectl configtest && apachectl restart

For systemd:

[Service]
Environment="CENTINEL_SECRET_KEY=sk_live_your_key_here"

Docker

FROM alpine:3.21

RUN apk add --no-cache \
    apache2 \
    apache2-lua \
    apache2-proxy \
    lua5.1-curl \
    lua5.1-socket \
    lua5.1-cjson \
    ca-certificates

RUN mkdir -p /run/apache2

RUN wget -q -O /tmp/centinel-apache.zip \
    https://docs.centinelanalytica.com/downloads/centinel-apache.zip && \
    unzip /tmp/centinel-apache.zip -d /var/www/localhost/lua/ && \
    rm /tmp/centinel-apache.zip

COPY httpd.conf /etc/apache2/httpd.conf

EXPOSE 80
CMD ["httpd", "-D", "FOREGROUND", "-f", "/etc/apache2/httpd.conf"]
docker run -e CENTINEL_SECRET_KEY="sk_live_xxx" -p 80:80 your-image

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.

centinel.init({
    secret_key = os.getenv("CENTINEL_SECRET_KEY"),
    timeout_ms = 2000,          -- default: 2000
    connect_timeout_ms = 2000,  -- default: 2000
    fail_open = true            -- default: true (allow requests if API is down)
})

On API failure the module backs off exponentially: 1s, 2s, 4s, 8s up to 5 minutes 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 PassEnv CENTINEL_DEBUG in httpd.conf).

Check the error log for [Centinel] entries:

tail -f /var/log/apache2/error.log | grep Centinel

Replace centinel-handler.lua with your own handler to control initialization directly:

local centinel = require("centinel-apache")

local initialized = false

function access_check(r)
    if not initialized then
        centinel.init({
            secret_key = r.subprocess_env["CENTINEL_SECRET_KEY"]
                or os.getenv("CENTINEL_SECRET_KEY"),
            validator_url = os.getenv("CENTINEL_VALIDATOR_URL")
                or "https://validator.centinelanalytica.com/validate",
            timeout_ms = 2000,
            fail_open = true,
            debug = false,
            protected_paths = { "^/api/", "^/admin" },
            unprotected_paths = { "^/health$", "^/metrics$" }
        })
        initialized = true
    end

    return centinel.access_handler(r)
end

Point LuaHookAccessChecker to your custom handler file.


Configuration reference

OptionTypeDefaultDescription
secret_keystringAPI key from the dashboard. Required.
validator_urlstringhttps://validator.centinelanalytica.com/validateValidator endpoint.
timeout_msnumber2000Request timeout (ms).
connect_timeout_msnumber2000Connection 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.

Environment variables

VariableDescription
CENTINEL_SECRET_KEYAPI key. Required.
CENTINEL_VALIDATOR_URLCustom validator endpoint.
CENTINEL_DEBUGSet to true or 1 for debug logging.

All three require PassEnv directives in httpd.conf to be accessible from Lua.


Changelog

  • 1.1.0 – Switched HTTP client to lcurl for HTTP/2 support and persistent connection reuse.
  • 1.0.0 – Initial release.