Data Security¶
11:15 AM. An analyst on JupyterHub runs SELECT email FROM events LIMIT 100 against the "curated" schema in Trino. It returns real emails. No error, no audit ping — just PII on a screen that was supposed to be masked by default.
Predict before you read on: which control was missing?
A. RBAC never restricted the analyst's role to a masked view. B. The masking view exists but events (the raw table) was grantable too. C. Audit logging would have caught it after the fact, but nothing would have prevented it. D. This is a break-glass path working as designed, and the ticket trail is the control.
A data platform concentrates what every product database holds in isolation — PII, payments, auth logs, health-adjacent telemetry, internal metrics — so a breach here is not "one app's users," and the scenario above (most likely A or B) is why this page threat-models the platform like a bank, not like a blog's Postgres, mapping controls to threats rather than listing vendors.
Related: metadata, notebooks, analytics tenancy, fraud.
Threat model (data platform)¶
| Threat | Example | What actually stops it |
|---|---|---|
| Exfil via SQL | Analyst SELECT email FROM events | Column masking, authorised views, audit, no raw notebooks |
| Exfil via notebook/Spark | Write to personal S3 | Egress controls, job IAM, review |
| Tenant leak | Missing customer_id filter | RLS in the store, not only the UI |
| Producer impersonation | Random client writes Kafka | SASL, ACLs, mTLS |
| Consumer over-read | One group reads payments and debug | Topic ACLs, separate clusters for PCI |
| Ransomware / disk theft | Snapshot walk-off | Encryption at rest, KMS, backup policy |
| Insider + logs | Support engineer | JIT access, audit, masking |
| Supply chain | Malicious PyPI in the image | Pin images, no arbitrary pip on prod |
| Replay / undelete | Kafka + Iceberg time travel restores PII | Retention + legal hold process |
| Schema as attack | Giant string field with payloads | Validation, size limits |
If your design only says "S3 is encrypted," you have modelled disk theft and nothing else.
Encryption¶
At rest: object storage default encryption + KMS CMKs; Kafka disk; CH/Postgres volumes. This is table stakes.
In transit: TLS everywhere. Kafka SASL_SSL; CH TLS; Trino HTTPS; JDBC TLS. Plaintext "inside the VPC" is how a single compromised pod sniffs passwords.
Keys: KMS/Vault. Never in git, images, or Airflow Variables UI screenshots. Rotate. Separate keys per env and per residency.
Application-level encryption for a few super-sensitive columns (PAN tokens, secrets) when the store's RBAC is not enough. You then cannot GROUP BY the plaintext — that is the point.
Authentication and RBAC¶
Humans: SSO (OIDC/SAML). No local CH passwords in a wiki.
Jobs: service principals with least privilege. Spark jobs that only write s3://lake/curated/events/ must not hold s3:*.
RBAC: roles = job functions (analyst_product, oncall_obs, etl_orders), not individuals.
CREATE ROLE analyst;
GRANT SELECT ON SCHEMA analytics TO ROLE analyst;
GRANT ROLE analyst TO USER alice;
ABAC / row filters: tenant, region, "not PII." Trino/CH row policies. Spark + Iceberg: grant on tables, plus views for masked columns.
Lakehouse grants (Iceberg + catalog): treat the catalog like a database GRANT system. Object-store IAM is defense in depth, not the user-facing model (users should not get raw S3 list on the whole bucket).
Column masking and row security¶
CREATE VIEW events_masked AS
SELECT
event_id,
timestamp,
service,
CASE WHEN is_pii_viewer() THEN email ELSE '***' END AS email,
CASE WHEN is_pii_viewer() THEN user_id ELSE NULL END AS user_id
FROM events_raw;
Row-level:
Rules:
- Product UIs and SQL engines enforce tenant filters (analytics).
- Masked view is default; raw table is a break-glass role.
- Aggregates can still leak (unique
user_idcounts on tiny sets). Policy must cover that for health/finance data.
Kafka ACLs (minimum that is real)¶
| Principal | Allow | Deny |
|---|---|---|
| Ingest gateway | WRITE on product-events | Read payments-cdc |
| Flink scorer | READ transactions, WRITE scores | Admin |
| Analysts | nothing on Kafka | "Just in case" console on prod |
| Spark lake job | READ specific topics | WRITE to prod topics |
allow everyone on a "internal" cluster is not an ACL strategy. Compacted topics with PII are stores. Treat them like databases.
Schema registry: authenticated, ACLs on subjects, compatibility FORWARD/FULL so a producer cannot ship a poison schema that crashes every consumer (availability is a security property too).
Lakehouse grants¶
- Bucket policy: jobs write only their prefix.
- Table grants in Glue/Nessie/Polaris/Unity:
SELECTon curated, not raw. - Time travel:
SELECTon historical snapshots can undelete GDPR. Restrict who can query old snapshots; expire per policy. - Compaction jobs need write; analysts do not.
Audit¶
If you cannot answer "who read email last week," you do not have a control, you have a hope.
Log:
- Trino/CH/Postgres query text (or hash + columns touched).
- Kafka ACL denials.
- S3 data access (careful: volume).
- Catalogue/admin changes.
- Notebook spawns and egress (notebooks).
Retain audits longer than the data if law requires. Protect audit logs from the people they watch.
PII, GDPR, residency¶
Classify in the catalogue: direct identifiers, quasi, sensitive categories.
Minimise: do not land PAN, raw email, or access tokens in the lake if the product does not need them. Hash/tokenise at the gateway (analytics Flink mask).
Erasure path (must be tested):
- OLTP
- CDC → lake deletes (Iceberg/Hudi)
- ClickHouse mutations
- Search, graph, object exports
- Backups: documented delay
Residency: EU tenants → EU Kafka, CH, S3. A region column on a US cluster is not residency.
Retention: TTL is a security control. Keeping 2 years of raw "for ML" is a threat-model choice; price it and name the owner.
PCI/fraud: PAN never in CH/Iceberg; tokens only. Fraud scores are still sensitive.
Secrets¶
Runtime fetch from Secrets Manager / Vault / cloud native. Rotate. Airflow Connections encrypted. No secrets in spark-submit CLI (they show up in ps and Spark UI).
Worked threat: "just a notebook"¶
An analyst on JupyterHub with a Spark session and s3://lake/raw/** can:
SELECTPII.- Write a CSV to
~/and download. - Push to an external IP.
Controls: masked views only, no raw IAM, network policy deny egress, download disabled or DLP, sample-size limits, audit. See notebooks. If you cannot fund this, do not give notebooks lake credentials.
V1 checklist (ship these)¶
- Encryption at rest + TLS
- SSO humans, service accounts for jobs
- Kafka ACLs per app, not
ANONYMOUS - Schema registry auth
- PII tagged on gold tables
- Masked views as default SQL
- Tenant predicate in CH/Trino
- Audit on interactive SQL
- Secrets in a manager
- Deletion drill on one user
- Retention TTLs that match policy
Do not add yet: homomorphic encryption, a second identity provider, custom KMS.
How it fails¶
| Failure | Symptom | Absorb |
|---|---|---|
| RLS bug | Cross-tenant ticket | Dual control: app + DB; canary tenant test |
ACL ALLOW * leftover | Silent | Periodic ACL dump diff |
| Time travel restores PII | Legal | Snapshot expire + role |
| Spark UI leaks SQL with secrets | Screenshots | Redact; no secrets in SQL |
| DLQ full of PII | World-readable topic | ACL + TTL on DLQ |
Apply this at work¶
- Draw data stores. For each, name who can read PII.
- Pick one threat from the table. Trace a path. Put a control on that path.
- Run a GDPR delete in staging end-to-end.
- Remove one overly broad Kafka ACL this week.
Security that exists only in an architecture slide will not show up in an incident — the incidents in this academy are availability-shaped; add access review to your real drills.
Kafka ACL sketch (topic families)¶
product-events WRITE: ingest-gw READ: flink-enrich, spark-lake
product-events-dlq WRITE: flink-enrich READ: oncall-role
payments-cdc WRITE: debezium READ: flink-fraud, spark-finance
scores WRITE: flink-fraud READ: spark-labels
Admin: platform only. kafka-acls dumped to git weekly; diff in CI. ANONYMOUS disabled.
Iceberg / object store¶
- Prefix
s3://lake/raw/— ETL roles only. s3://lake/curated/— Trino analyst role via catalog grants, nots3:ListBucketon*.s3://lake/tmp/— TTL 3 days, no PII (policy + scanner).
Spark UI still shows SQL. Assume analysts can see queries; no secrets in predicates.
ClickHouse grants¶
CREATE USER analyst IDENTIFIED BY ...;
GRANT SELECT ON analytics.events_masked TO analyst;
GRANT SELECT ON analytics.tiles_1m TO analyst;
-- no SELECT on events_raw
-- no INSERT/ALTER
Row policy: customer_id IN (SELECT cid FROM user_tenants WHERE user = currentUser()) for support roles. Product app uses a service user per tenant or a session setting you set server-side.
Break-glass¶
On-call raw_reader role: 4-hour JWT, ticket required, all queries audited, Slack notify. If break-glass is the daily path, grants are wrong.
Network¶
- Kafka/CH/Trino not on public IPs.
- Notebooks: no egress.
- Spark drivers in private subnets.
- TLS even inside the VPC.
A "trusted network" is not a tenant boundary.
Review script for a new dataset¶
- PII columns tagged?
- Who can SELECT raw?
- Kafka topic ACL?
- Retention vs legal?
- GDPR path tested?
- Masked view default?
- Audit on?
If three answers are "later," the dataset stays in bronze and not in Hub IAM.
Threat: compromised Spark job¶
A dependency on PyPI runs in the ETL image and reads s3://lake/raw. Controls: pin hashes, private index, image scanning, runtime IRSA scoped to one prefix, network deny to the public internet from jobs, detect unexpected ListBucket on other prefixes. Treat job IAM as production admin.
Threat: Trino wild join as exfil¶
Incident 6 is availability. The same query is exfil if it writes to an external connector or returns 40M emails to a JDBC client. Controls: no write connectors for analysts, result size limits, column masks, audit, no INSERT to personal catalogues.
Kafka at-rest and compacted PII¶
Compacted topics retain latest PII forever until tombstone. GDPR on compacted user-profile requires a tombstone and retention of deleted keys. Treat compacted topics as databases in the erasure drill.
Encryption ≠ access control¶
SSE-S3 stops a stolen disk. It does not stop a Hub user with s3:*. Always pair KMS with IAM/RLS. People confuse these in reviews — call it out.
On-call access¶
Pager role: read logs/metrics, restart jobs, not SELECT email. Separate incident_data role if you must dump a user. Time-bound.
Checklist recap (print)¶
- TLS + at-rest + KMS
- SSO + job principals
- Kafka ACLs + registry auth
- CH/Trino/Iceberg grants
- Masked defaults
- Tenant RLS
- Audit interactive SQL
- Secrets manager
- Erasure drill
- Notebook IAM ≠ ETL IAM