Device & GDPR Signature
A consent-gated, one-way identifier that re-recognises a returning visitor without storing anything about them
What It Is
The gdpr_signature is a second, privacy-preserving way for the
reverse proxy to recognise a returning visitor — one that survives the visitor clearing
both their cookies and their localStorage. It is layered
alongside the signed SERVERID session cookie, never replacing it, and it
only activates with the visitor's explicit, revocable consent. The server never
stores anything identifiable about the visitor — only a one-way hash, and only when it matters.
Why It Exists
The signed session cookie and the passive recovery fingerprint
(lua/session_identity_rules.lua) can both be shed by a client that clears storage
and presents a different TLS/HTTP fingerprint. The signature adds an orthogonal,
consent-based recovery path that the browser itself reproduces deterministically, so a diverted
adversary who wipes everything and comes back is routed straight back to the honeypot.
The Flow
The source of truth is ids/GDPR.md; the client half lives in
gdpr_client.js and the server half in lua/gdpr_handler.lua.
Client half (browser)
- On
GET /the proxy injects a<script>and serves/__gdpr/config.gdpr_client.jsshows a consent banner and fetches/__gdpr/config→{ enabled, publicKey, ... }. - Only if the visitor clicks Agree, and immediately:
- build the payload
{ s: "<screenW>x<screenH>", t: <now> }; - compute
gdpr_token = base64( RSA-OAEP-SHA256( payload, publicKey ) ); - set the
gdpr_tokencookie and reload.
- build the payload
The encryption in gdpr_client.js:
function buildToken() {
if (!publicKey) return Promise.reject(new Error("no public key"));
var payload = collectCharacteristics(); // { s: screen.width + "x" + screen.height }
payload.t = Math.floor(Date.now() / 1000); // fresh timestamp each time
var data = new TextEncoder().encode(JSON.stringify(payload));
return window.crypto.subtle
.encrypt({ name: "RSA-OAEP" }, publicKey, data)
.then(function (cipher) { return bytesToB64(cipher); });
}
Server half (reverse proxy, per request, in memory only)
- Decrypt:
plaintext = RSA-OAEP-SHA256-decrypt( gdpr_token, PRIVATE key ). The private key never leaves the proxy. - Append the client IP server-side:
payload.ip = $remote_addr— the one characteristic the browser cannot supply or forge. - If the timestamp is within
GDPR_TIMESTAMP_WINDOW, build a stable canonical form of the characteristics (including the appended IP, timestamp stripped, keys sorted) and compute the one-waysignature = SHA256( deployment-salt | canonical ). - If Redis holds an unexpired
gdpr_honeypot_sig:<signature>, prolong its TTL and route the session to the honeypot (router Stage 1a). Otherwise fall through to the normal session-token routing. - The plaintext and characteristics are discarded immediately; only the hash may persist.
And the one-way hash in gdpr_handler.lua:
local function compute_signature(canonical)
local h = sha256:new()
h:update(_sig_secret)
h:update("|")
h:update(canonical)
return str.to_hex(h:final())
end
Whenever a request that carried a valid signature is routed to the honeypot for any
reason, the proxy writes gdpr_honeypot_sig:<signature> with a
GDPR_BINDING_TTL. A later storage wipe plus re-consent reproduces the same signature
and lands the visitor right back on the honeypot.
Why It Is Privacy-Preserving
- Consent first, and only then. No token is generated until the visitor clicks
Agree. Decline or never decide, and the
gdpr_tokencookie is never set; theSERVERIDsession stays the only identifier. - The browser is the only thing that can produce a token. The plaintext characteristics are recomputed live each time; only ciphertext is ever at rest, and it cannot be decrypted without the private key, which never leaves the proxy.
- The server never stores PII. The token is decrypted only in memory, only long enough to verify the timestamp and compute a one-way SHA-256 hash. Only that hash is stored, and only for honeypot-bound signatures, as a Redis key with a TTL.
- Revocation is "just stop sending it." The only server-side record is a one-way hash that expires on its own, so there is nothing identifiable to delete.
Returning-Visitor Recovery
Because the signature is derived from stable device characteristics plus the server-observed IP, the same device from the same screen size reproduces the same signature. That is how a returning visitor is re-recognised without storing anything about them: the honeypot binding keyed on the hash is still in Redis, so the visitor is diverted again before the normal session routing even runs. Change the screen size (or the IP), and the signature changes — this is a recovery path, not a precise fingerprint.
Configuration
| Variable | Effect |
|---|---|
GDPR_TIMESTAMP_WINDOW | How fresh a token's timestamp must be for the proxy to derive a signature from it, bounding replay of a captured token. |
GDPR_BINDING_TTL | How long a gdpr_honeypot_sig:<signature> binding lives in Redis before it expires on its own. |
/__gdpr/config enabled | Whether the consent banner and token flow are served to the browser at all. |
The signature only ever adds to honeypot routing; it is never used to send a session back to production, and it is independent of the risk score.
See It Live
The dashboard's Signature Simulation page reproduces the whole flow against the running proxy: resize the visitor window, press Agree & derive signature, and watch the hex signature the proxy logs. Resize to a new size for a different signature, then resize back to the first size to see the original signature — and the re-recognition — return.