Encrypt content at rest - phase 1 of the #189 data-minimization track #322

Merged
robbertbos merged 1 commit from phase-1-at-rest-encryption into main 2026-08-15 21:33:07 +00:00
Owner

cards.source_data (the message snapshot), cards.notes and outgoing_posts.content become Fernet ciphertext under two new HKDF domains (waggle-cardcontent-v1, waggle-outgoing-v1), decrypted transparently at the ORM boundary (EncryptedJSON/EncryptedText over bytea in models/base.py). A database dump, backup or SQL-injection read no longer yields message content, notes or unsent drafts.

Why this is functionally free: nothing filters on these columns in SQL. The note filter and the retention sweep's note check already ran in Python after load; the one cheap SQL pre-filter on notes is removed. Import dedupe keeps working through column projections, which decrypt like any ORM read.

The revision (e5a9c31d7f02) converts existing rows in place and is NOT rolling-safe - the changelog carries a 'Note when rolling out': stop the old pod first and take a database backup; reading the converted columns requires the WAGGLE_SECRET_KEY the migration ran with.

waggle rotate-keys now also re-encrypts the three content columns, on raw table clauses rather than the ORM (the mapped types decrypt with process settings, not --from/--to). docs/security.md gains the two domains and a 'Content at rest' section; the key-rotation runbook in deployment.md names the covered columns.

Gates: 1981 backend tests at 100% coverage (including two new migration tests: in-place conversion of seeded plaintext, and bytea column types at head), 1177 vitest, vue-tsc + build, pinned pre-commit, full e2e suite green against a stack that boots through the new migration.

cards.source_data (the message snapshot), cards.notes and outgoing_posts.content become Fernet ciphertext under two new HKDF domains (waggle-cardcontent-v1, waggle-outgoing-v1), decrypted transparently at the ORM boundary (EncryptedJSON/EncryptedText over bytea in models/base.py). A database dump, backup or SQL-injection read no longer yields message content, notes or unsent drafts. Why this is functionally free: nothing filters on these columns in SQL. The note filter and the retention sweep's note check already ran in Python after load; the one cheap SQL pre-filter on notes is removed. Import dedupe keeps working through column projections, which decrypt like any ORM read. The revision (e5a9c31d7f02) converts existing rows in place and is NOT rolling-safe - the changelog carries a 'Note when rolling out': stop the old pod first and take a database backup; reading the converted columns requires the WAGGLE_SECRET_KEY the migration ran with. waggle rotate-keys now also re-encrypts the three content columns, on raw table clauses rather than the ORM (the mapped types decrypt with process settings, not --from/--to). docs/security.md gains the two domains and a 'Content at rest' section; the key-rotation runbook in deployment.md names the covered columns. Gates: 1981 backend tests at 100% coverage (including two new migration tests: in-place conversion of seeded plaintext, and bytea column types at head), 1177 vitest, vue-tsc + build, pinned pre-commit, full e2e suite green against a stack that boots through the new migration.
Encrypt card and outgoing content at rest - phase 1 of #189
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 57s
CI / pre-commit (pull_request) Successful in 58s
CI / frontend-test (pull_request) Successful in 1m0s
test-build / build (backend) (pull_request) Successful in 1m6s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m17s
CI / e2e (pull_request) Successful in 3m30s
f8c82565c9
cards.source_data (the message snapshot), cards.notes and
outgoing_posts.content become Fernet ciphertext under two new HKDF domains
(waggle-cardcontent-v1, waggle-outgoing-v1), decrypted transparently at the
ORM boundary (EncryptedJSON/EncryptedText over bytea). A database dump,
backup or SQL-injection read no longer yields message content. Feasible
without functional loss because nothing filters on these columns in SQL:
the note filter and retention checks already ran in Python, and the one
cheap SQL pre-filter on notes is gone.

The revision converts existing rows in place and is not rolling-safe (see
the changelog's rollout note). waggle rotate-keys covers the new columns on
raw bytes, since the mapped types decrypt with the process settings rather
than --from/--to.
robbertbos force-pushed phase-1-at-rest-encryption from f8c82565c9
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 57s
CI / pre-commit (pull_request) Successful in 58s
CI / frontend-test (pull_request) Successful in 1m0s
test-build / build (backend) (pull_request) Successful in 1m6s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m17s
CI / e2e (pull_request) Successful in 3m30s
to d93c086c3a
All checks were successful
CI / release-scripts (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 19s
security-scan / SBOM (trivy) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 44s
CI / frontend-test (pull_request) Successful in 1m22s
CI / pre-commit (pull_request) Successful in 1m29s
test-build / build (frontend) (pull_request) Successful in 1m14s
test-build / build (backend) (pull_request) Successful in 1m39s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 4m51s
CI / e2e (pull_request) Successful in 5m37s
CI / release-scripts (push) Successful in 7s
security-scan / SBOM (trivy) (push) Successful in 11s
security-scan / Filesystem scan (trivy fs) (push) Successful in 15s
security-scan / JS SCA (npm audit) (push) Successful in 17s
security-scan / Python SAST (bandit) (push) Successful in 18s
security-scan / Python SCA (pip-audit) (push) Successful in 35s
CI / pre-commit (push) Successful in 58s
publish-main / build (frontend) (push) Successful in 59s
CI / frontend-test (push) Successful in 1m0s
publish-main / build (backend) (push) Successful in 1m10s
publish-main / build (push) Successful in 0s
CI / backend-test (push) Successful in 2m17s
CI / e2e (push) Successful in 3m24s
2026-08-15 21:25:48 +00:00
Compare
robbertbos deleted branch phase-1-at-rest-encryption 2026-08-15 21:33:07 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!322
No description provided.