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_luaenabled - Lua 5.1 libraries:
lua-curl,lua-socket,lua-cjson
Installation
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 modulecentinel-handler.lua- Apache hook entry point
On Alpine Linux:
apk add apache2-lua lua5.1-curl lua5.1-socket lua5.1-cjsonOn Debian/Ubuntu:
apt-get install libapache2-mod-lua lua-curl lua-socket lua-cjson
a2enmod lua# 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 earlyLuaScope 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.
export CENTINEL_SECRET_KEY="sk_live_your_key_here"
apachectl configtest && apachectl restartFor 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-imageAdvanced 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 CentinelReplace 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)
endPoint LuaHookAccessChecker to your custom handler file.
Configuration 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 | 2000 | Request timeout (ms). |
connect_timeout_ms | number | 2000 | 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. |
Environment variables
| Variable | Description |
|---|---|
CENTINEL_SECRET_KEY | API key. Required. |
CENTINEL_VALIDATOR_URL | Custom validator endpoint. |
CENTINEL_DEBUG | Set 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.