yum upgrades for production use, this is the repository for you.
Active subscription is required.
If you run NGINX in Europe — or serve European visitors — your access logs are a GDPR liability. Every request writes the client’s raw IP address to disk, and under EU law, an IP address is personal data. Data protection authorities have already issued fines for exactly this: logging IP addresses without a lawful basis. The NGINX ipscrub module exists to solve this exact problem.
You could stop logging entirely, but then you lose the ability to detect abuse, debug issues, and analyze traffic patterns. That is not a real option for production servers.
The ipscrub module eliminates this problem. It replaces every IP address with an irreversible cryptographic hash — right inside NGINX, before anything hits the disk. Requests from the same visitor still produce the same hash within a configurable time window, so you keep full traffic correlation. But the actual IP address is never stored. One module, one line of configuration, and your access logs are no longer personal data.
For GetPageSpeed subscribers, installation is a single dnf install. If you operate in the EU, this module is not optional — it is essential.
The Problem: NGINX Logs Are Personal Data
Under GDPR Article 4(1), IP addresses qualify as personal data. Every line in your default NGINX access log contains one:
93.184.216.34 - - [09/Mar/2026:10:15:00 +0000] "GET / HTTP/1.1" 200 615
That IP address can identify a household, a mobile subscriber, or — in many corporate networks — a specific employee. European data protection authorities treat this seriously. The Austrian DSB and French CNIL have both ruled that transferring or storing IP addresses without proper legal basis violates GDPR.
Most server administrators never think about this. The default combined log format ships with every NGINX installation, and it logs raw IPs. If your server has European visitors, you are processing personal data every second of every day.
The Solution: Cryptographic IP Anonymization with ipscrub
The ipscrub module solves this at the source. Instead of logging 93.184.216.34, your logs show:
1Q0Z59 - - [09/Mar/2026:11:22:00 +0800] "GET /page-1 HTTP/1.1" 200 615 "-" "curl/8.12.1"
1Q0Z59 - - [09/Mar/2026:11:22:00 +0800] "GET /page-2 HTTP/1.1" 200 615 "-" "curl/8.12.1"
1Q0Z59 - - [09/Mar/2026:11:22:00 +0800] "GET /page-3 HTTP/1.1" 200 615 "-" "curl/8.12.1"
The hash 1Q0Z59 cannot be reversed to recover the original IP. However, all requests from the same visitor produce the same hash within the same time period. You retain traffic analysis capability without storing personal data.
How It Works Under the Hood
The module uses a straightforward cryptographic approach:
- Salt generation: On startup (and periodically thereafter), the module generates a 128-bit random nonce using
arc4random_buf(), a cryptographically secure random number generator. - Hashing: For each request, the module concatenates the salt with the client’s IP address and computes a SHA-1 hash of the result.
- Output: The hash is Base64-encoded and truncated to 6 characters for IPv4 addresses or 22 characters for IPv6 addresses.
- Salt rotation: After a configurable period (default: 10 minutes), a new random salt is generated. All subsequent hashes use the new salt, making it impossible to correlate entries across periods.
Because the salt lives only in memory and is never written to disk, even the server operator cannot reverse the hashes after the salt rotates. This protects against legal compulsion scenarios where authorities demand identification of a user by IP address.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux, and Fedora
Install the ipscrub module from the GetPageSpeed RPM repository:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-ipscrub
Then load the module by adding the following line at the top of /etc/nginx/nginx.conf:
load_module modules/ngx_ipscrub_module.so;
Alternatively, include the auto-generated module configuration file that ships with the package:
include /usr/share/nginx/modules/*.conf;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-ipscrub
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Configuration
The ipscrub module provides one directive and one primary variable. That is all you need.
Directive: ipscrub_period_seconds
| Property | Value |
|---|---|
| Syntax | ipscrub_period_seconds <seconds>; |
| Default | 600 (10 minutes) |
| Context | http |
This directive controls how often the random salt rotates. A shorter period increases privacy (hashes expire sooner) but reduces the window for correlating requests from the same visitor. A longer period improves correlation at the cost of a wider exposure window.
For most deployments, the default 10-minute period strikes a good balance:
ipscrub_period_seconds 3600; # Rotate salt every hour for longer correlation
ipscrub_period_seconds 60; # Rotate salt every minute for maximum privacy
Variable: $remote_addr_ipscrub
This variable holds the anonymized IP hash. Use it anywhere you would normally use $remote_addr in log formats. For IPv4 clients, it produces a 6-character Base64 string. For IPv6 clients, it produces a 22-character string.
Quick Start: Anonymize Your Logs in 2 Minutes
After installing the ipscrub module, the entire setup is just a custom log format and a single directive. Here is a complete working configuration:
load_module modules/ngx_ipscrub_module.so;
events {
worker_connections 1024;
}
http {
ipscrub_period_seconds 600;
log_format anonymized '$remote_addr_ipscrub - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/access.log anonymized;
location / {
root /usr/share/nginx/html;
}
}
}
Reload NGINX and you are done. Your access logs now contain irreversible hashes instead of IP addresses.
Dual Logging During Transition
If you want to verify the anonymized logs meet your needs before switching fully, run both formats simultaneously:
log_format anonymized '$remote_addr_ipscrub - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
log_format standard '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/anonymized.log anonymized;
access_log /var/log/nginx/standard.log standard;
location / {
root /usr/share/nginx/html;
}
}
Once you are confident, remove the standard log line. That single deletion makes your NGINX server GDPR-compliant with respect to access logging.
Why Not Just Truncate the IP?
A common alternative is using NGINX’s map directive to zero out the last octet:
map $remote_addr $anonymized_addr {
~(?P<ip>\d+\.\d+\.\d+)\. $ip.0;
default $remote_addr;
}
This turns 93.184.216.34 into 93.184.216.0. It is simple, but it is not sufficient for GDPR compliance:
- Still identifying: A /24 subnet narrows the user down to 256 addresses. In many residential or corporate networks, this effectively identifies the individual.
- No IPv6 support: The regex above only handles IPv4. IPv6 requires entirely different truncation logic.
- No forward secrecy: The truncated IP never changes, so log data from different time periods can be correlated indefinitely.
- Legally questionable: European data protection authorities have not accepted IP truncation as adequate anonymization in all cases.
The ipscrub module avoids all of these problems. The hash is irreversible, covers both IPv4 and IPv6 automatically, and rotates on a configurable schedule. For European compliance, ipscrub is the correct choice.
Combining ipscrub with GeoIP
Need geographic data without storing IP addresses? Use the GeoIP2 module alongside ipscrub. GeoIP2 resolves the IP to a country or city at request time, and you log the result without the raw address:
log_format geo_anon '$remote_addr_ipscrub - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$geoip2_data_country_code" "$geoip2_data_city_name"';
This gives you geographic analytics while keeping your logs free of personal data. Several European data protection authorities have explicitly recommended this approach.
Worker Process Behavior
NGINX typically runs multiple worker processes. Each worker process maintains its own random salt in memory. This means:
- Requests from the same IP handled by different workers produce different hashes within the same period.
- Requests from the same IP handled by the same worker produce identical hashes within the same period.
For GDPR compliance, this behavior is irrelevant — the goal is to avoid storing raw IPs, and that goal is fully met regardless of worker count. For precise session correlation, use cookies or application-level mechanisms. The ipscrub module is a privacy tool, not a session tracker.
Security Best Practices
Protect the NGINX Error Log
The ipscrub module anonymizes the $remote_addr_ipscrub variable in access logs. However, NGINX’s error log still records raw IP addresses by default. To close this gap:
- Rotate error logs aggressively using
logrotatewith short retention (for example, 24 hours). - Restrict access to error log files.
- Reduce error log verbosity in production:
error_log /var/log/nginx/error.log crit;
Choose the Right Salt Rotation Period
| Period | Correlation Window | Privacy Level | Best For |
|---|---|---|---|
| 60s | Very short | Maximum | Healthcare, finance, high-sensitivity |
| 600s (default) | 10 minutes | High | General web servers |
| 3600s | 1 hour | Moderate | Analytics-heavy deployments |
| 86400s | 24 hours | Lower | Long session analysis |
For GDPR compliance, the default 600-second period is sufficient. Avoid periods longer than 24 hours — extended correlation windows weaken the privacy benefit.
Do Not Expose the Hash to Clients
While the $remote_addr_ipscrub variable can be used in response headers via add_header, avoid this in production. Exposing the hash lets attackers fingerprint the salt rotation schedule by correlating their own requests.
Testing Your Setup
Step 1: Verify the Configuration
sudo nginx -t
If NGINX reports unknown "remote_addr_ipscrub" variable, the module is not loaded. Add the load_module directive at the top of nginx.conf.
Step 2: Reload and Make a Request
sudo nginx -s reload
curl http://localhost/
Step 3: Check the Log
tail -1 /var/log/nginx/access.log
You should see a 6-character alphanumeric hash (like 1Q0Z59) instead of an IP address.
Step 4: Verify Salt Rotation
Temporarily set a very short period to confirm rotation works:
ipscrub_period_seconds 5;
Reload, make a request, wait 6 seconds, make another. The two hashes should differ.
Troubleshooting
“unknown variable” Error
nginx: [emerg] unknown "remote_addr_ipscrub" variable
The ipscrub module is not loaded. Add load_module modules/ngx_ipscrub_module.so; at the very top of nginx.conf, before the events block.
“directive is not allowed here” Error
nginx: [emerg] "ipscrub_period_seconds" directive is not allowed here
The ipscrub_period_seconds directive only works in the http context. Move it out of any server or location block.
Different Hashes for the Same IP
This is normal when running multiple worker processes. Each worker generates its own salt. See the Worker Process Behavior section for details.
Error Log Still Shows Raw IPs
The ipscrub module does not modify NGINX’s error log or the built-in $remote_addr variable. See Security Best Practices for mitigation.
Performance Impact
The ipscrub module adds negligible overhead:
- One SHA-1 hash per request — fast on any modern CPU.
- Salt generation occurs only once per rotation period (default: every 10 minutes), not per request.
- Memory footprint: 16-byte nonce plus a timestamp per worker process.
You will not measure any difference in response times or throughput.
License
The ipscrub module uses a modified BSD license. If your production service has a privacy policy, the license requires you to mention ipscrub. Review the full license terms for details.
Conclusion
If your NGINX server has European visitors, you should not be storing raw IP addresses in access logs. The ipscrub module makes this a solved problem: install it from the GetPageSpeed repository, swap $remote_addr for $remote_addr_ipscrub in your log format, and your logs are no longer personal data.
Combined with GeoIP2 for geographic insights and ModSecurity for request-level security, ipscrub is part of a privacy-first NGINX stack that European regulations demand.
The source code is available on GitHub.
