ipset-access: Zero-latency IP blocking using Linux kernel ipsets (v2)
Requires the Pro plan (or higher) of the GetPageSpeed NGINX Extras subscription.
Installation
You can install this module in any RHEL-based distribution, including, but not limited to:
- RedHat Enterprise Linux 7, 8, 9 and 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 and Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-ipset-access
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-ipset-access
Enable the module by adding the following at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_http_ipset_access.so;
This document describes nginx-module-ipset-access v2.0.9 released on Dec 20 2025.
Enterprise-grade IP-based access control for NGINX using Linux ipset. Block threats, rate-limit abusers, challenge bots, and protect your infrastructure.
β οΈ Commercial Software
This is a closed-source premium module available exclusively through the GetPageSpeed Repository.
β¨ Features
Core Features
| Feature | Description |
|---|---|
| Whitelist/Blacklist | Allow or deny based on ipset membership |
| Multiple ipsets | Check against multiple ipsets in one directive |
| Live updates | Modify ipsets without reloading NGINX |
| Custom status codes | Return any HTTP status when blocking |
Performance Features
| Feature | Description |
|---|---|
| Per-thread sessions | Thread-local libipset sessions eliminate lock contention |
| LRU Cache | Shared memory cache with configurable TTL |
| Cache hit rates | Typically 95%+ hit rate reduces kernel calls |
Security Features
| Feature | Description |
|---|---|
| Rate limiting | Limit requests per IP with configurable windows |
| Auto-ban | Automatically blacklist rate limit violators |
| JS Challenge | Proof-of-work challenge stops automated bots |
| Honeypot traps | Auto-blacklist IPs hitting trap URLs |
| Entry timeout | Auto-expire blacklist entries |
Operational Features
| Feature | Description |
|---|---|
| Dry-run mode | Test configuration without blocking |
| Fail-open/close | Control behavior on ipset errors |
| Prometheus metrics | Native /metrics endpoint for Grafana |
| JSON stats | Detailed statistics API |
| NGINX variables | $ipset_result and $ipset_matched_set |
| ## |
π Quick Start
1. Create ipsets
## Create a blacklist
sudo ipset create bad_guys hash:ip timeout 86400
## Create a rate-limit ban list
sudo ipset create ratelimited hash:ip timeout 1800
## Create a honeypot trap list
sudo ipset create honeypot hash:ip timeout 86400
2. Configure NGINX
load_module modules/ngx_http_ipset_access_module.so;
http {
server {
listen 80;
# Block known bad IPs
ipset_blacklist bad_guys;
# Rate limit: 100 requests per minute
ipset_ratelimit rate=100 window=60s autoban=ratelimited;
# Your content
location / {
root /var/www/html;
}
# Honeypot trap - returns 404 by default
location /wp-admin.php {
ipset_autoadd honeypot timeout=86400;
}
# Metrics endpoint
location /metrics {
ipset_metrics;
allow 127.0.0.1;
deny all;
}
}
}
3. Test and reload
sudo nginx -t && sudo nginx -s reload
π¦ Installation
This module is available exclusively through the GetPageSpeed Premium Repository.
Step 1: Subscribe to GetPageSpeed Repository
Visit GetPageSpeed Repository Subscription to get access.
Step 2: Install the Repository
## RHEL/CentOS/Rocky/Alma Linux 8+
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
Step 3: Install the Module
sudo dnf install nginx-module-ipset-access
Step 4: Enable the Module
Add to /etc/nginx/nginx.conf before any http {} blocks:
load_module modules/ngx_http_ipset_access_module.so;
Step 5: Reload NGINX
sudo nginx -t && sudo systemctl reload nginx
π Configuration Reference
Access Control
ipset_blacklist set1 [set2 ...]
Context: http, server
Default: β
Blocks requests if the client IP appears in any of the listed ipsets. Multiple ipsets are checked in order until a match is found.
## Single set
ipset_blacklist bad_guys;
## Multiple sets (OR logic - blocked if in ANY set)
ipset_blacklist spammers hackers tor_exits;
## Disable
ipset_blacklist off;
ipset_whitelist set1 [set2 ...]
Context: http, server
Default: β
Allows requests only if the client IP appears in at least one of the listed ipsets. All other IPs are rejected.
## Only allow trusted IPs
ipset_whitelist trusted_partners office_ips;
Important: Whitelisted IPs bypass all module restrictions including:
- Rate limiting (ipset_ratelimit)
- JavaScript challenges (ipset_challenge)
This is useful for admin IPs that shouldn't be subject to rate limits or challenges:
## Admin IPs bypass rate limiting and challenges
ipset_whitelist admin_ips;
ipset_ratelimit rate=100 window=1m autoban=ratelimited ban_time=1800;
ipset_challenge on;
ipset_status code
Context: http, server
Default: 403
HTTP status code returned when a request is blocked.
ipset_status 403; # Forbidden (default)
ipset_status 444; # Close connection without response (NGINX special)
ipset_status 429; # Too Many Requests
ipset_status 503; # Service Unavailable
Caching & Performance
ipset_cache_ttl time
Context: http, server
Default: 60s
How long to cache ipset lookup results. Cached results avoid repeated kernel calls for the same IP.
ipset_cache_ttl 30s; # 30 seconds
ipset_cache_ttl 5m; # 5 minutes
ipset_cache_ttl 1h; # 1 hour
Debugging Note: If you remove an IP from an ipset but the module still reports it as "matched", this is due to caching. The cached result will expire after the configured TTL. For immediate effect during testing, you can temporarily set ipset_cache_ttl 0; to disable caching (not recommended for production due to performance impact).
Performance Impact:
- Higher TTL = Better performance, but slower to reflect ipset changes
- Lower TTL = More responsive to ipset changes, but more kernel calls
- Recommended: 30s to 5m for most use cases
ipset_fail_open on|off
Context: http, server
Default: off
Controls behavior when an ipset lookup fails (e.g., set doesn't exist).
ipset_fail_open off; # Deny on error (secure, default)
ipset_fail_open on; # Allow on error (available but risky)
ipset_dryrun on|off
Context: http, server
Default: off
When enabled, logs what would be blocked but doesn't actually block. Perfect for testing new rules in production.
ipset_dryrun on; # Log but don't block
Check logs for messages like:
ipset: DRYRUN would block 1.2.3.4 (matched: bad_guys)
Important: When using $ipset_result and $ipset_matched_set variables with dryrun mode, these values reflect the point-in-time state when the request was processedβnot the current state of the ipset. If you check the ipset manually later and don't find the IP, possible reasons include:
- Timeout expiration: The IP was added with a timeout (e.g.,
timeout=86400) and has since expired - Cache lag: The module caches lookup results (default 60s). An entry removed from the ipset may still show as "matched" until the cache expires
- Manual removal: Someone or something (fail2ban, scripts) removed the entry
This is expected behaviorβdryrun shows you exactly what production would see at request time.
Rate Limiting
ipset_ratelimit parameters
Context: http, server
Default: β
Limits requests per IP within a time window. Can automatically add violators to an ipset.
Parameters:
| Parameter | Required | Description |
|---|---|---|
rate=N |
Yes | Maximum requests per window |
window=TIME |
No | Time window (default: 60s) |
autoban=SET |
No | ipset to add violators |
ban_time=N |
No | Seconds until auto-expire (default: 3600) |
Examples:
## Basic: 100 requests per minute
ipset_ratelimit rate=100;
## With custom window: 1000 requests per hour
ipset_ratelimit rate=1000 window=1h;
## With auto-ban: Add violators to ipset for 30 minutes
ipset_ratelimit rate=60 window=1m autoban=ratelimited ban_time=1800;
## Strict API protection
ipset_ratelimit rate=10 window=1s autoban=api_abusers ban_time=3600;
How it works:
1. Each IP gets a request counter and window start time
2. Counter increments on each request
3. When window expires, counter resets
4. If counter exceeds rate, returns 429 Too Many Requests
5. If autoban is set, IP is added to specified ipset
Note: Rate limit state is stored in shared memory and survives worker restarts.
JavaScript Challenge
ipset_challenge on|off
Context: http, server
Default: off
Enables JavaScript challenge mode. Browsers must solve a proof-of-work puzzle to access the site. Effective against automated bots and scrapers.
ipset_challenge on;
How it works:
1. First request receives a challenge page (HTTP 503)
2. Browser executes JavaScript that solves a hash puzzle
3. Solution is stored in a cookie (_ipset_verified)
4. Subsequent requests with valid cookie pass through
5. Cookie expires after 24 hours
ipset_challenge_difficulty level
Context: http, server
Default: 2
Controls challenge difficulty (1-8). Higher = longer solve time.
| Level | Approximate Solve Time |
|---|---|
| 1 | ~100ms |
| 2 | ~500ms (default) |
| 3 | ~1 second |
| 4 | ~2 seconds |
| 5 | ~5 seconds |
| 6+ | ~10+ seconds |
ipset_challenge on;
ipset_challenge_difficulty 3; # ~1 second solve time
Challenge Page Features: - Modern, responsive design - Animated loading spinner - Progress feedback - Auto-redirect on success - No external dependencies
Honeypot Auto-add
ipset_autoadd setname [timeout=seconds] [status=code]
Context: server, location
Default: β
Automatically adds client IP to specified ipset when the location is accessed and returns an HTTP status code. Perfect for honeypot traps.
Parameters:
| Parameter | Required | Description |
|---|---|---|
| setname | Yes | Target ipset name |
timeout=N |
No | Entry timeout in seconds |
status=N |
No | HTTP status code to return (default: 404) |
Examples:
## Basic: Add to honeypot set and return 404 (default)
location /config.php {
ipset_autoadd honeypot;
}
## With timeout: Auto-expire after 24 hours
location /wp-admin.php {
ipset_autoadd scanners timeout=86400;
}
## Return 403 Forbidden instead of 404
location /admin.php {
ipset_autoadd honeypot timeout=86400 status=403;
}
## Return 429 Too Many Requests
location /api/hack {
ipset_autoadd abusers timeout=3600 status=429;
}
Common Honeypot Paths:
## WordPress traps - return 404 to look like missing file
location ~ ^/(wp-admin\.php|wp-login\.php|xmlrpc\.php)$ {
ipset_autoadd honeypot timeout=86400;
}
## Config file traps - return 403 to simulate forbidden access
location ~ ^/(\\.env|config\\.php|phpinfo\\.php)$ {
ipset_autoadd honeypot timeout=86400 status=403;
}
## Shell/exploit traps - severe, block for 1 week
location ~ ^/(shell|cmd|eval|exec)\\.php$ {
ipset_autoadd malicious timeout=604800 status=403;
}
Note: When an IP is auto-added, the module immediately returns the specified HTTP status code (default 404), preventing further request processing. The connection's keep-alive is also disabled to prevent further requests on the same connection.
Observability
ipset_stats
Context: location
Default: β
Enables the JSON statistics endpoint.
location = /_stats {
ipset_stats;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
}
See JSON Stats API for response format.
ipset_metrics
Context: location
Default: β
Enables the Prometheus metrics endpoint.
location = /metrics {
ipset_metrics;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
}
See Prometheus Metrics for available metrics.
π NGINX Variables
The module exposes two variables for use in logging, headers, or conditionals.
$ipset_result
The access decision made for this request.
| Value | Description |
|---|---|
allow |
Request allowed |
deny |
Request blocked |
dryrun |
Would be blocked (dry-run mode) |
ratelimited |
Rate limit exceeded |
challenged |
Challenge page served |
$ipset_matched_set
Name of the ipset that matched (if any). Empty if no match.
Note: This variable reflects the match state at request time, not the current state of the ipset. If you check the ipset manually and don't find the IP: - The entry may have expired (ipsets support per-entry timeouts) - The module's cache (default 60s) may show a recently-removed entry as still matched - Something may have removed the entry after the request was processed
Usage Examples
Custom access log:
log_format security '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'ipset_result="$ipset_result" '
'matched_set="$ipset_matched_set"';
access_log /var/log/nginx/security.log security;
Add headers for debugging:
add_header X-IPSet-Result $ipset_result always;
add_header X-IPSet-Matched $ipset_matched_set always;
Conditional logging:
## Only log blocked requests
map $ipset_result $loggable {
"deny" 1;
default 0;
}
access_log /var/log/nginx/blocked.log combined if=$loggable;
π Prometheus Metrics
The /metrics endpoint returns metrics in Prometheus exposition format.
Available Metrics
## HELP nginx_ipset_requests_total Total requests processed
## TYPE nginx_ipset_requests_total counter
nginx_ipset_requests_total{result="checked"} 1234567
nginx_ipset_requests_total{result="allowed"} 1234000
nginx_ipset_requests_total{result="blocked"} 500
nginx_ipset_requests_total{result="error"} 67
## HELP nginx_ipset_cache_total Cache operations
## TYPE nginx_ipset_cache_total counter
nginx_ipset_cache_total{result="hit"} 1200000
nginx_ipset_cache_total{result="miss"} 34567
## HELP nginx_ipset_cache_entries Current cache entries
## TYPE nginx_ipset_cache_entries gauge
nginx_ipset_cache_entries 5432
## HELP nginx_ipset_autoadd_total Auto-add operations
## TYPE nginx_ipset_autoadd_total counter
nginx_ipset_autoadd_total{result="success"} 42
nginx_ipset_autoadd_total{result="failed"} 3
## HELP nginx_ipset_ratelimit_total Rate limit events
## TYPE nginx_ipset_ratelimit_total counter
nginx_ipset_ratelimit_total{action="triggered"} 156
nginx_ipset_ratelimit_total{action="autobanned"} 23
## HELP nginx_ipset_challenge_total Challenge events
## TYPE nginx_ipset_challenge_total counter
nginx_ipset_challenge_total{result="issued"} 1000
nginx_ipset_challenge_total{result="passed"} 950
nginx_ipset_challenge_total{result="failed"} 50
## HELP nginx_ipset_uptime_seconds Module uptime
## TYPE nginx_ipset_uptime_seconds gauge
nginx_ipset_uptime_seconds 86400
Grafana Dashboard Queries
Request rate by result:
rate(nginx_ipset_requests_total[5m])
Block rate:
rate(nginx_ipset_requests_total{result="blocked"}[5m])
Cache hit rate:
rate(nginx_ipset_cache_total{result="hit"}[5m]) /
(rate(nginx_ipset_cache_total{result="hit"}[5m]) + rate(nginx_ipset_cache_total{result="miss"}[5m]))
Rate limit triggers per minute:
rate(nginx_ipset_ratelimit_total{action="triggered"}[1m]) * 60
π JSON Stats API
The /_stats endpoint returns detailed statistics in JSON format.
Response Format
{
"version": "2.0.7",
"uptime_seconds": 86400,
"requests": {
"checked": 1234567,
"allowed": 1234000,
"blocked": 500,
"errors": 67
},
"cache": {
"hits": 1200000,
"misses": 34567,
"entries": 5432,
"hit_rate": 97.20
},
"autoadd": {
"success": 42,
"failed": 3
},
"ratelimit": {
"triggered": 156,
"autobanned": 23
},
"challenge": {
"issued": 1000,
"passed": 950,
"failed": 50
}
}
Field Descriptions
| Field | Description |
|---|---|
version |
Module version |
uptime_seconds |
Seconds since module loaded |
requests.checked |
Total requests processed |
requests.allowed |
Requests that passed |
requests.blocked |
Requests that were blocked |
requests.errors |
ipset lookup errors |
cache.hits |
Cache hits (avoided kernel call) |
cache.misses |
Cache misses (required kernel call) |
cache.entries |
Current cached entries |
cache.hit_rate |
Hit rate percentage |
autoadd.success |
Successful honeypot additions |
autoadd.failed |
Failed honeypot additions |
ratelimit.triggered |
Rate limit violations |
ratelimit.autobanned |
IPs auto-added to ban list |
challenge.issued |
Challenge pages served |
challenge.passed |
Challenges solved successfully |
challenge.failed |
Challenge failures |
| ## |
ποΈ Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β REQUEST FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Incoming Request β
β β β
β βΌ β
β βββββββββββββββββ β
β β Rate Limit βββββ Exceeded? βββββΆ 429 + Auto-ban β
β β Check β β
β βββββββββ¬ββββββββ β
β β OK β
β βΌ β
β βββββββββββββββββ β
β β Challenge βββββ No cookie? βββββΆ Serve JS Puzzle β
β β Check β β
β βββββββββ¬ββββββββ β
β β Passed β
β βΌ β
β βββββββββββββββββ βββββββββββββββ β
β β Cache Check ββββββΆβ HIT ββββββΆ Use cached result β
β βββββββββ¬ββββββββ βββββββββββββββ β
β β MISS β
β βΌ β
β βββββββββββββββββ β
β β ipset Query βββββ Thread-local libipset session β
β β (kernel) β β
β βββββββββ¬ββββββββ β
β β β
β βΌ β
β βββββββββββββββββ β
β β Store in Cacheβ β
β βββββββββ¬ββββββββ β
β β β
β βΌ β
β βββββββββββββββββ β
β β Decision βββββ Blacklist match? βββββΆ Block (403/444) β
β β βββββ Whitelist miss? βββββΆ Block (403/444) β
β βββββββββ¬ββββββββ β
β β Allow β
β βΌ β
β βββββββββββββββββ β
β β Honeypot βββββ Location match? βββββΆ Add to ipset β
β β Check β β
β βββββββββ¬ββββββββ β
β β β
β βΌ β
β Continue to β
β Content Handler β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β SHARED MEMORY β
β ββββββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββββββββββββββ β
β β Stats β LRU Cache β Rate Limit Buckets β β
β β (counters) β (IP β Result) β (IP β Request Count) β β
β ββββββββββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Memory Layout
| Component | Location | Purpose |
|---|---|---|
| libipset session | Thread-local | Per-worker session to avoid locks |
| Lookup cache | Shared memory | LRU cache of IPβresult mappings |
| Rate limit buckets | Shared memory | Per-IP request counters |
| Statistics | Shared memory | Atomic counters for metrics |
| ## |
π Examples
Example 1: Basic Blacklist
## Create ipset
sudo ipset create blacklist hash:ip
sudo ipset add blacklist 1.2.3.4
server {
listen 80;
ipset_blacklist blacklist;
location / {
root /var/www/html;
}
}
Example 2: API with Rate Limiting
server {
listen 80;
# Strict rate limiting for API
ipset_ratelimit rate=100 window=1m autoban=api_banned ban_time=3600;
# Only allow known partners
ipset_whitelist api_partners;
ipset_status 401;
location /api/ {
proxy_pass http://backend;
}
}
Example 3: Full Security Stack
server {
listen 80 default_server;
# Layer 1: Known threats
ipset_blacklist malware_ips tor_exits datacenter_ranges;
ipset_status 444;
ipset_cache_ttl 5m;
# Layer 2: Rate limiting
ipset_ratelimit rate=60 window=1m autoban=ratelimited ban_time=1800;
# Layer 3: Bot challenge
ipset_challenge on;
ipset_challenge_difficulty 2;
# Real content
location / {
root /var/www/html;
}
# Honeypot traps - return 404 (default) to look like missing files
location ~ ^/(wp-admin|phpmyadmin|admin)\.php$ {
ipset_autoadd honeypot timeout=86400;
}
# Monitoring
location = /metrics {
ipset_metrics;
allow 10.0.0.0/8;
deny all;
}
}
Example 4: Dry-run Testing
server {
listen 80;
# Test new rules without blocking
ipset_blacklist new_threat_list;
ipset_dryrun on;
location / {
root /var/www/html;
}
}
Check logs:
tail -f /var/log/nginx/error.log | grep "DRYRUN"
π§ Troubleshooting
Module not loading
nginx: [emerg] dlopen() failed
Solution: Ensure NGINX was built with --with-compat and the module was built against the same NGINX version.
ipset not found
ipset: INVALID_SETNAME
Solution: Create the ipset before starting NGINX:
sudo ipset create myset hash:ip
Permission denied
ipset: kernel error
Solution: NGINX worker needs CAP_NET_ADMIN capability:
sudo setcap cap_net_admin+ep /usr/sbin/nginx
SELinux denials (RHEL/CentOS/AlmaLinux)
SELinux is preventing /usr/sbin/nginx from getattr access on the netlink_netfilter_socket
Solution: Install the included SELinux policy module:
cd selinux/
sudo ./install.sh
Or manually:
## Verify
semodule -l | grep nginx_ipset
The policy allows httpd_t (NGINX's SELinux domain) to use netlink_netfilter sockets required by libipset.
High memory usage
Solution: Reduce cache TTL or limit cache size in shared memory configuration.
Rate limiting not working
Solution: Ensure the ipset for auto-ban exists and has timeout support:
sudo ipset create ratelimited hash:ip timeout 3600
Log shows "matched=setname" but IP isn't in the ipset
This is expected behavior. The module reports what it saw at request time. If you check the ipset later and don't find the IP:
-
Timeout expiration: The IP was added with a timeout and has since expired
# Check if set supports timeouts ipset list setname | head -5 # Look for "timeout" in the header -
Module cache: The module caches lookups (default 60s). A recently-removed IP may still appear as "matched"
# Temporarily disable cache for debugging (not for production!) ipset_cache_ttl 0; -
Entry was removed: fail2ban, scripts, or manual commands may have removed it
-
Trap configuration issue: If using honeypot traps with
ipset_autoadd, legitimate bots may have triggered traps. Check your trap locations don't overlap with legitimate bot paths (like sitemaps, robots.txt). Userobots.txtto exclude trap paths from crawling.
autoadd fails with "result=4"
This means you're using timeout=N in ipset_autoadd but the ipset was created without timeout support.
Solution: Recreate the ipset with timeout support:
## Using ipset directly
sudo ipset destroy honeypot4
sudo ipset create honeypot4 hash:ip family inet timeout 86400
## Using firewall-cmd (RHEL/CentOS/Amazon Linux)
sudo firewall-cmd --permanent --delete-ipset=honeypot4
sudo firewall-cmd --permanent --new-ipset=honeypot4 --type=hash:ip \
--option=family=inet --option=timeout=86400
sudo firewall-cmd --reload
π Requirements
- NGINX β₯ 1.22 (built with
--with-compat) - Linux kernel with ipset support (nf_tables or xt_set module)
- libipset library and development headers
- Capabilities:
CAP_NET_ADMINfor ipset operations
π License
This is proprietary software. All rights reserved.
Available exclusively through GetPageSpeed Premium Repository.
π€ Author
Danila Vershinin
GetPageSpeed LLC
π Support
- Honeypot v2.0 Using ipset-access for auto-banning bots
- Support: Available for premium subscribers
- Contact: GetPageSpeed Support
NGINX IPSet Access Module
A premium NGINX module by GetPageSpeed LLC
www.getpagespeed.com
GitHub
You may find additional configuration tips and documentation for this module in the GitHub repository for nginx-module-ipset-access.