# Apache HTTP Server

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

Source: https://docs.centinelanalytica.com/platforms/web-server/apache

## Overview

Centinel protects Apache HTTP Server using a Lua module loaded via `mod_lua`. An access checker hook intercepts every request, validates it against the Centinel API, and enforces the returned decision before your application logic runs.

## Prerequisites

* Secret key from your [dashboard](https://docs.centinelanalytica.com/install/dashboard.md)
* Apache 2.4+ with `mod_lua` enabled
* 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.

## Install

1. **Download and extract the module**

   Download [centinel-apache.zip](https://docs.centinelanalytica.com/downloads/centinel-apache.zip) and extract both files to your Lua module directory:

   ```bash
   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

2. **Install dependencies**

   On Alpine Linux:

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

   On Debian/Ubuntu:

   ```bash
   apt-get install apache2 lua-socket lua-cjson luarocks libcurl4-openssl-dev lua5.1-dev
   a2enmod lua
   luarocks install Lua-cURLv3 LUA_VERSION=5.1 CURL_INCDIR=/usr/include
   ```

   > **Why 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`.

3. **Configure httpd.conf**

   ```apache
   # 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.

4. **Set your secret key and restart**

   ```bash
   export CENTINEL_SECRET_KEY="sk_live_your_key_here"
   apachectl configtest && apachectl restart
   ```

   For systemd:

   ```ini
   [Service]
   Environment="CENTINEL_SECRET_KEY=sk_live_your_key_here"
   ```

### Docker

```dockerfile
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 /usr/local/lib/lua/ && \
    rm /tmp/centinel-apache.zip

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

EXPOSE 80
CMD ["httpd", "-D", "FOREGROUND", "-f", "/etc/apache2/httpd.conf"]
```

```bash
docker run -e CENTINEL_SECRET_KEY="sk_live_xxx" -p 80:80 your-image
```

## Configure

**Path protection**

All paths are protected by default except static assets. Override in `centinel.init()`:

```lua
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`.

**Timeouts**

```lua
centinel.init({
    secret_key = os.getenv("CENTINEL_SECRET_KEY"),
    timeout_ms = 500,          -- default: 500
    connect_timeout_ms = 300,  -- default: 300
})
```

> **This module always fails open:** If the validator is unreachable, slow, or returns an error, the request is allowed through. There is no fail-closed mode and no configurable backoff. If you need requests denied during a validator outage, use a different integration.

**Debug**

```lua
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:

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

**Custom handler**

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

```lua
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 = 500,
            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

| Option               | Type    | Default                                            | Description                                                                                      |
| -------------------- | ------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `secret_key`         | string  | —                                                  | API key from the [dashboard](https://docs.centinelanalytica.com/install/dashboard.md). Required. |
| `validator_url`      | string  | `https://validator.centinelanalytica.com/validate` | Validator endpoint.                                                                              |
| `timeout_ms`         | number  | `500`                                              | Request timeout (ms).                                                                            |
| `connect_timeout_ms` | number  | `300`                                              | Connection timeout (ms).                                                                         |
| `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.

## Verify

Reload Apache, then send a request that should be denied:

```bash
curl -s -o /dev/null -w '%{http_code}\n' -A 'python-requests/2.31' http://localhost/api/test
tail -f /var/log/apache2/error.log | grep Centinel
```

A `403` means enforcement is live. A `200` with nothing in the error log usually means the module
never loaded, or a Lua dependency is missing. See Troubleshooting below.

## 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:

```bash
apt-get install luarocks libcurl4-openssl-dev lua5.1-dev
luarocks install Lua-cURLv3 LUA_VERSION=5.1 CURL_INCDIR=/usr/include
```

### `undefined symbol: lua_tointeger`

Full error:

```text
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_tointeger
```

luarocks compiled `lcurl` against Lua 5.3 or 5.4 headers, but `mod_lua` uses Lua 5.1. Reinstall with the correct target:

```bash
luarocks remove Lua-cURLv3
luarocks install Lua-cURLv3 LUA_VERSION=5.1 CURL_INCDIR=/usr/include
```

### Module 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.

```apache
LuaScope server
```

### Protection appears to do nothing

The module loads its dependencies defensively, so a missing Lua library disables protection rather
than erroring. Both failures are silent in normal operation:

* Without `lcurl`, no validator call is made and every request is allowed. Look for `[Centinel] HTTP backend` in the error log.
* Without `mime` (from luasocket), base64 decoding always fails, so every interstitial challenge turns into an allow. Blocks still serve a generic fallback page.

Check the module loaded and both libraries resolved:

```bash
grep -i version /usr/local/lib/lua/centinel-apache.lua
tail -f /var/log/apache2/error.log | grep Centinel
```

### Only one Set-Cookie header is sent

`mod_lua` can set a response header but not append to it, so the module emits at most one
`Set-Cookie` and logs the rest as dropped. If a challenge needs more than one cookie, this platform
cannot deliver it.

## Changelog

* **v1.2.0** — Failover backoff fix
* **v1.1.1** — Response header passthrough
* **v1.1.0** — HTTP/2 via lcurl
* **v1.0.0** — Initial release
