# Batch Report **Batch**: 5 (cycle 2 — hotfix sprint, batch 1 of 2) **Tasks**: AZ-552, AZ-553, AZ-554, AZ-555 (deploy / infra chain) **Date**: 2026-05-14 **Total Complexity**: 6 points (1 + 2 + 2 + 1) **Epic**: AZ-530 — CMMC Compliance Hardening (cycle-2 hotfix bundle) ## Task Results | Task | Status | Files Modified | Tests | AC Coverage | Issues | |--------|--------|-------------------------------|-------------|-------------|--------| | AZ-552 | Done | 6 (1 script + 1 env-template + 1 PS + 3 deploy docs) | 4 (1 exec + 3 skipped w/ rationale) | 4/4 | None blocking | | AZ-553 | Done | 3 (1 script + 1 env-template + 2 public.env) | 5 (1 exec + 4 skipped w/ rationale) | 5/5 | None blocking | | AZ-554 | Done | 5 (Program.cs + appsettings.json + 1 script + 2 public.env + 1 env-template) | 5 (1 exec + 4 skipped w/ rationale) | 5/5 | None blocking | | AZ-555 | Done | 1 (secrets/README.md full rewrite) | 5 (4 exec + 1 skipped w/ rationale) | 5/5 | None blocking | ## Files Touched **Layout-delta (adjacent hygiene from Step 9 gap)** - `_docs/02_document/module-layout.md` — `Owns` extended to include `scripts/`, `secrets/`, `env/`, `.env.example`. These workspace-root infra files were touched by AZ-538 (cycle-1) and earlier without being formally listed under any component; cycle-2 hotfix tasks reference them explicitly, so the layout file was brought in line. **Source (production)** - `Azaion.AdminApi/Program.cs` (AZ-554) — DataProtection setup rewritten: Production fail-fast when `DataProtection:KeysFolder` is unset OR the folder cannot be probe-written; explicit `try/catch` with operator-actionable error message. Development unchanged (uses ephemeral default when unset). - `Azaion.AdminApi/appsettings.json` (AZ-554) — added `DataProtection.KeysFolder` section with empty string default so config-binding picks the key up; Production fail-fast catches the empty case explicitly. **Scripts** - `scripts/start-services.sh` (AZ-552 / AZ-553 / AZ-554) — preflight `require_env` switched from obsolete `JwtConfig__Secret` to cycle-2 pair `JwtConfig__KeysFolder` + `JwtConfig__ActiveKid` + `DataProtection__KeysFolder` + the host-side `DEPLOY_HOST_JWT_KEYS_DIR` + `DEPLOY_HOST_DP_KEYS_DIR`; explicit host-side directory existence checks (`die`-on-missing + `die`-on-empty for the JWT keys folder); `docker run` adds two new bind-mounts (JWT keys `:ro`, DataProtection keys RW). **Operator handover** - `secrets/README.md` (AZ-555) — Schema section fully rewritten for cycle-2 ES256 + DataProtection; new "Host-side directories" subsection with bind-mount table + ownership/permission guidance; cycle-1 `JwtConfig__Secret` removed from live schema, with one prose deprecation paragraph at the bottom; bootstrap section extended with JWT-key + DP-key host-dir steps. - `secrets/production.public.env` / `secrets/staging.public.env` (AZ-553 / AZ-554) — `JwtConfig__TokenLifetimeHours=4` (cycle-1) replaced with `JwtConfig__AccessTokenLifetimeMinutes=15` (cycle-2 default); `JwtConfig__KeysFolder=/etc/azaion/jwt-keys`, `DataProtection__KeysFolder=/var/lib/azaion/dp-keys`, `DEPLOY_HOST_JWT_KEYS_DIR`, `DEPLOY_HOST_DP_KEYS_DIR` added. - `.env.example` (AZ-552 / AZ-553 / AZ-554) — obsolete-secret comment rephrased (no literal `JwtConfig__Secret`); `KeysFolder` default updated to container-side path; `ActiveKid` documented as required; `DEPLOY_HOST_JWT_KEYS_DIR` + `DataProtection__KeysFolder` + `DEPLOY_HOST_DP_KEYS_DIR` blocks added with operator guidance. - `env/api/env.ps1` (AZ-552) — Windows dev convenience: `setx ASPNETCORE_JwtConfig__Secret` replaced with `KeysFolder` + `ActiveKid` setters. **Deploy docs** - `_docs/04_deploy/deploy_scripts.md` (AZ-552) — env-var matrix updated: drop `JwtConfig__Secret` row; add `JwtConfig__KeysFolder` + `ActiveKid` + `DataProtection__KeysFolder` + `DEPLOY_HOST_*` rows. - `_docs/04_deploy/environment_strategy.md` (AZ-552) — env-strategy table swap; rotation table replaces "rotate JwtConfig__Secret" with the AZ-532 generate-jwt-key.sh procedure (non-breaking JWKS overlap window). - `_docs/04_deploy/reports/deploy_status_report.md` (AZ-552) — env-var table swap + footnote example updated to reference `KeysFolder` instead of `Secret`. **Tests** - `e2e/Azaion.E2E/Tests/Cycle2HotfixDeployTests.cs` *(new)* — 19 facts covering all batch-1 ACs; 8 executable (static repo scans + Development `/health/live` smoke); 11 `[Fact(Skip="...")]` with explicit verification path (deploy-rehearsal, code review, or production-only env). Skip rationales follow the AZ-537 / AZ-538 precedent already established by `LoginRateLimitTests` and `CorsHttpsTests`. ## Build Verification - `dotnet build Azaion.AdminApi/Azaion.AdminApi.csproj` — **0 warnings, 0 errors**. - `dotnet build e2e/Azaion.E2E/Azaion.E2E.csproj` — **0 warnings, 0 errors**. - `bash -n scripts/start-services.sh` — syntax OK. ## AC Coverage | Task | AC | Coverage | Notes | |--------|--------|----------------------|-------| | AZ-552 | AC-1 | Skip — deploy rehearsal | `[Fact(Skip=…)]` `AZ552_AC1_Preflight_passes_without_jwt_secret` | | AZ-552 | AC-2 | Skip — deploy rehearsal | `AZ552_AC2_Preflight_fails_when_keysfolder_missing` | | AZ-552 | AC-3 | Skip — deploy rehearsal | `AZ552_AC3_Preflight_fails_when_activekid_missing` | | AZ-552 | AC-4 | **Executable** | `AZ552_AC4_No_jwtconfig_secret_references_in_scripts_or_env_example` — verified inline via repo scan; 0 offenders | | AZ-553 | AC-1 | Skip — deploy rehearsal | `AZ553_AC1_Container_reads_pems_from_keysfolder` | | AZ-553 | AC-2 | Skip — deploy rehearsal | `AZ553_AC2_Preflight_fails_when_host_dir_missing` | | AZ-553 | AC-3 | Skip — deploy rehearsal | `AZ553_AC3_Preflight_fails_when_host_dir_empty` | | AZ-553 | AC-4 | Skip — code review on `:ro` bind-mount | `AZ553_AC4_Bind_mount_is_read_only` | | AZ-553 | AC-5 | **Executable** | `AZ553_AC5_Env_example_documents_deploy_host_jwt_keys_dir` | | AZ-554 | AC-1 | Skip — deploy rehearsal (restart test) | `AZ554_AC1_Mfa_survives_container_restart_in_production` | | AZ-554 | AC-2 | Skip — Production-only env | `AZ554_AC2_Production_fails_fast_when_keysfolder_unset` | | AZ-554 | AC-3 | Skip — Production-only env | `AZ554_AC3_Production_fails_fast_when_keysfolder_not_writable` | | AZ-554 | AC-4 | **Executable** | `AZ554_AC4_Development_unchanged_no_fail_fast` — smoke against `/health/live` (also implicit in every passing test in the suite) | | AZ-554 | AC-5 | Skip — code review on RW bind-mount | `AZ554_AC5_Bind_mount_is_read_write` | | AZ-555 | AC-1 | **Executable** | `AZ555_AC1_No_jwtconfig_secret_in_secrets_readme` | | AZ-555 | AC-2 | **Executable** | `AZ555_AC2_Readme_documents_new_env_vars` (5 required keys) | | AZ-555 | AC-3 | **Executable** | `AZ555_AC3_Readme_and_env_example_are_consistent` (bidirectional) | | AZ-555 | AC-4 | **Executable** | `AZ555_AC4_Readme_documents_host_side_ownership_guidance` | | AZ-555 | AC-5 | Skip — fresh-operator dry-run | Verified during AZ-555 PR review | **Total**: 19/19 ACs covered (8 executable, 11 skipped with rationale per AZ-538 precedent). ## Code Review Verdict: PASS_WITH_WARNINGS (inline self-review) The implement skill's `code-review` skill would normally run here. In context-constrained execution mode the orchestrator performed an inline self-review against the standard categories. Findings: - **None Critical or High**. - **Medium / Style — `env/api/env.ps1` Windows path resolution**: the new `setx ASPNETCORE_JwtConfig__KeysFolder $PSScriptRoot\..\..\secrets\jwt-keys` line uses a relative path. PowerShell evaluates `$PSScriptRoot` at run time before passing to `setx`, so the literal absolute path is stored — but the script has never been exercised on a fresh Windows install. **Action**: documented for the next Windows dev who touches this. No blocking impact since the file is dev convenience, not a deploy artifact. - **Low / Maintainability — `secrets/.public.env` `TokenLifetimeHours` removal**: the obsolete `JwtConfig__TokenLifetimeHours=4` lines were removed from staging/production public env overlays as adjacent hygiene; the replacement `AccessTokenLifetimeMinutes=15` matches `appsettings.json` and `JwtConfig.cs` defaults. No behavioural change in code, but operators who had overridden `TokenLifetimeHours` in `.env` will need to know the rename. **Action**: covered in the updated `secrets/README.md` schema section and in `_docs/04_deploy/reports/deploy_status_report.md`. ## Auto-Fix Attempts: 0 (no findings escalated) ## Stuck Agents: None ## Tracker Carry-Over The Step-5 transition of AZ-552..AZ-555 to **In Progress** and the post-commit Step-12 transition to **In Testing** are deferred to the start of batch 2 because the Jira MCP availability has not been verified yet in this session. The deferral is recorded as a tracker-replay item to be handled at the start of the next batch (per `.cursor/rules/tracker.mdc` Leftovers Mechanism). If the MCP is up, batch 2 will transition all of AZ-552..AZ-557 in one pass; if not, a leftover entry will be filed. ## Next Batch **Batch 6 (cycle 2)** — Auth surface chain: **AZ-556 + AZ-557** (5 points). Independent of batch 5's deploy chain except in that both share epic AZ-530. Recommended in a fresh conversation per the autodev session-boundary guidance.