Skip to content

feat(rotation): add zero-downtime key rotation utility#147

Merged
Alberto-Codes merged 6 commits into
mainfrom
feat/rotation-4-4-zero-downtime-key-rotation
Mar 28, 2026
Merged

feat(rotation): add zero-downtime key rotation utility#147
Alberto-Codes merged 6 commits into
mainfrom
feat/rotation-4-4-zero-downtime-key-rotation

Conversation

@Alberto-Codes
Copy link
Copy Markdown
Owner

Before this PR, same-backend passphrase rotation (e.g., rotating a Fernet key) had no supported path — additional_backends raises ConfigurationError for duplicate backend_id values. This adds rotate_encryption_keys(), a standalone async utility that re-encrypts all session data directly via SQLAlchemy, bypassing the TypeDecorator to control which key decrypts.

  • Add rotate_encryption_keys(db_url, old_backend, new_backend) -> RotationResult in new rotation.py module; reads raw base64 TEXT, parses envelope, decrypts with old_backend, re-encrypts with new_backend, writes back with UPDATE WHERE update_time = :read_time optimistic concurrency guard
  • Add RotationResult(rotated, skipped) dataclass; re-runs are idempotent (already-rotated records carry new backend_id and are skipped silently)
  • Export both from public API (__init__.py __all__)
  • Add ADR-009 documenting two rotation paths (batch vs lazy), no-coordinator decision, update_time vs version column rationale
  • Add docs/how-to/key-rotation.md operator guide with Path A (lazy cross-backend via additional_backends) and Path B (batch via rotate_encryption_keys()) examples
  • Add 10 unit tests (mocked engine, covers NULL column, optimistic concurrency, backend-id filtering, error safety) and 6 integration tests (real SQLite + real FernetBackend keys)

Test: uv run pytest tests/unit/test_rotation.py tests/integration/test_key_rotation.py -v


PR Review

Checklist

  • Self-reviewed my code
  • Tests pass (uv run pytest)
  • Lint passes (uv run ruff check .)
  • Breaking changes use ! in title and BREAKING CHANGE: in body

Review Focus

  • rotation.py:_rotate_table — raw SQL SELECT/UPDATE loop; per-record asyncio.to_thread() crypto; optimistic concurrency via update_time; events table rowcount=0 treated as cascade-deleted (not skipped)
  • rotation.py:rotate_encryption_keys — four separate transactions (one per table); re-run idempotency contract in docstring
  • Party pre-review consensus: NULL event_data test, docstring idempotency/events semantics, and scalability note all addressed before this PR

Related

  • Story 3.3 (multi-backend coexistence) — Path A foundation (additional_backends)
  • ADR-007 (architecture migration) — why update_time is available as concurrency guard

Adds `rotate_encryption_keys()` — a standalone async function that
re-encrypts all session data across all four tables from one backend
to another. Designed for same-backend passphrase rotation where
`additional_backends` cannot be used (duplicate `backend_id`).

- Add `src/adk_secure_sessions/rotation.py` with `rotate_encryption_keys()`
  and `RotationResult` dataclass; raw SQLAlchemy engine bypass of
  TypeDecorator with per-record `asyncio.to_thread()` crypto and
  `UPDATE WHERE update_time = :read_time` optimistic concurrency guard
- Export `rotate_encryption_keys` and `RotationResult` from public API
- Add ADR-009 documenting batch vs lazy rotation paths, no-coordinator
  decision, and `update_time` over `version` column rationale
- Add `docs/how-to/key-rotation.md` operator guide covering both
  rotation paths with code examples and trade-offs
- Add 10 unit tests (mocked engine) and 6 integration tests (real SQLite)
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 98.71795% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/adk_secure_sessions/rotation.py 98.70% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Alberto-Codes Alberto-Codes marked this pull request as ready for review March 11, 2026 20:33
Copilot AI review requested due to automatic review settings March 11, 2026 20:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a first-class, operator-facing key rotation path for encrypted session data, complementing the existing additional_backends lazy migration path and documenting the operational strategy in an ADR and How-To guide.

Changes:

  • Introduce rotate_encryption_keys(db_url, old_backend, new_backend) -> RotationResult to batch re-encrypt data across sessions, events, app_states, and user_states.
  • Export the new rotation utility from the public package API and add unit + integration coverage.
  • Document key rotation strategy and operations guidance; update MkDocs navigation to include the new pages/ADRs.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/adk_secure_sessions/rotation.py Implements the async SQLAlchemy-based batch rotation utility and RotationResult.
src/adk_secure_sessions/init.py Exports rotate_encryption_keys and RotationResult via imports + __all__.
tests/unit/test_rotation.py Adds mocked-engine unit tests for rotation behaviors (filtering, concurrency, NULL handling, message safety).
tests/integration/test_key_rotation.py Adds SQLite + real backend integration tests for end-to-end rotation lifecycle.
docs/how-to/key-rotation.md Operator guide describing Path A (lazy) vs Path B (batch) rotation workflows.
docs/adr/ADR-009-key-rotation-strategy.md ADR documenting the two-path strategy and design rationale (no coordinator, update_time guard).
mkdocs.yml Wires the new How-To page and ADR entries into the docs nav.
.claude/rules/conventions.md Adds explicit guidance about sync/async boundary review risk.
.claude/rules/dev-quality-checklist.md Adds benchmark-claim hygiene items to the quality checklist.
_bmad/bmm/workflows/4-implementation/code-review/checklist.md Adds a checklist item about numeric claims being measurement-derived.
_bmad-output/test-artifacts/test-review.md Tracks story assignment note for an existing test-review recommendation.
_bmad-output/implementation-artifacts/sprint-status.yaml Updates sprint generation metadata and story status for Epic 4 / Story 4.4.
_bmad-output/implementation-artifacts/4-4-zero-downtime-key-rotation.md Story artifact capturing ACs, implementation notes, and mapping to tests/docs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/adk_secure_sessions/rotation.py Outdated
Comment thread src/adk_secure_sessions/rotation.py Outdated
Comment thread src/adk_secure_sessions/rotation.py Outdated
Comment thread src/adk_secure_sessions/rotation.py
Comment thread docs/how-to/key-rotation.md Outdated
Comment thread docs/how-to/key-rotation.md Outdated
Comment thread tests/integration/test_key_rotation.py Outdated
- Fix idempotency overclaim: re-runs are only safe for cross-backend
  rotation; for same-backend, a single pass is expected — document
  clearly in docstring and how-to guide
- Change `raise DecryptionError(msg) from exc` to `from None` to
  prevent key material leakage via exception chaining (NFR6); consistent
  with existing codebase pattern in backends and type_decorator
- Add ciphertext guard to UPDATE WHERE clause: `AND {enc_col} = :old_val`
  prevents silent overwrites when concurrent writes collide within the
  same timestamp window (SQLite low-precision risk)
- Rename integration test to `test_can_rotate_back_between_fernet_backends`
  with accurate docstring describing reverse-rotation scenario
- Add unit test verifying old_val is passed in UPDATE params (T011)
- Rename `## See Also` → `## Related` in how-to guide (doc convention)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/integration/test_key_rotation.py Outdated
Comment thread src/adk_secure_sessions/rotation.py Outdated
…d guarantee fixture teardown

- Add UnicodeEncodeError to base64 decode except clause so corrupted
  non-ASCII TEXT rows raise DecryptionError instead of leaking the
  raw UnicodeEncodeError to callers
- Wrap populated_old_key_db fixture setup in try/finally to guarantee
  svc.close() runs even if an append_event call raises
- Add unit test (T012) for the non-ASCII → DecryptionError path
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/how-to/key-rotation.md
Comment thread src/adk_secure_sessions/rotation.py
Comment thread tests/integration/test_key_rotation.py Outdated
Comment thread docs/how-to/key-rotation.md Outdated
Split Security Notes in how-to guide into Path A (zero-downtime) and
Path B (requires service stop). Add pre-condition to rotation.py
docstring. Rename test module docstring from "zero-downtime" to "batch".
Replace Dependabot with Renovate for dependency management, matching
sister projects. Add uv-secure config to ignore GHSA-5239-wwwm-4pmq
(Pygments ReDoS, low severity, no fix available). Selectively upgrade
7 transitive deps with known CVEs without bumping google-adk.
Three contracts: layered architecture (protocols/exceptions →
serialization → backends/rotation → services), backend independence
(fernet and aes_gcm don't cross-import), and rotation isolation (no
backend or service imports). Runs in pre-commit and CI lint job.
@Alberto-Codes Alberto-Codes merged commit cbb6c40 into main Mar 28, 2026
9 checks passed
@Alberto-Codes Alberto-Codes deleted the feat/rotation-4-4-zero-downtime-key-rotation branch March 28, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants