⚠️ Status: untested. This extension is provided as-is and has not been tested in production. Please feel free to fork, modify, improve, and open pull requests.Licensed under GNU GPLv3 (see LICENSE).
Two integration approaches are provided — pick one:
- OpenResty / Lua (
openresty-lua/) — richest, useslua-resty-http+ shared-dict cache. - Stock nginx
auth_request(auth_request/) — no Lua; delegates to a small FastCGI checker.
Blocks requests at the edge by consulting the ip-block.com
decision API from OpenResty's access phase, with a per-IP decision cache in a
lua_shared_dict.
- Tested with: OpenResty 1.31.1.1 (latest stable, June 2026), which bundles
ngx_http_lua_module+ LuaJIT. - Dependency:
lua-resty-http0.17.x (resty.http).
| File | Purpose |
|---|---|
ip_block.lua |
The module. Contains init(opts) and access(opts). |
nginx.conf |
Complete, commented example configuration. |
-
Install OpenResty and
lua-resty-http:# lua-resty-http via the OpenResty package manager opm get ledgetech/lua-resty-http # or via LuaRocks: luarocks install lua-resty-http
-
Copy
ip_block.luasomewhere on your Lua package path, e.g./etc/openresty/ip-block/ip_block.lua. -
In your
http {}block add (seenginx.conffor the full version):lua_package_path "/etc/openresty/ip-block/?.lua;;"; lua_shared_dict ip_block_cache 10m; lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; resolver 1.1.1.1 8.8.8.8 ipv6=off; init_by_lua_block { require("ip_block").init({ site_id = "your-site-id", api_key = "your-api-key", }) }
-
In the
server {}orlocation {}you want protected:access_by_lua_block { require("ip_block").access() }
-
Reload:
openresty -s reload(ornginx -s reload).
Pass options to init() (global) or access({...}) (per-location override).
| Option | Default | Meaning |
|---|---|---|
enabled |
true |
Master on/off switch. |
site_id |
"" |
Your ip-block.com site id. |
api_key |
"" |
Your API key (sent in the JSON body). |
api_url |
https://api.ip-block.com/v1/check |
Decision endpoint. |
fail_open |
true |
On any error/timeout/non-2xx, allow the request. Set false to fail closed. |
timeout_ms |
1000 |
Per-request timeout (connect+send+read). |
cache_ttl |
300 |
Seconds to cache a per-IP decision (0 disables caching). |
cache_name |
ip_block_cache |
Name of the lua_shared_dict. |
behind_proxy |
false |
If true, read the client IP from real_ip_header. |
real_ip_header |
X-Forwarded-For |
Header to read the client IP from. Left-most entry is used. |
block_action |
403 |
403 (plain deny) or redirect. |
block_redirect_url |
https://www.ip-block.com/blocked.php |
Used when block_action=redirect. |
block_message |
Access denied. |
Body text for the 403. |
whitelist |
{} |
Array of IPs that are never checked. |
You can source secrets from the environment (os.getenv) as shown in nginx.conf.
Make sure the variables are visible to the nginx master process (e.g. via
env IPBLOCK_API_KEY; in the main config and your service manager).
- Only
{"action":"block"}blocks. Anything else (including malformed or missingaction) is treated per thefail_openpolicy. - Errors are not cached, so a transient API outage does not pin an IP; the next
request retries. Successful
allow/blockdecisions are cached forcache_ttl. - Whitelisted IPs short-circuit before any cache or API call.
ssl_verify = truerequireslua_ssl_trusted_certificateto point at a valid CA bundle, and a workingresolver.- The cache is shared across all worker processes of a single nginx instance. For a fleet of nginx nodes, each node keeps its own cache (that is fine — decisions are idempotent and short-lived).
An alternative to the OpenResty/Lua module that works with stock nginx — no Lua
at the proxy layer. It uses the built-in
ngx_http_auth_request_module
to run an internal subrequest against a small checker for every request.
- Tested with: nginx 1.28 (stable) / 1.29 (mainline). The
auth_requestmodule is included in the official nginx.org packages (build flag--with-http_auth_request_module; verify withnginx -V). - Checker runtime: PHP 8 via php-fpm (
checker.php). Any HTTP endpoint that returns 2xx = allow / 403 = block can be substituted.
| File | Purpose |
|---|---|
nginx.conf |
auth_request wiring + internal /_ip_block_check location. |
checker.php |
FastCGI checker: calls the API, caches per-IP, returns 204/403. |
client --> nginx (location /)
| auth_request /_ip_block_check (internal GET subrequest)
v
checker.php --POST /v1/check--> api.ip-block.com
| 204 allow / 403 block
v
allow: serve content | block: 403 -> optional redirect to blocked.php
auth_request drops the request body and method on the subrequest, so nginx
forwards the real client IP, User-Agent and Referer to the checker as FastCGI
params. 2xx from the checker allows the request; 401/403 denies it.
- Confirm your nginx has the module:
nginx -V 2>&1 | grep -o with-http_auth_request_module. - Copy
checker.phpto e.g./etc/nginx/ip-block/checker.phpand make sure the php-fpm pool can read it. - Merge the
upstream,location /,@ip_block_deniedand= /_ip_block_checkblocks fromnginx.confinto your config. Pointfastcgi_passat your php-fpm socket/port andSCRIPT_FILENAMEat the checker's real path. - Set your
IPB_SITE_ID/IPB_API_KEY(and other params) in the= /_ip_block_checklocation, or export them to php-fpm as environment vars. nginx -t && nginx -s reload.
| Param | Default | Meaning |
|---|---|---|
IPB_SITE_ID |
— | Your site id. |
IPB_API_KEY |
— | API key (sent in JSON body). |
IPB_API_URL |
https://api.ip-block.com/v1/check |
Decision endpoint. |
IPB_FAIL_OPEN |
1 |
1 = allow on error/timeout; 0 = fail closed. |
IPB_TIMEOUT_MS |
1000 |
API timeout. |
IPB_CACHE_TTL |
300 |
Per-IP cache seconds (0 disables). |
IPB_CACHE_DIR |
system temp /ip-block-cache |
Disk cache location. |
IPB_WHITELIST |
— | Comma-separated IPs never checked. |
IPB_CLIENT_IP |
$remote_addr |
Real client IP (set from realip in nginx). |
IPB_USER_AGENT / IPB_REFERRER |
— | Forwarded from the client request. |
If nginx is behind a CDN/load-balancer, enable the realip module in http{} so
$remote_addr becomes the true client address:
set_real_ip_from 10.0.0.0/8; # your trusted proxy range(s)
real_ip_header X-Forwarded-For;
real_ip_recursive on;- Only
{"action":"block"}blocks; everything else allows (subject tofail_open). - Errors are not cached; the next request retries the API.
- Whitelisted IPs short-circuit before the cache and API call.
checker.php uses a simple on-disk cache (one file per site+IP). For higher volume
swap the two file_get_contents/file_put_contents cache calls for APCu, Redis, or
memcached. The OpenResty variant in ../openresty-lua/ uses an in-memory
lua_shared_dict and is faster if you can run OpenResty.