DEV Community

Cover image for You can't delete an event. GDPR says you must. Crypto-shredding is the truce.
Norbert Rosenwinkel
Norbert Rosenwinkel

Posted on

You can't delete an event. GDPR says you must. Crypto-shredding is the truce.

Two rules that can't both be true

Event sourcing has one rule: you never delete. You append. The log is the source of truth, and rewriting history is the cardinal sin.

GDPR Article 17 has one rule too: when a user asks, you erase their personal data. Not "hide it," not "flag it deleted" — erase it, everywhere, including backups.

Put an event-sourced system in front of a privacy regulator and those two rules collide head-on. The user's name, email, and address are baked into CustomerRegistered, AddressChanged, OrderPlaced — dozens of immutable events, replicated to read models, snapshotted, and sitting in every nightly backup you've ever taken.

"Just delete the events" breaks event sourcing. "Never delete" breaks the law. Most teams discover this tension after they've committed to append-only.

A word on why this isn't academic for me. I build from Germany. Article 17 is EU law — the GDPR, or DSGVO as we call it here — not a German invention, but Germany enforces it about as hard as anywhere in Europe: regional data-protection authorities that issue real fines, and "we were careful" has never been a defense that held up. That pressure is exactly why I wanted erasure to fall out of the architecture instead of being a promise I make to an auditor and then pray I can keep.

Why "delete the row" doesn't actually erase anything

Say you give in and hard-delete the events for one user. You've still got their data in:

  • every read-model projection rebuilt from those events,
  • every snapshot that rolled them up,
  • every backup taken before the deletion,
  • every replica and every export that already left the building.

Chasing personal data across all of those, provably, on a 30-day regulatory clock, is a nightmare — and a single missed backup tape means you didn't comply. Physical deletion doesn't scale to a system designed to keep everything forever.

Crypto-shredding: delete the key, not the data

The trick is to stop trying to delete the data and instead delete the ability to read it.

Encrypt each subject's personal data under a key that belongs only to that subject. Keep the ciphertext wherever it lands — events, snapshots, backups, replicas. When the erasure request comes in, you don't hunt down the data. You destroy the one key.

The instant that key is gone, every copy of that ciphertext — including the ones on backup tapes you can't even reach — turns into undecryptable noise. The bytes still exist; they're just permanently meaningless. That's crypto-shredding, and it's what makes Article 17 erasure architecturally sound instead of a manual scavenger hunt.

The data model becomes: plaintext is derived, ciphertext is permanent, and recoverability is a property of a key you control.

The tenant-aware twist (so a leak doesn't cascade)

There's a second property worth getting for free here. If you bind the ciphertext to whose data it is, a leaked key from one tenant can't unlock another's.

AES-GCM takes associated data (AAD) — bytes that aren't encrypted but must match at decryption time or the authentication tag fails. Fold the tenant id into the AAD, and a ciphertext encrypted for tenant A simply won't decrypt under tenant B's context — even if the attacker has the right key bytes. The tenant binding is mathematical, not a WHERE tenant_id = ... you hope nobody forgets.

What it looks like in code

This is the model Stratara builds in (a .NET event-sourcing stack), but the technique is framework-agnostic — the pieces are a key store, an AES-GCM encryptor, and a key you can destroy.

Mark the sensitive fields:

public sealed record CustomerRegistered(
    Guid CustomerId,
    [property: EncryptData] string FullName,
    [property: EncryptData] string Email);
Enter fullscreen mode Exit fullscreen mode

Encryption happens at the serialization boundary — when the event is written to the store or the bus, not in memory. Your handler reads customer.Email and gets plaintext; the bytes at rest are sealed.

A key is addressed by a scope — a sensitivity level optionally narrowed to a tenant and/or user:

var scope = new KeyScope(DataSensitivityLevel.TenantScoped, tenantId.ToString());
Enter fullscreen mode Exit fullscreen mode

The store hands out a versioned, KEK-wrapped data-encryption key per scope (never plaintext at rest). Rotation adds a version and keeps old ciphertext readable. And the erasure request — the whole point — is one call:

await keyStore.EraseScopeAsync(scope, cancellationToken);
// every ciphertext under this scope is now permanently undecryptable — backups included
Enter fullscreen mode Exit fullscreen mode

For larger payloads (attachments, exports) the same scope + a purpose label bind a whole stream:

await using var sealedStream = await blobEncryptor.EncryptAsync(
    plainStream, scope, purpose: "attachment", cancellationToken);
Enter fullscreen mode Exit fullscreen mode

What it costs at runtime

The question that always follows "encrypt every sensitive field" is "what does that do to throughput?" Measured: sealing a field with AES-GCM runs around 4 µs per object — a few hundred thousand encrypt operations a second on a single core of a fanless MacBook Air M4, and it parallelizes across them. The number worth knowing is what an object with no [EncryptData] fields costs: about 45 nanoseconds over plain System.Text.Json. Routing everything through the secure serializer is essentially free; you only pay for the fields you actually mark. Encryption stays a per-field decision, not a per-system tax.

The honest limits (because there always are some)

Crypto-shredding is genuinely strong, but it is not magic, and anyone selling it as magic is wrong:

  • It only shreds what stayed encrypted. Personal data that escaped the encrypted boundary — written to a log line, an analytics event, a search index, a CSV someone emailed — is not under the key and is not shredded. The boundary is only as good as your discipline about what crosses it.
  • It shreds recoverability, not the bytes. If your threat model requires provable physical destruction, crypto-shredding doesn't give you that — it gives you computational irreversibility, which is what regulators actually accept, but know the difference.
  • A key extracted before the shred is a key forever. If an attacker copied the DEK while it was live, destroying it later doesn't help for the data they already grabbed. Crypto-shredding protects against future reads of retained ciphertext, not past compromise.
  • Key custody is now the whole ballgame. You've moved the problem from "delete data everywhere" to "manage and destroy keys reliably." That's a better problem — it's small and centralized — but it's a real one. The data-encryption keys should themselves be wrapped by a master key (KEK) you keep in an HSM / KMS / vault, and your keystore backups need their own erasure story.

Name those, and crypto-shredding goes from a compliance checkbox to an actual control.

Where this earns its keep

Beyond GDPR Article 17, the same per-subject-key model underwrites a lot of the compliance surface teams dread:

  • SOC 2 / ISO 27001 — demonstrable data-isolation and key-lifecycle controls.
  • HIPAA — per-patient cryptographic separation.
  • Multi-tenant SaaS — a leaked row from one tenant is useless against another, by construction.

It turns "we'll be careful" into "the storage layer is careful by default."


The hard part of event sourcing was never the append. It's the two things append-only quietly makes harder — proving nobody rewrote history, and erasing someone who has the right to be forgotten. The first is hash-chaining (I wrote that one up separately). The second is crypto-shredding, above.

The technique drops into any append-only store. If you want it wired — [EncryptData], scoped keys, one-call erasure — it's in Stratara, the .NET stack I maintain; zero-dependency samples in the repo, docs at https://docs.stratara.tech. Source-available under FSL-1.1-MIT (not OSI-approved OSS; flips to plain MIT two years after each release).

Happy to get torn apart in the comments — especially on the limits. And if the approach earns a place in your toolbox, a star on the repo helps the next person fighting the append-only-vs-Article-17 problem find it.

Top comments (0)