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
Debian/Ubuntu users
The lua-curl package from apt does not work with mod_lua. It installs, but the shared object file that require("lcurl") needs is never created. Follow the luarocks install method in the Debian/Ubuntu tab below.
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-socket lua-cjson luarocks libcurl4-openssl-dev lua5.1-dev
a2enmod lua
luarocks install Lua-cURLv3 LUA_VERSION=5.1 CURL_INCDIR=/usr/includeWhy luarocks instead of apt for lcurl
The Debian lua-curl apt package does not install the lcurl.so shared object that mod_lua loads via require("lcurl"). luarocks builds it directly. The LUA_VERSION=5.1 flag is required because luarocks compiles against Lua 5.3/5.4 headers by default, but mod_lua uses Lua 5.1. Without it, Apache fails to load the module with undefined symbol: lua_tointeger.
# 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.
Troubleshooting
module 'lcurl' not found
The lua-curl apt package on Debian/Ubuntu installs without producing lcurl.so, so require("lcurl") fails. Install via luarocks instead:
apt-get install luarocks libcurl4-openssl-dev lua5.1-dev
luarocks install Lua-cURLv3 LUA_VERSION=5.1 CURL_INCDIR=/usr/includeundefined symbol: lua_tointeger
Full error:
error loading module 'lcurl' from file '/usr/local/lib/lua/5.1/lcurl.so':
/usr/local/lib/lua/5.1/lcurl.so: undefined symbol: lua_tointegerluarocks compiled lcurl against Lua 5.3 or 5.4 headers, but mod_lua uses Lua 5.1. Reinstall with the correct target:
luarocks remove Lua-cURLv3
luarocks install Lua-cURLv3 LUA_VERSION=5.1 CURL_INCDIR=/usr/includeModule loads but requests time out or fail
Check that LuaScope server is set in httpd.conf. Without it, Apache creates a fresh Lua state per request, forcing a new TLS handshake to the validator on every hit. That causes severe latency and request timeouts under load.
LuaScope serverFailover not triggering during an API outage
Update to v1.2.0 or later. Earlier versions stored backoff state in the Lua runtime, which Apache resets on every request. The counter never reached the threshold, so failover never engaged and requests failed instead of failing open.
Check your installed version:
grep -i version /usr/local/lib/lua/centinel-apache.luaHow to check logs
Enable debug logging and tail the Apache error log:
tail -f /var/log/apache2/error.log | grep CentinelEnable debug mode via config:
centinel.init({
secret_key = os.getenv("CENTINEL_SECRET_KEY"),
debug = true
})Or via environment variable (requires PassEnv CENTINEL_DEBUG in httpd.conf):
export CENTINEL_DEBUG=trueConfiguration 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
v1.2.0
- Fixed failover/backoff logic for Apache's per-request worker model. Previous versions stored backoff state in the Lua runtime, which reset on every request. Backoff state now persists correctly, ensuring fail-open behavior during upstream outages.
- Improved error handling and logging for connection failures.
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 – Switched HTTP client to lcurl for HTTP/2 support and persistent connection reuse.
- 1.0.0 – Initial release.