diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3d515..d4e062d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.1] - 2026-02-19 + +### Fixed +- Fixed master key loading from `MASTER_KEYS` so decoded key material remains usable after secure buffer zeroing +- Fixed `MasterKeyChain.Close()` to zero all in-memory master keys before clearing chain state + +### Security +- Hardened master key memory lifecycle by zeroing temporary decode buffers and keychain-resident keys on teardown +- Added regression tests for key usability-after-load and key zeroing-on-close behavior + +### Documentation +- Added `docs/releases/v0.5.1.md` release notes and `docs/releases/v0.5.1-upgrade.md` upgrade guide +- Updated current release references and pinned examples to `v0.5.1` + ## [0.5.0] - 2026-02-19 ### Added diff --git a/README.md b/README.md index 483a61b..6f2383f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Secrets is inspired by **HashiCorp Vault** โค๏ธ, but it is intentionally **muc The default way to run Secrets is the published Docker image: ```bash -docker pull allisson/secrets:v0.5.0 +docker pull allisson/secrets:v0.5.1 ``` Use pinned tags for reproducible setups. `latest` is also available for fast iteration. @@ -29,15 +29,20 @@ Then follow the Docker setup guide in [docs/getting-started/docker.md](docs/gett 1. ๐Ÿณ **Run with Docker image (recommended)**: [docs/getting-started/docker.md](docs/getting-started/docker.md) 2. ๐Ÿ’ป **Run locally for development**: [docs/getting-started/local-development.md](docs/getting-started/local-development.md) -## ๐Ÿ†• What's New in v0.5.0 +## ๐Ÿ†• What's New in v0.5.1 -- ๐Ÿ›ก๏ธ Added per-client rate limiting for authenticated API endpoints -- ๐ŸŒ Added configurable CORS support (disabled by default) -- โฑ๏ธ Changed default token expiration from 24h to 4h for stronger security -- ๐Ÿ” Added comprehensive security hardening guide: [docs/operations/security-hardening.md](docs/operations/security-hardening.md) -- ๐Ÿ“˜ Added release notes: [docs/releases/v0.5.0.md](docs/releases/v0.5.0.md) -- โฌ†๏ธ Added upgrade guide: [docs/releases/v0.5.0-upgrade.md](docs/releases/v0.5.0-upgrade.md) -- ๐Ÿ“ฆ Updated pinned Docker docs/examples to `allisson/secrets:v0.5.0` +- ๐Ÿ› ๏ธ Fixed master key loading to preserve usable key material while zeroing temporary decoded buffers +- ๐Ÿงน Hardened keychain teardown to zero in-memory master keys before clearing chain state +- ๐Ÿ”’ Expanded regression coverage for master key memory lifecycle and close behavior +- ๐Ÿ“˜ Added release notes: [docs/releases/v0.5.1.md](docs/releases/v0.5.1.md) +- โฌ†๏ธ Added upgrade guide: [docs/releases/v0.5.1-upgrade.md](docs/releases/v0.5.1-upgrade.md) +- ๐Ÿ“ฆ Updated pinned Docker docs/examples to `allisson/secrets:v0.5.1` + +Release history quick links: + +- Current: [v0.5.1 release notes](docs/releases/v0.5.1.md) +- Previous: [v0.5.0 release notes](docs/releases/v0.5.0.md) +- Previous upgrade guide: [v0.5.0 upgrade guide](docs/releases/v0.5.0-upgrade.md) ## ๐Ÿ“š Docs Map @@ -48,8 +53,8 @@ Then follow the Docker setup guide in [docs/getting-started/docker.md](docs/gett - ๐Ÿงฐ **Troubleshooting**: [docs/getting-started/troubleshooting.md](docs/getting-started/troubleshooting.md) - โœ… **Smoke test script**: [docs/getting-started/smoke-test.md](docs/getting-started/smoke-test.md) - ๐Ÿงช **CLI commands reference**: [docs/cli/commands.md](docs/cli/commands.md) -- ๐Ÿš€ **v0.5.0 release notes**: [docs/releases/v0.5.0.md](docs/releases/v0.5.0.md) -- โฌ†๏ธ **v0.5.0 upgrade guide**: [docs/releases/v0.5.0-upgrade.md](docs/releases/v0.5.0-upgrade.md) +- ๐Ÿš€ **v0.5.1 release notes**: [docs/releases/v0.5.1.md](docs/releases/v0.5.1.md) +- โฌ†๏ธ **v0.5.1 upgrade guide**: [docs/releases/v0.5.1-upgrade.md](docs/releases/v0.5.1-upgrade.md) - ๐Ÿ” **Release compatibility matrix**: [docs/releases/compatibility-matrix.md](docs/releases/compatibility-matrix.md) - **By Topic** diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4fbf842..7fc81ac 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,24 @@ > Last updated: 2026-02-19 +## 2026-02-19 (docs v11 - v0.5.1 patch release prep) + +- Added release notes page: `docs/releases/v0.5.1.md` +- Added upgrade guide: `docs/releases/v0.5.1-upgrade.md` +- Updated docs metadata source (`docs/metadata.json`) to `current_release: v0.5.1` +- Updated root README and docs index to promote `v0.5.1` release links +- Updated operator runbook index and production runbooks to reference `v0.5.1` +- Updated compatibility matrix with `v0.5.0 -> v0.5.1` patch upgrade path +- Added direct `v0.4.x -> v0.5.1` compatibility path for skip-upgrade operators +- Updated pinned Docker image examples from `allisson/secrets:v0.5.0` to `allisson/secrets:v0.5.1` +- Updated API docs release labels to `v0.5.1` where current-release references are shown +- Reduced patch-version churn in OpenAPI coverage notes by using current-release wording +- Added v0.5.1-specific master-key regression triage note to troubleshooting +- Added copy/paste quick verification commands to `docs/releases/v0.5.1-upgrade.md` +- Added patch-release safety note to `docs/releases/v0.5.1.md` +- Added release history quick links in root `README.md` +- Added runtime version fingerprint checks for mixed deployment triage + ## 2026-02-19 (docs v10 - v0.5.0 security hardening release prep) - Added comprehensive security hardening guide: `docs/operations/security-hardening.md` diff --git a/docs/README.md b/docs/README.md index d8b68f9..6026e30 100644 --- a/docs/README.md +++ b/docs/README.md @@ -70,14 +70,16 @@ Welcome to the full documentation for Secrets. Pick a path and dive in ๐Ÿš€ OpenAPI scope note: -- `openapi.yaml` is a baseline subset for common API flows in `v0.5.0` +- `openapi.yaml` is a baseline subset for common API flows in the current release (`docs/metadata.json`) - Full endpoint behavior is documented in the endpoint pages under `docs/api/` -- Tokenization endpoints are included in `openapi.yaml` for `v0.5.0` +- Tokenization endpoints are included in `openapi.yaml` for the current release ## ๐Ÿš€ Releases -- ๐Ÿ“ฆ [releases/v0.5.0.md](releases/v0.5.0.md) -- โฌ†๏ธ [releases/v0.5.0-upgrade.md](releases/v0.5.0-upgrade.md) +- ๐Ÿ“ฆ [releases/v0.5.1.md](releases/v0.5.1.md) +- โฌ†๏ธ [releases/v0.5.1-upgrade.md](releases/v0.5.1-upgrade.md) +- ๐Ÿ“ฆ [releases/v0.5.0.md](releases/v0.5.0.md) (historical) +- โฌ†๏ธ [releases/v0.5.0-upgrade.md](releases/v0.5.0-upgrade.md) (historical) - ๐Ÿ” [releases/compatibility-matrix.md](releases/compatibility-matrix.md) - ๐Ÿ“ฆ [releases/v0.4.1.md](releases/v0.4.1.md) (historical) - ๐Ÿ“ฆ [releases/v0.4.0.md](releases/v0.4.0.md) (historical) diff --git a/docs/api/tokenization.md b/docs/api/tokenization.md index 4f6b0f7..3ff0e7f 100644 --- a/docs/api/tokenization.md +++ b/docs/api/tokenization.md @@ -14,7 +14,7 @@ with optional deterministic behavior and token lifecycle management. OpenAPI coverage note: -- Tokenization endpoint coverage is included in `docs/openapi.yaml` for `v0.5.0` +- Tokenization endpoint coverage is included in `docs/openapi.yaml` for the current release - This page remains the most detailed contract reference with examples and operational guidance All endpoints require `Authorization: Bearer `. diff --git a/docs/api/versioning-policy.md b/docs/api/versioning-policy.md index 60b5d99..356443d 100644 --- a/docs/api/versioning-policy.md +++ b/docs/api/versioning-policy.md @@ -11,17 +11,17 @@ This page defines compatibility expectations for HTTP API changes. - Existing endpoint paths and JSON field names are treated as stable unless explicitly deprecated - OpenAPI source of truth: `docs/openapi.yaml` -## OpenAPI Coverage (v0.5.0) +## OpenAPI Coverage - `docs/openapi.yaml` is a baseline subset focused on high-traffic/common integration flows -- `docs/openapi.yaml` includes tokenization endpoint coverage in `v0.5.0` +- `docs/openapi.yaml` includes tokenization endpoint coverage in the current release - `docs/openapi.yaml` includes `429 Too Many Requests` response modeling for protected routes - Endpoint pages in `docs/api/*.md` define full public behavior for covered operations - Endpoints may exist in runtime before they are expanded in OpenAPI detail ## App Version vs API Version -- Application release `v0.5.0` is pre-1.0 software and may evolve quickly +- Application release is pre-1.0 software and may evolve quickly - API v1 path contract (`/v1/*`) remains the compatibility baseline for consumers - Breaking API behavior changes require explicit documentation and migration notes diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 7d83e8f..13db793 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -12,10 +12,10 @@ Local binary: ./bin/app [flags] ``` -Docker image (v0.5.0): +Docker image (v0.5.1): ```bash -docker run --rm --env-file .env allisson/secrets:v0.5.0 [flags] +docker run --rm --env-file .env allisson/secrets:v0.5.1 [flags] ``` ## Core Runtime @@ -33,7 +33,7 @@ Local: Docker: ```bash -docker run --rm --network secrets-net --env-file .env -p 8080:8080 allisson/secrets:v0.5.0 server +docker run --rm --network secrets-net --env-file .env -p 8080:8080 allisson/secrets:v0.5.1 server ``` ### `migrate` @@ -49,7 +49,7 @@ Local: Docker: ```bash -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 migrate +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 migrate ``` ## Key Management @@ -71,7 +71,7 @@ Local: Docker: ```bash -docker run --rm allisson/secrets:v0.5.0 create-master-key --id default +docker run --rm allisson/secrets:v0.5.1 create-master-key --id default ``` ### `create-kek` @@ -91,7 +91,7 @@ Local: Docker: ```bash -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 create-kek --algorithm aes-gcm +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 create-kek --algorithm aes-gcm ``` ### `rotate-kek` @@ -111,7 +111,7 @@ Local: Docker: ```bash -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 rotate-kek --algorithm aes-gcm +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 rotate-kek --algorithm aes-gcm ``` After master key or KEK rotation, restart API server instances so they load updated key material. @@ -138,7 +138,7 @@ Examples: --deterministic \ --algorithm aes-gcm -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 \ +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 \ create-tokenization-key --name payment-cards --format luhn-preserving --deterministic --algorithm aes-gcm ``` @@ -162,7 +162,7 @@ Examples: --deterministic \ --algorithm chacha20-poly1305 -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 \ +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 \ rotate-tokenization-key --name payment-cards --format luhn-preserving --deterministic --algorithm chacha20-poly1305 ``` @@ -186,7 +186,7 @@ Examples: ./bin/app clean-expired-tokens --days 30 --format text # Docker form -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 \ +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 \ clean-expired-tokens --days 30 --dry-run --format json ``` @@ -269,7 +269,7 @@ Examples: ./bin/app clean-audit-logs --days 90 --format text # Docker form -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 \ +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 \ clean-audit-logs --days 90 --dry-run --format json ``` diff --git a/docs/getting-started/docker.md b/docs/getting-started/docker.md index 4a00d5e..2ae32dd 100644 --- a/docs/getting-started/docker.md +++ b/docs/getting-started/docker.md @@ -4,25 +4,27 @@ This is the default way to run Secrets. -For release reproducibility, this guide uses the pinned image tag `allisson/secrets:v0.5.0`. +For release reproducibility, this guide uses the pinned image tag `allisson/secrets:v0.5.1`. You can use `allisson/secrets:latest` for fast iteration. **โš ๏ธ Security Warning:** This guide is for **development and testing only**. For production deployments, see [Security Hardening Guide](../operations/security-hardening.md) and [Production Deployment Guide](../operations/production.md). -## v0.5.0 Security Defaults +## Current Security Defaults -- `AUTH_TOKEN_EXPIRATION_SECONDS` default is now `14400` (4 hours) +- `AUTH_TOKEN_EXPIRATION_SECONDS` default is `14400` (4 hours) - `RATE_LIMIT_ENABLED` default is `true` (per authenticated client) - `CORS_ENABLED` default is `false` -If upgrading from `v0.4.x`, review [v0.5.0 upgrade guide](../releases/v0.5.0-upgrade.md). +These defaults were introduced in `v0.5.0` and remain unchanged in `v0.5.1`. + +If upgrading from `v0.5.0`, review [v0.5.1 upgrade guide](../releases/v0.5.1-upgrade.md). ## โšก Quickstart Copy Block Use this minimal flow when you just want to get a working instance quickly: ```bash -docker pull allisson/secrets:v0.5.0 +docker pull allisson/secrets:v0.5.1 docker network create secrets-net || true docker run -d --name secrets-postgres --network secrets-net \ @@ -31,19 +33,19 @@ docker run -d --name secrets-postgres --network secrets-net \ -e POSTGRES_DB=mydb \ postgres:16-alpine -docker run --rm allisson/secrets:v0.5.0 create-master-key --id default +docker run --rm allisson/secrets:v0.5.1 create-master-key --id default # copy generated MASTER_KEYS and ACTIVE_MASTER_KEY_ID into .env -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 migrate -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 create-kek --algorithm aes-gcm +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 migrate +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 create-kek --algorithm aes-gcm docker run --rm --name secrets-api --network secrets-net --env-file .env -p 8080:8080 \ - allisson/secrets:v0.5.0 server + allisson/secrets:v0.5.1 server ``` ## 1) Pull the image ```bash -docker pull allisson/secrets:v0.5.0 +docker pull allisson/secrets:v0.5.1 ``` ## 2) Start PostgreSQL @@ -61,7 +63,7 @@ docker run -d --name secrets-postgres --network secrets-net \ ## 3) Generate a master key ```bash -docker run --rm allisson/secrets:v0.5.0 create-master-key --id default +docker run --rm allisson/secrets:v0.5.1 create-master-key --id default ``` Copy the generated values into a local `.env` file. @@ -93,15 +95,15 @@ EOF ## 5) Run migrations and bootstrap KEK ```bash -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 migrate -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 create-kek --algorithm aes-gcm +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 migrate +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 create-kek --algorithm aes-gcm ``` ## 6) Start the API server ```bash docker run --rm --name secrets-api --network secrets-net --env-file .env -p 8080:8080 \ - allisson/secrets:v0.5.0 server + allisson/secrets:v0.5.1 server ``` ## 7) Verify @@ -121,7 +123,7 @@ Expected: Use the CLI command to create your first API client and policy set: ```bash -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 create-client \ +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 create-client \ --name bootstrap-admin \ --active \ --policies '[{"path":"*","capabilities":["read","write","delete","encrypt","decrypt","rotate"]}]' \ diff --git a/docs/getting-started/local-development.md b/docs/getting-started/local-development.md index 75ae86a..59eb920 100644 --- a/docs/getting-started/local-development.md +++ b/docs/getting-started/local-development.md @@ -6,13 +6,15 @@ Use this path if you want to modify the source code and run from your workstatio **โš ๏ธ Security Warning:** This guide is for **development and testing only**. For production deployments, see [Security Hardening Guide](../operations/security-hardening.md) and [Production Deployment Guide](../operations/production.md). -## v0.5.0 Security Defaults +## Current Security Defaults -- `AUTH_TOKEN_EXPIRATION_SECONDS` default is now `14400` (4 hours) +- `AUTH_TOKEN_EXPIRATION_SECONDS` default is `14400` (4 hours) - `RATE_LIMIT_ENABLED` default is `true` (per authenticated client) - `CORS_ENABLED` default is `false` -If upgrading from `v0.4.x`, review [v0.5.0 upgrade guide](../releases/v0.5.0-upgrade.md). +These defaults were introduced in `v0.5.0` and remain unchanged in `v0.5.1`. + +If upgrading from `v0.5.0`, review [v0.5.1 upgrade guide](../releases/v0.5.1-upgrade.md). ## Prerequisites diff --git a/docs/getting-started/smoke-test.md b/docs/getting-started/smoke-test.md index 4936d23..299d926 100644 --- a/docs/getting-started/smoke-test.md +++ b/docs/getting-started/smoke-test.md @@ -56,5 +56,5 @@ If transit decrypt fails with `422`, see [Troubleshooting](troubleshooting.md#42 - [Docker getting started](docker.md) - [Local development](local-development.md) - [Troubleshooting](troubleshooting.md) -- [v0.5.0 release notes](../releases/v0.5.0.md) +- [v0.5.1 release notes](../releases/v0.5.1.md) - [Curl examples](../examples/curl.md) diff --git a/docs/getting-started/troubleshooting.md b/docs/getting-started/troubleshooting.md index b5c273a..304ff1a 100644 --- a/docs/getting-started/troubleshooting.md +++ b/docs/getting-started/troubleshooting.md @@ -18,6 +18,7 @@ Use this quick route before diving into detailed sections: 8. Startup fails with key config errors -> go to `Missing or Invalid Master Keys` 9. Monitoring data is missing -> go to `Metrics Troubleshooting Matrix` 10. Tokenization endpoints fail after upgrade -> go to `Tokenization migration verification` +11. Master key loads but key-dependent crypto fails -> go to `Master key load regression triage (v0.5.1)` ## ๐Ÿ“‘ Table of Contents @@ -31,6 +32,7 @@ Use this quick route before diving into detailed sections: - [Database connection failure](#database-connection-failure) - [Migration failure](#migration-failure) - [Missing or Invalid Master Keys](#missing-or-invalid-master-keys) +- [Master key load regression triage (v0.5.1)](#master-key-load-regression-triage-v051) - [Missing KEK](#missing-kek) - [Metrics Troubleshooting Matrix](#metrics-troubleshooting-matrix) - [Tokenization migration verification](#tokenization-migration-verification) @@ -197,6 +199,22 @@ If CORS is disabled or origin is not allowed, browser requests can fail even if - decoded key must be exactly 32 bytes - ensure `ACTIVE_MASTER_KEY_ID` exists in `MASTER_KEYS` +## Master key load regression triage (v0.5.1) + +- Symptom: startup succeeds, but key-dependent operations fail unexpectedly after a recent rollout +- Likely cause: running a pre-`v0.5.1` build where decoded master key buffers could be zeroed too early +- Mixed-version rollout symptom: some requests pass while others fail if old and new images are serving traffic together +- Version fingerprint checks: + - local binary: `./bin/app --version` + - pinned image check: `docker run --rm allisson/secrets:v0.5.1 --version` + - running containers: `docker ps --format 'table {{.Names}}\t{{.Image}}'` +- Fix: + - upgrade to `v0.5.1` or newer + - restart API instances after deploy + - run key-dependent smoke checks (token issuance, secrets write/read, transit round-trip) + - review [v0.5.1 release notes](../releases/v0.5.1.md) and + [v0.5.1 upgrade guide](../releases/v0.5.1-upgrade.md) + ## Missing KEK - Symptom: secret write/transit operations fail after migration @@ -220,7 +238,7 @@ If CORS is disabled or origin is not allowed, browser requests can fail even if - Symptom: tokenization endpoints return `404`/`500` after upgrading to `v0.4.x` - Likely cause: tokenization migration (`000002_add_tokenization`) not applied or partially applied - Fix: - - run `./bin/app migrate` (or Docker `... allisson/secrets:v0.5.0 migrate`) + - run `./bin/app migrate` (or Docker `... allisson/secrets:v0.5.1 migrate`) - verify migration logs indicate `000002_add_tokenization` applied for your DB - confirm initial KEK exists (`create-kek` if missing) - re-run smoke flow for tokenization (`tokenize -> detokenize -> validate -> revoke`) diff --git a/docs/metadata.json b/docs/metadata.json index ac94999..64af35f 100644 --- a/docs/metadata.json +++ b/docs/metadata.json @@ -1,5 +1,5 @@ { - "current_release": "v0.5.0", + "current_release": "v0.5.1", "api_version": "v1", "last_docs_refresh": "2026-02-19" } diff --git a/docs/operations/production-rollout.md b/docs/operations/production-rollout.md index 329968a..4ed96d1 100644 --- a/docs/operations/production-rollout.md +++ b/docs/operations/production-rollout.md @@ -6,7 +6,7 @@ Use this runbook for a standard production rollout with verification and rollbac ## Scope -- Deploy target: Secrets `v0.5.0` +- Deploy target: Secrets `v0.5.1` - Database schema changes: run migrations before traffic cutover - Crypto bootstrap: ensure initial KEK exists for write/encrypt flows @@ -23,17 +23,17 @@ Use this runbook for a standard production rollout with verification and rollbac ```bash # 1) Pull target release -docker pull allisson/secrets:v0.5.0 +docker pull allisson/secrets:v0.5.1 # 2) Run migrations -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 migrate +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 migrate # 3) Bootstrap KEK only for first-time environment setup -docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.0 create-kek --algorithm aes-gcm +docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.5.1 create-kek --algorithm aes-gcm # 4) Start API docker run --rm --name secrets-api --network secrets-net --env-file .env -p 8080:8080 \ - allisson/secrets:v0.5.0 server + allisson/secrets:v0.5.1 server ``` ## Verification Gates @@ -81,7 +81,7 @@ Gate C (policy and observability): ## See also - [Production deployment guide](production.md) -- [v0.5.0 release notes](../releases/v0.5.0.md) -- [v0.5.0 upgrade guide](../releases/v0.5.0-upgrade.md) +- [v0.5.1 release notes](../releases/v0.5.1.md) +- [v0.5.1 upgrade guide](../releases/v0.5.1-upgrade.md) - [Release compatibility matrix](../releases/compatibility-matrix.md) - [Smoke test guide](../getting-started/smoke-test.md) diff --git a/docs/operations/production.md b/docs/operations/production.md index 5d08f95..6d8a98b 100644 --- a/docs/operations/production.md +++ b/docs/operations/production.md @@ -164,7 +164,7 @@ Adjust retention to match your compliance and incident-response requirements. - Follow [Production rollout golden path](production-rollout.md) for step-by-step deployment, verification gates, and rollback triggers - Use [Release compatibility matrix](../releases/compatibility-matrix.md) before planning upgrades -- Keep [v0.5.0 upgrade guide](../releases/v0.5.0-upgrade.md) attached to rollout change tickets +- Keep [v0.5.1 upgrade guide](../releases/v0.5.1-upgrade.md) attached to rollout change tickets ## See also @@ -175,8 +175,8 @@ Adjust retention to match your compliance and incident-response requirements. - [Monitoring](monitoring.md) - [Operator drills (quarterly)](operator-drills.md) - [Policy smoke tests](policy-smoke-tests.md) -- [v0.5.0 release notes](../releases/v0.5.0.md) -- [v0.5.0 upgrade guide](../releases/v0.5.0-upgrade.md) +- [v0.5.1 release notes](../releases/v0.5.1.md) +- [v0.5.1 upgrade guide](../releases/v0.5.1-upgrade.md) - [Release compatibility matrix](../releases/compatibility-matrix.md) - [Environment variables](../configuration/environment-variables.md) - [Security model](../concepts/security-model.md) diff --git a/docs/operations/runbook-index.md b/docs/operations/runbook-index.md index 0f1602d..e91b054 100644 --- a/docs/operations/runbook-index.md +++ b/docs/operations/runbook-index.md @@ -6,8 +6,8 @@ Use this page as the single entry point for rollout, validation, and incident ru ## Release and Rollout -- [v0.5.0 release notes](../releases/v0.5.0.md) -- [v0.5.0 upgrade guide](../releases/v0.5.0-upgrade.md) +- [v0.5.1 release notes](../releases/v0.5.1.md) +- [v0.5.1 upgrade guide](../releases/v0.5.1-upgrade.md) - [Release compatibility matrix](../releases/compatibility-matrix.md) - [Production rollout golden path](production-rollout.md) - [Production deployment guide](production.md) diff --git a/docs/releases/compatibility-matrix.md b/docs/releases/compatibility-matrix.md index a7a64f5..2c7f05c 100644 --- a/docs/releases/compatibility-matrix.md +++ b/docs/releases/compatibility-matrix.md @@ -8,11 +8,19 @@ Use this page to understand upgrade impact between recent releases. | From -> To | Schema migration impact | Runtime/default changes | Required operator action | | --- | --- | --- | --- | +| `v0.5.0 -> v0.5.1` | No new mandatory migration | Master key memory handling bugfix and teardown zeroing hardening | Deploy `v0.5.1` and verify key-dependent flows (token, secrets, transit) | +| `v0.4.x -> v0.5.1` | No new destructive schema migration required for core features | Token TTL default `24h -> 4h`; rate limiting enabled by default; CORS config introduced (disabled by default); includes `v0.5.1` master key memory handling hardening | Set explicit `AUTH_TOKEN_EXPIRATION_SECONDS`, review `RATE_LIMIT_*`, configure `CORS_*` only if browser access is required, then run key-dependent smoke checks | | `v0.4.0 -> v0.4.1` | No new mandatory migration beyond v0.4.0 baseline | Policy matcher bugfix and docs alignment | Update image tag and validate policy wildcard behavior | | `v0.4.x -> v0.5.0` | No new destructive schema migration required for core features | Token TTL default `24h -> 4h`; rate limiting enabled by default; CORS config introduced (disabled by default) | Set explicit `AUTH_TOKEN_EXPIRATION_SECONDS`, review `RATE_LIMIT_*`, configure `CORS_*` only if browser access is required | ## Upgrade verification by target +For `v0.5.1`: + +1. `GET /health` and `GET /ready` pass +2. `POST /v1/token` issues tokens successfully +3. Secrets and transit round-trip flows succeed without key configuration errors + For `v0.5.0`: 1. `GET /health` and `GET /ready` pass @@ -28,6 +36,8 @@ For `v0.5.0`: ## See also +- [v0.5.1 release notes](v0.5.1.md) +- [v0.5.1 upgrade guide](v0.5.1-upgrade.md) - [v0.5.0 release notes](v0.5.0.md) - [v0.5.0 upgrade guide](v0.5.0-upgrade.md) - [Production rollout golden path](../operations/production-rollout.md) diff --git a/docs/releases/v0.5.1-upgrade.md b/docs/releases/v0.5.1-upgrade.md new file mode 100644 index 0000000..b843715 --- /dev/null +++ b/docs/releases/v0.5.1-upgrade.md @@ -0,0 +1,82 @@ +# โฌ†๏ธ Upgrade Guide: v0.5.0 -> v0.5.1 + +> Release date: 2026-02-19 + +Use this guide to safely upgrade from `v0.5.0` to `v0.5.1`. + +## Scope + +- Release type: patch (`v0.5.1`) +- API compatibility: no `v1` contract changes +- Database migration: no new mandatory migration for this patch + +## What Changed + +- Fixed master key loading from `MASTER_KEYS` to preserve active key material after decode +- Added secure zeroing of all keychain-held master keys during `Close` +- Added regression test coverage for these memory lifecycle paths + +## Recommended Upgrade Steps + +1. Update image/binary to `v0.5.1` +2. Restart API instances with standard rolling rollout process +3. Run baseline checks: + - `GET /health` + - `GET /ready` +4. Run key-dependent smoke checks: + - `POST /v1/token` + - Secrets write/read + - Transit encrypt/decrypt round-trip + +## Quick Verification Commands + +Use these after rollout to validate key-dependent paths quickly: + +```bash +curl -sS http://localhost:8080/health + +TOKEN_RESPONSE="$(curl -sS -X POST http://localhost:8080/v1/token \ + -H "Content-Type: application/json" \ + -d '{"client_id":"","client_secret":""}')" + +CLIENT_TOKEN="$(printf '%s' "${TOKEN_RESPONSE}" | jq -r '.token')" + +curl -sS -X POST http://localhost:8080/v1/secrets/upgrade/smoke \ + -H "Authorization: Bearer ${CLIENT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"value":"c21va2UtdjA1MQ=="}' + +curl -sS -X GET http://localhost:8080/v1/secrets/upgrade/smoke \ + -H "Authorization: Bearer ${CLIENT_TOKEN}" + +# Transit round-trip using an existing transit key name +# If the key does not exist yet in this environment, create it first: +# curl -sS -X POST http://localhost:8080/v1/transit/keys \ +# -H "Authorization: Bearer ${CLIENT_TOKEN}" \ +# -H "Content-Type: application/json" \ +# -d '{"name":"","algorithm":"aes-gcm"}' +curl -sS -X POST http://localhost:8080/v1/transit/keys//encrypt \ + -H "Authorization: Bearer ${CLIENT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"plaintext":"dHJhbnNpdC12MDUxLXNtb2tl"}' + +# Use ciphertext returned by the previous encrypt call +curl -sS -X POST http://localhost:8080/v1/transit/keys//decrypt \ + -H "Authorization: Bearer ${CLIENT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"ciphertext":":"}' + +# Expect decrypted plaintext to equal: dHJhbnNpdC12MDUxLXNtb2tl +``` + +## Rollback Notes + +- If rollback is required, revert API instances to the last known stable image +- Keep additive schema migrations applied unless a validated rollback plan exists +- Re-run health and smoke checks after rollback + +## See also + +- [v0.5.1 release notes](v0.5.1.md) +- [Release compatibility matrix](compatibility-matrix.md) +- [Production rollout golden path](../operations/production-rollout.md) diff --git a/docs/releases/v0.5.1.md b/docs/releases/v0.5.1.md new file mode 100644 index 0000000..082b334 --- /dev/null +++ b/docs/releases/v0.5.1.md @@ -0,0 +1,60 @@ +# ๐Ÿš€ Secrets v0.5.1 Release Notes + +> Release date: 2026-02-19 + +This patch release fixes master key memory handling to keep loaded key material usable while +preserving secure zeroing behavior for temporary and teardown paths. + +## Highlights + +- Fixed master key loading from environment variables to avoid zeroing the in-use key slice +- Hardened keychain shutdown by zeroing all master keys before clearing chain state +- Added regression tests for key usability after load and secure zeroing on close + +## Fixes + +- `LoadMasterKeyChainFromEnv` now stores a copy of decoded key bytes before zeroing temporary buffers +- `MasterKeyChain.Close` now zeros every loaded master key before clearing the key map + +## Security Impact + +- Reduces risk of leaked key material remaining in temporary decode buffers +- Ensures explicit in-memory zeroing of master keys during keychain teardown + +## Runtime and Compatibility + +- API baseline remains `v1` (`/v1/*`) +- No endpoint, payload, or status code contract changes +- No schema migrations required specifically for this patch release + +## Patch Release Safety + +- Most environments require no configuration changes for this release +- Rolling upgrade is recommended; keep standard health and smoke checks in place +- Rollback to the previous stable image is safe when incident criteria are met + +## Upgrade Notes + +1. Deploy binaries/images with `v0.5.1` +2. Run standard health checks (`GET /health`, `GET /ready`) +3. Validate key-dependent flows (token issuance, secrets write/read, transit encrypt/decrypt) + +## Operator Verification Checklist + +1. Confirm service health and readiness after rollout +2. Confirm startup succeeds with configured `MASTER_KEYS` and `ACTIVE_MASTER_KEY_ID` +3. Confirm secrets and transit workflows succeed under normal traffic +4. Confirm no unexpected key configuration or decryption errors in logs + +## Documentation Updates + +- Added [v0.5.1 upgrade guide](v0.5.1-upgrade.md) +- Updated [release compatibility matrix](compatibility-matrix.md) with `v0.5.0 -> v0.5.1` +- Updated current-release references across docs and pinned image examples to `v0.5.1` + +## See also + +- [v0.5.1 upgrade guide](v0.5.1-upgrade.md) +- [Release compatibility matrix](compatibility-matrix.md) +- [Key management operations](../operations/key-management.md) +- [Security model](../concepts/security-model.md) diff --git a/internal/crypto/domain/master_key.go b/internal/crypto/domain/master_key.go index 44f61c5..e2360ae 100644 --- a/internal/crypto/domain/master_key.go +++ b/internal/crypto/domain/master_key.go @@ -37,8 +37,15 @@ func (m *MasterKeyChain) Get(id string) (*MasterKey, bool) { return nil, false } -// Close securely clears all master keys from memory and resets the keychain. +// Close securely zeros all master keys from memory, clears the chain, and resets the active ID. func (m *MasterKeyChain) Close() { + // Zero all master keys before clearing the chain + m.keys.Range(func(key, value interface{}) bool { + if masterKey, ok := value.(*MasterKey); ok { + Zero(masterKey.Key) + } + return true + }) m.activeID = "" m.keys.Clear() } @@ -82,7 +89,13 @@ func LoadMasterKeyChainFromEnv() (*MasterKeyChain, error) { len(key), ) } - mkc.keys.Store(id, &MasterKey{ID: id, Key: key}) + // Make a copy of the key data before storing to prevent premature zeroing. + // The original 'key' slice will be zeroed for security, but the keychain + // needs its own copy to remain functional. + keyCopy := make([]byte, len(key)) + copy(keyCopy, key) + mkc.keys.Store(id, &MasterKey{ID: id, Key: keyCopy}) + // Zero the original decoded key to prevent memory dumps Zero(key) } diff --git a/internal/crypto/domain/master_key_test.go b/internal/crypto/domain/master_key_test.go index 02500ad..5ddc7b9 100644 --- a/internal/crypto/domain/master_key_test.go +++ b/internal/crypto/domain/master_key_test.go @@ -71,6 +71,44 @@ func TestMasterKeyChain_Close(t *testing.T) { assert.False(t, found2) } +func TestMasterKeyChain_CloseZerosKeys(t *testing.T) { + // Verify that Close() zeros all keys before clearing the chain + key1Data := make([]byte, 32) + key2Data := make([]byte, 32) + + // Fill with non-zero data + for i := range key1Data { + key1Data[i] = byte(i) + key2Data[i] = byte(i + 100) + } + + mkc := &MasterKeyChain{activeID: "key1"} + mk1 := &MasterKey{ID: "key1", Key: key1Data} + mk2 := &MasterKey{ID: "key2", Key: key2Data} + + mkc.keys.Store("key1", mk1) + mkc.keys.Store("key2", mk2) + + // Verify keys contain data before Close + assert.NotEqual(t, make([]byte, 32), mk1.Key, "key1 should have data before Close()") + assert.NotEqual(t, make([]byte, 32), mk2.Key, "key2 should have data before Close()") + + // Close should zero the keys + mkc.Close() + + // Verify keys are zeroed + expectedZero := make([]byte, 32) + assert.Equal(t, expectedZero, mk1.Key, "key1 should be zeroed after Close()") + assert.Equal(t, expectedZero, mk2.Key, "key2 should be zeroed after Close()") + + // Verify chain is cleared + assert.Equal(t, "", mkc.activeID) + _, found1 := mkc.Get("key1") + _, found2 := mkc.Get("key2") + assert.False(t, found1) + assert.False(t, found2) +} + func TestLoadMasterKeyChainFromEnv(t *testing.T) { // Generate valid 32-byte keys encoded in base64 key1 := base64.StdEncoding.EncodeToString(make([]byte, 32)) @@ -231,11 +269,8 @@ func TestLoadMasterKeyChainFromEnv(t *testing.T) { } } -func TestLoadMasterKeyChainFromEnv_KeysAreZeroed(t *testing.T) { - // This test verifies that the key material in memory is zeroed after loading - // Note: Due to the implementation calling zero(key) after storing, - // the keys in the keychain are actually zeroed out (which appears to be a bug, - // but we test the actual behavior here) +func TestLoadMasterKeyChainFromEnv_KeysAreUsable(t *testing.T) { + // Verify that loaded master keys contain valid key material and are usable key1Data := []byte("12345678901234567890123456789012") key1 := base64.StdEncoding.EncodeToString(key1Data) @@ -247,6 +282,7 @@ func TestLoadMasterKeyChainFromEnv_KeysAreZeroed(t *testing.T) { mkc, err := LoadMasterKeyChainFromEnv() assert.NoError(t, err) assert.NotNil(t, mkc) + defer mkc.Close() // Get the key from the keychain mk, found := mkc.Get("key1") @@ -254,12 +290,18 @@ func TestLoadMasterKeyChainFromEnv_KeysAreZeroed(t *testing.T) { assert.NotNil(t, mk) assert.Len(t, mk.Key, 32) - // Due to zero(key) being called after storing the slice reference, - // the key data is actually zeroed out - expectedZeroed := make([]byte, 32) - assert.Equal(t, expectedZeroed, mk.Key) + // Keys should contain the actual key material, not zeros + assert.Equal(t, key1Data, mk.Key, "Master key should contain actual key data") - mkc.Close() + // Verify key is not all zeros + allZeros := true + for _, b := range mk.Key { + if b != 0 { + allZeros = false + break + } + } + assert.False(t, allZeros, "Master key should not be zeroed after loading") } func TestLoadMasterKeyChainFromEnv_CloseOnError(t *testing.T) {