Site icon GetPageSpeed

NGINX ipscrub Module: Anonymize IP Addresses in Logs

NGINX ipscrub Module: Anonymize IP Addresses in Logs for GDPR Compliance

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:

  1. Salt generation: On startup (and periodically thereafter), the module generates a 128-bit random nonce using arc4random_buf(), a cryptographically secure random number generator.
  2. Hashing: For each request, the module concatenates the salt with the client’s IP address and computes a SHA-1 hash of the result.
  3. Output: The hash is Base64-encoded and truncated to 6 characters for IPv4 addresses or 22 characters for IPv6 addresses.
  4. 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_module directive 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:

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:

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:

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:

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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version