Nobody decides to build a movement-tracking dataset. You install a web server, it writes its default log format, and eighteen months later you are holding a per-second record of which network every user connected from, what they asked for, and when. It survives in three places you set up on purpose and two you forgot about.
This is a practical look at what a default stack records, why the usual anonymisation trick does not work, and how to keep enough to run the service.
Start with the line itself. The combined log format that ships as the default in common web servers produces something like this:
198.51.100.42 - - [17/Aug/2026:09:14:22 +0000] "GET /reset?token=8f3a...c1 HTTP/2.0"
200 4213 "https://example.org/settings" "Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 ...)"
Every field is there for a reason and every field carries something beyond that reason.
| Field | What it also gives you |
|---|---|
| Client address | Approximate location, employer or ISP, and a join key that links otherwise separate sessions into one person's activity. |
| Timestamp to the second | Waking hours, time zone, working pattern. Two users on the same network are separable by rhythm alone. |
| Request line | The full query string. Password reset tokens, invitation codes, email addresses passed as parameters and search terms all land here in plaintext. |
| Referer | The previous page, including any identifiers another site put in its own URLs. |
| User agent | OS and browser version, which is a coarse device fingerprint and a precise inventory of who is running unpatched software. |
The query string row is the one that produces incidents. A password reset link in an access log is a live credential sitting in a file that ships to a log aggregator, gets indexed, gets backed up, and is readable by everyone with production support access. Several large providers have published exactly this as a postmortem, usually as credentials logged in plaintext by a component nobody thought of as a logger.
Hashing an IP address does not anonymise it
The standard reflex is to hash the client address before writing it, so that sessions can still be correlated without storing the address itself. This is worth understanding precisely, because it is nearly universal and it does not do what it appears to do.
IPv4 has 2^32 addresses, about 4.3 billion. A single modern GPU computes SHA-256 at a rate on the order of ten billion hashes per second. Enumerating the entire IPv4 space and building a complete reverse table is therefore under a second of work, and the table is small enough to keep. An unsalted hash of an IP address is a reversible encoding with extra steps.
Adding a fixed application-wide salt does not fix it either. It stops a precomputed table built before your salt was known, and it costs the attacker one more pass over the same 4.3 billion values once they have the salt, which they will have if they got the logs from your infrastructure.
What actually changes the result is a rotating secret salt that is discarded: derive the daily key, keep it in memory, destroy it at rollover. Yesterday's identifiers can no longer be linked to today's, and nobody can reverse either set without the key, which no longer exists. Several large providers use this pattern and it is a good default.
Truncation is blunter and often better. Zeroing the final octet of an IPv4 address, or keeping only the first 48 bits of an IPv6 address, destroys the identifier rather than obscuring it. You lose per-host resolution and keep network-level resolution, which is what most abuse and capacity work actually needs. Unlike hashing, the loss is real and there is no key that undoes it.
The legal position is settled enough to plan around. The Court of Justice of the European Union held in Breyer that a dynamic IP address can be personal data in the hands of an online service where means reasonably likely to be used exist to identify the person behind it. Pseudonymised data remains personal data under the GDPR by definition. A hashed address is pseudonymised, not anonymous, and the compliance argument that treats it as anonymous is the same argument the mathematics above refutes.
The copies you did not plan
Retention policy is usually written for the file on the web server. The data is rarely only there.
- The log shipper's destination. Whatever retention the search backend enforces is the real retention, and it is often longer because storage is cheap and the default is generous.
- The error tracker. Exception reporters attach request context by default, which can include headers, cookies and body fragments.
- The CDN and load balancer. They log independently, upstream of anything your application does, under a different vendor's policy.
- Database slow-query logs. Query text plus bound parameters, which is to say user data, written to a file with its own lifecycle.
- Backups of all of the above. A 30-day log retention with 12-month backup retention is a 12-month log retention.
Before designing anything, the useful exercise is to grep your own production logs for the things you believe are never logged. Email addresses, bearer tokens, the string password, the reset path. The result is usually informative.
A design that still lets you operate
Minimisation fails when it is framed as logging less. Framed as deciding which question each field answers, it converges quickly.
- Split audit from debug. Security-relevant events (authentication, permission changes, administrative actions) need long retention, tight access and integrity. Operational debug logs need hours to days. Merging them forces the shorter requirement to inherit the longer retention.
- Redact at the source. A redaction step in the query layer protects nobody, because the raw value was already written to disk and shipped. The transform belongs in the logging call, before serialisation.
- Allowlist the fields. Structured logging with an explicit set of permitted keys fails safe. A denylist of forbidden keys fails the moment someone adds a field, which is the normal case.
- Strip query strings from access logs. Log the path, not the parameters. If a specific parameter is genuinely needed for operations, name it explicitly and add it back.
- Sample the boring paths and keep all the errors. One percent of successful requests answers capacity questions accurately. Full retention of non-2xx responses answers the debugging questions. This cuts volume by orders of magnitude, which happens to cut cost as well.
- Aggregate at write time where you can. If the requirement is requests per endpoint per minute, increment a counter. A counter cannot be subpoenaed for an individual's history because that history was never in it.
- Enforce retention in the storage layer. Object lifecycle rules and index-level TTLs delete on their own. A cron job that deletes old files stops silently, and nothing alerts on data that failed to disappear.
What minimisation is worth
The compliance case is direct: the GDPR requires data to be adequate, relevant and limited to what is necessary, and kept no longer than necessary. Logs are usually the least examined part of an estate against those two requirements.
The operational case is stronger. A field you never wrote cannot appear in a breach, which shrinks what a notification has to cover. It cannot be handed over in response to a subpoena or an emergency data request, because there is nothing in the table. It cannot be misused by an employee with production access. Every other control depends on a process continuing to work correctly. This one depends on a column not existing.
That is the same reasoning behind end-to-end encryption, applied one layer down. Encrypting message contents means a provider cannot read them. Minimising logs means the provider does not hold the record of who talked to whom and from where, which for a great many adversaries is the more useful half anyway. Both are answers to the same question, which is what your infrastructure would be able to disclose on the worst day it has.
Originally published at havenmessenger.com
Top comments (0)