What CDC actually changes about your risk
Turning on CDC does not create new data. It creates new copies, in a system with different access control, different retention, and an audit story that is usually weaker than the database’s. Three properties make a change log meaningfully riskier than the table it came from:
-
It keeps history the table does not. An
UPDATEthat scrubs a customer’s address leaves one row behind, but the log holds the before-image indefinitely. - It fans out. One connector becomes a topic, which becomes a warehouse table, a search index and three dashboards. Every hop inherits the sensitive columns unless something removes them.
- It outlives deletion. Deleting the row emits a delete event; it does not retract the events that carried the values.
Filter at the connector, not downstream
The single highest-leverage decision is where a sensitive column is removed. Filtering downstream means the value already crossed the network, already sat in a broker, and is already in whatever retention that broker was configured with. Filtering at the connector means it never leaves the source host.
{
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
// Never capture it at all -- strongest option
"column.exclude.list": "public.customers.ssn,public.customers.card_number",
// Or capture it hashed, so joins still work downstream
"column.mask.hash.SHA-256.with.salt.<salt-name>": "public.customers.email"
}
Hashing is the useful middle ground: a salted hash keeps a column joinable and countable without carrying the plaintext. Choose the salt deliberately — a per-column salt that never rotates makes the hash stable and therefore joinable across time, which is exactly what analysts want and exactly what makes it re-identifiable if the salt leaks.
CDC privileges are broader than “read-only”
A common mistake is assuming a CDC user is just a reporting user. It is
not. Reading the log is a replication-level capability, and on most
engines it cannot be scoped per table the way a SELECT grant
can — the log carries every table, and the connector filters afterwards.
| Engine | Needs | Why it is wider than it looks |
|---|---|---|
| Postgres | REPLICATION attribute, plus a replication slot |
The slot reads the whole WAL. An abandoned slot also pins WAL forever and can fill the disk — an availability risk, not just a security one. |
| MySQL | REPLICATION SLAVE, REPLICATION CLIENT |
Grants the whole binlog for every database on the server, not the subset you configured. |
| Oracle | LogMiner access and dictionary views | Redo access spans the instance; incremental snapshots additionally want a writable signal table. |
Practical consequence: put the CDC user on a dedicated account with no interactive login, rotate its credentials like a service account, and audit slot and connector creation — the connector config is where column filtering lives, so whoever can edit it can quietly turn masking off.
In transit and at rest
- TLS on every hop — source to connector, connector to broker, broker to sink. The connector-to-source leg is the one most often left plaintext because it “stays inside the VPC”.
- Broker ACLs per topic. CDC topics usually deserve tighter ACLs than application topics, because they contain complete row images rather than curated events.
- The DLQ is a copy too. A dead-letter topic holds full failed payloads, is usually created with default permissions, and is routinely forgotten in access reviews. It deserves the same ACLs and retention scrutiny as the topic it protects.
- Connector configs hold credentials. Use the externalized-secrets mechanism rather than inline passwords; the REST API will otherwise return them to anyone who can read connector state.
Deletion requests and an append-only log
A right-to-erasure request is where CDC’s architecture and privacy law meet awkwardly. Deleting the source row produces a delete event; it does not remove the earlier events carrying that person’s data, and a compacted topic keeps the latest value per key rather than none.
Three approaches, in descending order of robustness:
- Never carry the identifier in the clear. If the log only ever held a salted hash, erasure in the source is sufficient and nothing downstream needs surgery. This is why the filtering decision above is worth making early.
- Compaction plus tombstones. Emit a genuine tombstone (null value for the key) and let compaction retire prior versions. Correct in the limit, but it is eventual — compaction runs on its own schedule, not on your deadline.
- Crypto-shredding. Encrypt per-subject with a key held outside the log, and destroy the key on request. The ciphertext may remain; without the key it is inert. This is the standard answer when a hard deadline meets an append-only store.
Whichever you choose, decide it before the first production topic exists. Retrofitting erasure onto a log that already contains two years of plaintext is a migration, not a configuration change.