Reading time: a coffee break

2026-08-21

Why we do not log external IP addresses

Executive summary

External IP addresses in the logs promise attribution and do not deliver it. An external IP identifies rented infrastructure. What logging reliably delivers instead: personal data within the meaning of the GDPR, with all the obligations that come with it. Whoever drops external IP addresses from their logs loses little forensically and gains privacy in return. That a dynamic IP address is personal data has been settled since CJEU Breyer (C-582/14, 2016). Logging is still permitted: Recital 49 GDPR names network security as a legitimate interest. It just costs purpose limitation, a retention limit, access controls and, in the event of a breach, one more data set you have to report. Our math: this homework costs more than the IP is worth forensically.

Intro

In conversations about incident response I often hear that external IPs have to go into the logs "so we can prevent attacks and know where they come from". After many years of SOC work I say: no, they do not have to, and you will not know who it was either. The only thing they collect is potentially personal data. Why that is, and why our website runs without IP logging as a result, is what I want to lay out here.

What an external IP really identifies

The idea behind IP logging: an address can be tied to an attacker. For internet traffic in 2026, almost none of that holds any more:

  • VPN exits and Tor: The IP belongs to a tunnel exit shared by thousands of users.
  • Cloud VMs: The IP belongs to a hyperscaler, rented for minutes, then someone else has it.
  • CGNAT: Mobile and many landline providers share one public IP across hundreds of subscribers at the same time.
  • GeoIP: The database that tells you "Country: X" may already be outdated at the moment of logging. IP ranges move between providers and regions.

In short: the IP is like starlight, a greeting from the past.

The counting error

IPs do not even tell you how many attackers you are dealing with. One actor can come through a botnet with a million addresses, or a million times from the same cloud IP. Any statistic about the "number of attackers" based on external IP addresses measures rented or hijacked infrastructure, not attackers.

The IP is the attacker's input

It gets worse: the source IP is not neutral, it is the attacker's choice. Whoever wants their traffic to appear to come from a certain country rents an exit there. False-flag operations through "fitting" countries of origin are standard.

A metric the attacker sets is not a forensic basis.

Who can actually do attribution

Real attribution exists, but it works differently: tradecraft, overlaps in attack infrastructure over long periods, and disclosures from providers. That is the level of law enforcement and specialised intelligence teams with access to data. And even there the chain often ends at a jurisdiction that does not answer. The access.log is not playing in that league.

What the IP is actually good for: correlation, briefly

The IP has one real, short-lived use: it links events within a few minutes. Was the SSH login at 03:12 the same source as the POST at 03:14? Against a bot: often yes. Against someone who means it: no, they switch sources between two steps. So the thread only works against those who make no effort, and for those a session ID, a request ID or a truncated or hashed address with a short lifetime is enough (see The middle way). For a static site without accounts the question does not even arise.

What remains without IPs

Dropping external IP addresses costs surprisingly little. What is in our logs and actually answers questions:

  • Requested paths: Scans for /wp-admin/, .env or /.git/ you spot by the path alone.
  • Status codes and patterns: 404 waves, error clusters, broken links.
  • User agents: Bot behaviour and tool signatures (these can be faked too).
  • Timing: Load peaks and how campaigns unfold.

These fields answer the question "are we being scanned, and what are they looking for?" completely.

Our own log shows this is not theory.

Example 1

{"t":"2026-08-10T22:25:57+00:00","host":"nexcurity.com","method":"GET","uri":"/wp-includes/css/buttons.css","proto":"HTTP/1.1","status":404,"bytes":891,"rt":0.000,"ref":"http://nexcurity.com/wp-includes/css/buttons.css","ua":"Go-http-client/2.0"}

A scanner using a "Go-http-client" is looking for WordPress files.

Example 2

{"t":"2026-08-11T10:37:39+00:00","host":"nexcurity.com","method":"GET","uri":"/en/home/node/.aws/credentials","proto":"HTTP/1.1","status":404,"bytes":891,"rt":0.000,"ref":"https://www.nexcurity.com/home/node/.aws/credentials","ua":"Mozilla/5.0 (compatible; Google-Extended/1.0; +http://www.google.com/bot.html)"}
{"t":"2026-08-16T03:44:10+00:00","host":"nexcurity.com","method":"GET","uri":"/de/.gcloud/credentials","proto":"HTTP/1.1","status":404,"bytes":891,"rt":0.000,"ref":"https://www.nexcurity.de/.gcloud/credentials","ua":"Mozilla/5.0 (compatible; Google-Extended/1.0; +http://www.google.com/bot.html)"}
{"t":"2026-08-16T03:44:16+00:00","host":"nexcurity.com","method":"GET","uri":"/en/aws/iam/temporary-credentials","proto":"HTTP/1.1","status":404,"bytes":891,"rt":0.000,"ref":"https://www.nexcurity.com/aws/iam/temporary-credentials","ua":"Mozilla/5.0 (compatible; Google-Extended/1.0; +http://www.google.com/bot.html)"}

The user agent, like the IP, is freely chosen by the attacker. In mid-August a credential scan ran through our log.

The bot poses as "Google-Extended". I am quite sure the real Googlebot does not crawl credentials. And honestly, just between us: Google-Extended does not exist as a crawler. It is a robots.txt token that lets you opt out of having your content used for AI training.

As a side note, the path list is a time capsule of the hype cycle. Next to the classics (.env in all variants, AWS and GCP credentials), bots are now specifically hunting for config files, tokens and API keys for AI services. What they are looking for, we know quite precisely. Who is searching, and why, is something the IP address would not have told us.

The middle way

If "no IP" is too drastic for you, you can truncate (/24 or /48) or hash. With hashing, the salt decides: with a fixed salt the address is merely a pseudonym and remains personal data. Only a salt that changes daily turns it into something that correlates for one day and belongs to no one tomorrow. Add a retention limit of 24 to 72 hours.

Two important distinctions

  1. Runtime processing is not logging. Rate limiting and brute-force protection may and should use the IP in memory. Without IP addresses in the logs, fail2ban stops working. Use limit_req in nginx, the RateLimit middleware in Traefik and PerSourcePenalties in OpenSSH 9.8 and later instead; that is in-memory throttling per source.
  2. Internal IP addresses are a different topic. Inside your own network an IP is a device in your inventory. There, attribution works.

Conclusion

Logging external IP addresses for defence is overrated, including as a preventive deny from external to internal based on IOCs. IP IOCs have a short half-life. Use them for noise reduction, in memory. What does get added is the data protection burden.

And as already said: internal IP addresses, and traffic from internal to external, are a different story.

Recommendations

  • Logging inventory: Check which of your internet-facing services store external IP addresses, including reverse proxy and load balancer logs (X-Forwarded-For).
  • Question the need: Why do we need external IP addresses? Which decision did we ever make with them?
  • Rebuild protections instead of switching them off: Move rate limiting to in-memory processing.
  • Align the privacy policy: If nothing is logged, the policy may say so plainly.

Notes

  • Nothing logged only holds once CDN, provider load balancer and WAF store no IP addresses either.
  • Nginx and the error log. While the access.log can get its own log_format, the error log cannot. IP addresses are logged there. Unless nginx sits behind a reverse proxy without the realip module enabled; then it only sees the reverse proxy's IP. An additional log_not_found off in the nginx config suppresses the 404 open() failed lines, which most often carry a client IP.
  • Outside the web stack, sshd and ufw log IP addresses by default. For anonymisation, rsyslog ships the mmanon module; alternatively, filter on the journald side.