Starting both hosts after U2 fails to decrypt a stored slave API key: the row was encrypted under the old file-based key ring, and the database key ring holds a different, freshly generated key. The key ring working correctly is exactly why the old key is not found. U2 moved the ring without a path for ciphertext that predates it. Confirmed with the user that nothing is deployed yet, so no migration ships and the first deploy meets an empty database. ASM-08 records the condition and makes it blocking for U6 if it ever stops holding. Also notes that the master lets the CryptographicException escape while the slave catches it and reports "master API key rejected" instead — the ambiguity U4's event was designed around, showing up on day one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
34 KiB
Requirements — Gitea Deployment Workflow
Feature: gitea-deployment-workflow
Branch: feature/gitea-deployment-workflow
Date: 2026-07-27
Requirements depth: Comprehensive
1. Intent Analysis
User Request (verbatim, as logged in audit.md)
"Ik wil een Gitea workflow gaan opzetten om de CMS te kunnen deployen. In de instructies moet ook behandeld worden waar de frontend van de website moet gaan komen, maar de workflow voor de website zelf zal hierin niet worden gebouwd, dat wordt per website in hun eigen workspace gedaan. Gebruik de aidlc workflow om alles op te zetten. Voor Uptime gebruik ik UptimeRobot, voor analytics gebruik ik Umami en voor logging console logs samen met Sentry. Ik heb in de workspace
K:\Development\SlpSoftware\Projects\SlpSoftwareal een werkende workflow. Dit is voor een react vite front-end, maar is een goed startpunt voor deze workspace. Op dit moment zijn er alleen een lokale omgeving, test en productie. Het doel van deze workflow en eventuele deployment-opzet is dat we boel moeten kunnen uploaden als een .NET-applicatie en dat we geen server-configuratie hoeven te doen omdat dat voor andere websites waarschijnlijk niet kan. Vandaar dat ik een opzet wilde met de api en de 2 frontends als 1 website."
Analysis
| Dimension | Assessment |
|---|---|
| Request Clarity | Clear on goals and tooling; initially incomplete on hosting/transport, migration delivery and wwwroot ownership. All resolved across two clarification rounds. |
| Request Type | New Feature (CI/CD + operational tooling), with supporting Enhancement work inside the application. |
| Scope Estimate | Multiple Components — new .gitea/workflows/, changes to SlpModularCms.Api (Program.cs, .csproj), SlpModularCms.Core (security headers, Data Protection, health checks), SlpModularCms.Modules.Availability (bypass list), frontend/ (config, Sentry, Umami, lint fixes), plus documentation and operations artifacts. |
| Complexity Estimate | Moderate-to-Complex. Not algorithmically hard; the difficulty is the constraint set — one process serving three surfaces, a customer-owned wwwroot that must survive deploys, no server configuration permitted, two different hosting targets, asymmetric migration behaviour, and build-time environment coupling in the SPA. |
| Primary Business Driver | Make deployment of the CMS repeatable and safe on hosting where nothing can be configured server-side, so the product can be shipped per customer without bespoke server work. |
Core Constraint (drives most design decisions)
No server configuration may be required. This is why the API serves the public website, the admin SPA and the API from one process, and it is why anything normally solved in nginx or IIS configuration — security headers, routing, SPA fallbacks — must live inside the application instead.
2. Scope
In Scope
- Gitea Actions CI workflow with build, test and lint gates for both backend and frontend.
- Gitea Actions deploy workflow: automated to test, explicit opt-in to production.
wwwrootrestructuring so the customer's public website can never be destroyed by a CMS deploy.- A documented contract for what a per-website workspace must deliver.
- A dedicated health-check endpoint.
- Sentry (backend + frontend), Umami analytics, UptimeRobot monitoring.
- HTTP security-headers middleware.
- Persistent Data Protection key ring.
- Automatic
ApplicationDbContextmigration at startup. - Same-origin default for the admin SPA's API base URL.
- Fixing the 5 existing frontend lint errors and pinning 2 vulnerable transitive packages.
- Rollback strategy including pre-deploy database backup.
Explicitly Out of Scope
- The public website's own build and deploy workflow. Handled per website in its own workspace (user's explicit instruction). This feature delivers only the contract and the instructions.
- Any fourth environment. Only local, test and production exist.
- Backend integration/contract/e2e tests. Their absence is recorded in
code-quality-assessment.mdas existing technical debt; adding them is not part of this feature. - Code coverage thresholds (Q9: F not selected).
- Property-based testing (extension opted out).
- 90-day log retention — accepted as a documented deviation (see § 6, DEV-01).
3. Decisions Register
Every decision below traces to a question answer. Q* = first round, CQ* = clarification round.
Hosting and transport
| ID | Decision | Source |
|---|---|---|
| D-01 | Test runs on the user's own Raspberry Pi infrastructure with a .NET runtime. | Q2 = B |
| D-02 | Production runs on the Pi for now, with shared hosting as the eventual target. The transport layer must be structured so FTPS can be added alongside SSH/SCP later without restructuring the workflow. | CQ1 = C |
| D-03 | Transport is SSH/SCP (or SFTP — same SSH transport). FTPS is not needed yet. Confirmed viable because FileZilla connects to the Pi over SFTP, which proves SSH is available. | Q1 = X + CQ1 = C |
| D-04 | Workflow runs on the existing self-hosted Podman runner, with the toolchain installed explicitly in the workflow via actions/setup-dotnet and pnpm/action-setup rather than assumed present. |
Q3 = B |
| D-05 | Container-based actions must be avoided in favour of plain shell steps, because they fail on this runner with a 409 attach error. | Reference project's documented experience |
wwwroot layout and the public website
| ID | Decision | Source |
|---|---|---|
| D-06 | The public website moves into wwwroot/web/ (user's chosen name, not site). Final layout: wwwroot/admin/ (CMS admin SPA, shipped by this repo) and wwwroot/web/ (customer website, deployed separately). Requires a change to Program.cs. |
Q4 = C |
| D-07 | An additional wwwroot folder for the API is kept as an option but is not required — see ASM-02. |
Q4 = C ("eventueel … indien nodig") |
| D-08 | The website-workspace contract documents the target path plus technical constraints: routing/SPA-fallback behaviour, forbidden paths and filenames, and how to call /api/v1 from the website. A ready-made YAML deploy snippet is not required. |
Q5 = B |
Workflow shape and gates
| ID | Decision | Source |
|---|---|---|
| D-09 | Trigger strategy is identical to the reference: pull requests validate; push/merge to master deploys to test; production only via manual workflow_dispatch with an explicit deploy_production checkbox. |
Q6 = A |
| D-10 | Pipeline gates: backend build, backend tests, frontend type-check + build, frontend tests, frontend lint/format-check. No coverage threshold. | Q9 = A,B,C,D,E |
| D-11 | The 5 existing frontend lint errors are fixed within this feature, after which the lint gate is blocking. | Q7 = A |
| D-12 | A blocking vulnerability gate is added, and Microsoft.OpenApi and System.Security.Cryptography.Xml are pinned to patched versions within this feature. |
Q8 = A |
Database and configuration
| ID | Decision | Source |
|---|---|---|
| D-13 | ApplicationDbContext migrates automatically at startup, matching the two module contexts. Deployment becomes self-contained; no CLI access to the host is needed. |
Q10 = C |
| D-14 | The admin SPA defaults to same-origin for the API base URL, with an optional explicit override that must keep working for local development. | Q11 = X |
| D-15 | Two builds are produced (test and production) with their own Vite variables, exactly like the reference — because Sentry's environment tag and Umami's per-environment website ID are build-time values. Runtime-delivered configuration is recorded as a possible later improvement, not built now. |
CQ2 = C |
| D-16 | Test and production configuration comes from environment variables on the host, set manually per environment. The workflow does not write or manage them. | Q12 = A |
| D-17 | Data Protection uses a persistent key ring via PersistKeysToDbContext, reusing the existing database, so a redeploy can never make stored slave API keys unreadable. |
Q13 = A |
Monitoring and observability
| ID | Decision | Source |
|---|---|---|
| D-18 | Sentry on both sides: Sentry.AspNetCore in the API and @sentry/react in the admin SPA. |
Q14 = A |
| D-19 | One Sentry project for the CMS, with environment tags distinguishing test from production. |
Q15 = A |
| D-20 | Logging is console plus structured logging to Sentry, at levels more informative than exceptions alone. | Q16 = C |
| D-21 | Health check is liveness only — no database call. Fastest and most stable signal, and it must not go red for reasons unrelated to the process being alive. | Q17 = A |
| D-22 | /health must be added to AvailabilityMiddleware._bypassPrefixes, or the availability gate returns 503 for it on a disabled instance. |
Established during Reverse Engineering |
| D-23 | UptimeRobot monitors /health, the public website (/) and /admin per environment. |
Q18 = C |
| D-24 | Reuse the existing self-hosted Umami (analytics.slpsoftware.nl); add new website entries for the CMS environments. |
Q19 = A |
| D-25 | Umami measures both the public website and the admin SPA. | Q20 = B |
Reliability
| ID | Decision | Source |
|---|---|---|
| D-26 | Rollback = rebuild and redeploy an earlier commit, plus retaining the previous publish output on the host for a fast restore without building, plus a database backup before every production deploy. Migrations must be kept forward-compatible and never destructive. | Q21 = C |
| D-27 | Downtime control uses an atomic release-directory switch: deploy into a new directory, then switch. Shortest downtime, and the public website stays continuously reachable. app_offline.htm is not used, since it is an IIS-only mechanism that has no effect on the Pi. |
Q22 = A → corrected to CQ3 = C |
Security
| ID | Decision | Source |
|---|---|---|
| D-28 | The Security Baseline extension is enabled and blocking. | Extension opt-in = A |
| D-29 | Property-Based Testing is disabled. | Extension opt-in = C |
| D-30 | SECURITY-04 and SECURITY-14 are both addressed within this feature: security-headers middleware (with CSP exceptions for Umami and Sentry) and alerting through Sentry alert rules, with log retention documented as a deliberate deviation. | CQ4 = A |
| D-31 | CSP is strict for /admin and /api/v1, and more permissive for the public website, so a website builder is not constrained by a policy they never see. |
CQ5 = B |
| D-32 | The Operations phase runs after Construction. | Operations opt-in = A |
4. Functional Requirements
FR-01 — Continuous integration workflow
A .gitea/workflows/continuous_integration.yaml MUST exist that triggers on:
pull_request(opened, synchronize, reopened) — validation only, any branchpushtomaster— validation plus test deployworkflow_dispatch— with adeploy_productionboolean input, defaultfalse
It MUST run these gates, all blocking (D-10, D-11, D-12):
dotnet build SlpModularCms.sln -c Releasedotnet test SlpModularCms.sln -c Releasedotnet list package --vulnerable --include-transitive— fails the build on any advisorypnpm run buildinfrontend/(includestsc -b)pnpm testinfrontend/pnpm run lintandpnpm run format:checkinfrontend/
Tool versions MUST be pinned (actions/setup-dotnet, pnpm/action-setup) and no action may rely on a latest tag (D-04, SECURITY-10).
FR-02 — Reusable deploy workflow
A .gitea/workflows/deploy.yaml MUST exist as a workflow_call workflow accepting at minimum artifact_name, environment and deploy_path. It MUST use plain shell steps rather than container actions (D-05).
Its transport step MUST be structured so a second transport (FTPS) can be added later as an alternative without restructuring the workflow or its interface (D-02) — for example by taking the transport as an input with SSH/SCP as the only implemented value for now.
Environment-specific paths MUST come from Gitea Actions variables and credentials from secrets, never hardcoded, following the reference project's split.
FR-03 — Test deployment
On push/merge to master, or on any workflow_dispatch run, the pipeline MUST deploy to the test environment on the Pi automatically (D-01, D-09).
FR-04 — Production deployment
Production MUST deploy only on a workflow_dispatch run with deploy_production = true. Pushing to master MUST NOT be able to deploy production under any circumstance (D-09).
FR-05 — Separate test and production builds
The pipeline MUST produce two distinct build artifacts, each built with its own environment-specific Vite variables (VITE_APP_ENV, VITE_SENTRY_DSN, VITE_UMAMI_SCRIPT_URL, VITE_UMAMI_WEBSITE_ID_<ENV>) so Sentry events and analytics are attributed to the correct environment (D-15).
FR-06 — Atomic release switch
Deployment MUST place the new publish output in a fresh directory and then switch to it atomically, rather than overwriting a live directory (D-27). The application process MUST be restarted as part of the switch, since a running .NET process holds its assemblies.
The previous release directory MUST be retained on the host to allow a fast restore without rebuilding (D-26).
FR-07 — wwwroot restructuring
Program.cs MUST serve the public website from wwwroot/web/ at / instead of directly from wwwroot/ (D-06). The existing behaviour MUST be preserved:
/and non-file paths fall back to the public website'sindex.html/adminand/admin/**non-file paths fall back towwwroot/admin/index.html- Missing paths that look like files (with an extension) still return
404
frontend/vite.config.ts keeps base: '/admin/' for builds; no change is needed there.
FR-08 — The public website must survive every CMS deploy
No deployment step may delete, overwrite or mirror-sync the contents of wwwroot/web/ (D-06). Because releases are switched atomically (FR-06), wwwroot/web/ MUST live outside the swapped release directory and be linked or mounted into it — otherwise an atomic switch would silently discard the customer's website along with the old release. See ASM-01.
FR-09 — Website workspace contract
Documentation MUST specify, for a per-website workspace (D-08):
- The exact target path (
wwwroot/web/) and required structure, includingindex.html - That
wwwroot/admin/and the application root are forbidden targets - How SPA-fallback routing behaves for the website, and which paths are reserved (
/admin,/api/v1,/health) - How to call
/api/v1from the website — same-origin, so relative URLs work and no CORS configuration is needed - Which CSP applies to the public website and what that permits (D-31)
- How to include the Umami tracking script (D-25)
FR-10 — Health-check endpoint
The application MUST expose GET /health returning 200/Healthy when the process is alive and 503/Unhealthy otherwise, using AddHealthChecks() and MapHealthChecks("/health") (D-21). It MUST NOT perform a database call.
/health MUST be added to AvailabilityMiddleware._bypassPrefixes so the availability gate cannot mask it (D-22).
/health MUST NOT be presented as, or conflated with, /api/v1/Availability/status or /api/v1/System/capabilities, which are CMS domain functionality.
FR-11 — Automatic Core migrations
ApplicationDbContext MUST be migrated automatically at application startup, consistent with AvailabilityDbContext and MasterDbContext (D-13), so a fresh deployment needs no CLI access to the host.
Migrations MUST be forward-compatible and non-destructive, so redeploying an earlier commit remains a valid rollback (D-26).
FR-12 — Persistent Data Protection key ring
Data Protection MUST persist keys via PersistKeysToDbContext against the existing database (D-17), so redeploys and the atomic release switch cannot render stored slave API keys unreadable.
FR-13 — Same-origin API base URL for the admin SPA
frontend/src/lib/config.ts MUST treat an absent or empty VITE_API_BASE_URL as same-origin, while continuing to accept an explicit absolute URL for local development against https://localhost:7221 or :7222 (D-14). Its Zod validation MUST be relaxed accordingly, without silently accepting malformed values.
FR-14 — Sentry on the backend
Sentry.AspNetCore MUST be integrated into the API, reporting unhandled exceptions and structured logs at levels beyond exceptions alone (D-18, D-20), tagged with the environment (D-19). Sentry MUST be optional: an absent DSN disables it and leaves console logging active, mirroring the reference project's behaviour.
FR-15 — Sentry in the admin SPA
@sentry/react MUST be integrated into the admin SPA with environment and release tags, and MUST be skipped gracefully when no DSN is configured (D-18, D-19).
FR-16 — Umami analytics
The Umami tracking script MUST be included in the admin SPA, using per-environment website IDs from build-time variables, and MUST be absent during local development (D-24, D-25). Website entries are added to the existing self-hosted Umami instance. Inclusion for the public website is covered by FR-09.
FR-17 — UptimeRobot monitors
Documentation MUST specify UptimeRobot monitors for /health, / and /admin, per environment (D-23), including what each one does and does not prove.
FR-18 — HTTP security headers
Middleware MUST set, on all HTML-serving responses (D-30, SECURITY-04):
| Header | Value |
|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Referrer-Policy |
strict-origin-when-cross-origin |
Content-Security-Policy |
Path-scoped per D-31 |
CSP MUST be strict for /admin and /api/v1 (default-src 'self' plus explicit allowances for the Umami script origin and Sentry's ingest endpoint) and more permissive for the public website (D-31). Any use of unsafe-inline or unsafe-eval MUST be documented with justification.
FR-19 — Security alerting
Sentry alert rules MUST be configured for repeated authentication failures and authorization violations (D-30, SECURITY-14). The corresponding events MUST be emitted by the application with enough context to alert on, and MUST NOT contain passwords, tokens or PII.
FR-20 — Database backup before production deploy
A database backup MUST be taken before every production deployment (D-26). If it cannot be automated from the runner, the deployment instructions MUST make it an explicit, verifiable manual step in the production procedure.
FR-21 — Fix existing lint errors
The 5 errors and 1 warning currently reported by pnpm run lint MUST be fixed (D-11): AddCmsInstanceDialog.tsx:55, InviteUserDialog.tsx:50 and :54, SettingsPage.tsx:40, SetStatusDialog.tsx:32 and :72.
FR-22 — Pin vulnerable packages
Microsoft.OpenApi (currently 2.0.0) and System.Security.Cryptography.Xml (currently 10.0.9) MUST be pinned to patched versions so the blocking vulnerability gate passes (D-12).
FR-24 — Validate the token in the availability gate's admin bypass
(Added 2026-07-27 at Application Design, Q12 = A — resolves OPEN-02.)
AvailabilityMiddleware.IsAdminBypass currently calls JwtSecurityTokenHandler.ReadJwtToken, which parses a token without validating its signature. An unauthenticated caller can therefore forge an unsigned token carrying an Owner role claim and bypass the availability gate.
The bypass MUST instead rely on a properly authenticated principal — either by validating the token with the same parameters used by the JWT bearer scheme, or by evaluating HttpContext.User after authentication has run. Protected endpoints already authenticate correctly, so this is not a privilege escalation; the defect is that the gate itself is bypassable. (SECURITY-08.)
Existing behaviour that MUST be preserved: an Owner or Administrator with a valid token still passes the gate, so administrators can always reach the system to switch it back on.
FR-23 — Deployment and rollback documentation
Operations artifacts MUST document: one-time host setup, required Gitea variables and secrets, the deployment procedure per environment, the rollback procedure, and how to switch production to FTPS when it moves to shared hosting (D-02).
5. Non-Functional Requirements
NFR-01 — No server configuration required
Nothing in the deployment may depend on configuring nginx, IIS, or any server-side software beyond placing files and running the application. Anything normally handled by server configuration — security headers, routing, SPA fallbacks — must be handled inside the application. (Core constraint.)
NFR-02 — Deployment safety
No deployment step may destroy data it does not own. Specifically: the customer's public website (FR-08), the database (FR-11, FR-20), and Data Protection keys (FR-12). This is the single highest-priority non-functional property of this feature.
NFR-03 — Production cannot be deployed accidentally
Production deployment requires a deliberate, explicit action. No push, merge or ordinary workflow run may reach production (FR-04).
NFR-04 — Minimal and predictable downtime
Deployment downtime is limited to the process restart in the atomic switch (FR-06). The public website remains reachable throughout the CMS deploy.
NFR-05 — Reproducible builds
Tool versions and actions are pinned; pnpm install --frozen-lockfile is used. (SECURITY-10; note the absence of packages.lock.json — see DEV-02.)
NFR-06 — Observability sufficient to trust a deployment
After a deploy it must be possible to determine, without host access, whether the application is alive (FR-10), whether it is throwing errors (FR-14, FR-15), and whether it is being used (FR-16). This matters more than usual here because ModuleOrchestrator logs rather than throws when a module fails to load, so a deployment can silently start with reduced capability.
NFR-07 — Secrets never in source or logs
No credential, connection string or DSN in the repository or in workflow output. Credentials live in Gitea Actions secrets; runtime secrets live in host environment variables (D-16). (SECURITY-12.)
NFR-08 — Separation of health from product state
Infrastructure health (/health) and CMS domain state (availability, capabilities) remain strictly separate concerns, in both implementation and documentation (FR-10).
NFR-09 — Extensibility to a second hosting target
Adding FTPS for shared hosting must not require restructuring the workflow (FR-02, D-02).
NFR-10 — Maintainability of the workflow
Environment-specific values are declared once and referenced, not duplicated across jobs — following the reference project's env: block plus config job pattern, which exists because the env context is unavailable in a reusable workflow's with: block.
6. Security Compliance (Security Baseline extension — enabled, blocking)
Assessed at Requirements Analysis. Rules are evaluated against what this feature's requirements commit to.
| Rule | Status | Notes |
|---|---|---|
| SECURITY-01 Encryption at rest/transit | Addressed | Connection strings must enforce TLS (Encrypt=True); HSTS via FR-18. Database-level encryption at rest depends on the host and is a documented host-setup step in FR-23. |
| SECURITY-02 Access logging on intermediaries | N/A | No load balancer, API gateway or CDN is part of this architecture — the application is the only network-facing component, covered by SECURITY-03. |
| SECURITY-03 Application-level logging | Addressed | FR-14, FR-20 (D-20): structured logging to Sentry with environment tagging, no secrets or PII. Correlation/request ID must be included — see OPEN-01. |
| SECURITY-04 HTTP security headers | Addressed | FR-18, with the path-scoped CSP of D-31. |
| SECURITY-05 Input validation | Pre-existing, unchanged | The API validates via model binding and typed requests, uses EF Core parameterised queries, and returns RFC 9457. This feature adds no new input surface except /health, which takes no input. |
| SECURITY-06 Least-privilege access policies | Addressed | Deploy credentials must be scoped to the deployment target only, and Gitea secrets scoped to this repository (FR-02, FR-23). |
| SECURITY-07 Restrictive network configuration | Partially N/A | No cloud networking to configure. What applies — not exposing the database publicly, and restricting SSH access — is a documented host-setup requirement in FR-23. |
| SECURITY-08 Application-level access control | Improved | Hierarchical policies, JWT validation, per-origin CORS are pre-existing. This feature adds /health as a deliberately anonymous endpoint exposing no data beyond liveness, and fixes the pre-existing unvalidated-JWT gap in AvailabilityMiddleware.IsAdminBypass (code-quality-assessment.md item 16) via FR-24. |
| SECURITY-09 Hardening and misconfiguration | Addressed | Scalar/OpenAPI already Development-only; production errors already go through GlobalExceptionHandler as ProblemDetails; no default credentials (D-16); static-file serving must not enable directory browsing. |
| SECURITY-10 Supply chain | Addressed | Blocking vulnerability gate (FR-01, FR-22), pinned tool versions and actions (FR-01), --frozen-lockfile. SBOM generation and the missing packages.lock.json — see DEV-02. |
| SECURITY-11 Secure design | Addressed | Rate limiting already exists on login/refresh; security-critical logic is already isolated in Core/Identity; the misuse case explicitly considered by this feature is a deployment destroying the customer's website (NFR-02) and a redeploy silently breaking master↔slave trust (FR-12). |
| SECURITY-12 Authentication and credentials | Pre-existing, unchanged, plus FR-12 | Identity password policy meets the 8-character minimum with complexity; httpOnly/Secure/SameSite refresh cookie; rate-limited login. Breached-password-list checking and MFA are not implemented — see DEV-03. |
| SECURITY-13 Software and data integrity | Addressed | External scripts (Umami) must be loaded with SRI where the provider supports it, and are constrained by CSP (FR-18). Pipeline definitions are version-controlled and reviewable. Data-modification auditing beyond what exists — see DEV-04. |
| SECURITY-14 Alerting and monitoring | Addressed with documented deviation | Alerting via FR-19; monitoring via FR-17. Retention deviates — see DEV-01. |
| SECURITY-15 Exception handling and fail-safe defaults | Pre-existing, unchanged | GlobalExceptionHandler is registered globally; UseExceptionHandler is first in the pipeline; the availability gate fails closed while the master gate fails open by deliberate design (an unreachable Master must never permanently disable a customer site) — documented as an intentional, business-driven exception to "fail closed". |
Documented Deviations
| ID | Deviation | Rationale | Decided |
|---|---|---|---|
| DEV-01 | Log retention below the 90-day SECURITY-14 minimum. Sentry's free plan retains events for ~30 days. | Accepted knowingly; extending retention is a cost decision, not a technical one. Revisit if a compliance requirement appears. | CQ4 = A |
| DEV-02 | No packages.lock.json for .NET projects, and no SBOM generation. |
The frontend is locked via pnpm-lock.yaml; the .NET side relies on the blocking vulnerability gate instead. Recorded as follow-up work rather than silently ignored. |
Derived from SECURITY-10 assessment |
| DEV-03 | No breached-password-list checking and no MFA. | Pre-existing product scope, unrelated to deployment. Belongs to an Identity feature, not this one. | Derived from SECURITY-12 assessment |
| DEV-04 | No before/after audit trail on critical data changes. | Pre-existing; CmsInstance does track LastStatusPushedAt/LastContactedAt, but there is no general audit log. Out of scope for a deployment feature. |
Derived from SECURITY-13 assessment |
| DEV-05 | Data Protection keys are stored unencrypted at rest in the database, whereas SECURITY-01 requires encryption at rest for persisted data. (Added 2026-07-27 at U2 Functional Design.) | DPAPI is unavailable on Linux, and X.509 certificate encryption relocates the loss problem to the certificate — reintroducing the very failure mode FR-12 exists to eliminate. Compensating controls per BR-U2-08: TLS enforced on the database connection, and the database not publicly reachable. Certificate-based encryption is recorded as a separate follow-up. | U2 FD Q2 = C |
DEV-01…04 are pre-existing or cost-driven and none is introduced by this feature. DEV-05 is the one deviation this feature does introduce — it is a consequence of moving the key ring into the database, which on balance removes a far larger risk (silent, permanent loss of Master↔slave trust on every redeploy) than it adds.
7. Assumptions
| ID | Assumption | Why it matters | If wrong |
|---|---|---|---|
| ASM-01 | With the atomic release switch (FR-06), wwwroot/web/ must live outside the swapped release directory and be linked into it (e.g. a symlink to a persistent path on the host). |
Otherwise switching releases silently discards the customer's website — precisely the failure mode D-06 was chosen to prevent. This follows necessarily from combining Q4 = C with CQ3 = C, so it is stated rather than asked. | Raise it and the deploy design changes materially; flag before Construction if this is not acceptable. |
| ASM-02 | No wwwroot folder is needed for the API. The API is not static content — its assemblies live in the application root and it serves /api/v1 through routing. The option is kept open but nothing is built for it. |
Avoids building an unused folder. | If something static under an API path is intended, say so and FR-07 gains a third mount. |
| ASM-03 | The Pi already runs, or can run, a .NET 10 runtime, and the app is managed by a process manager (systemd) that the deploy can restart over SSH. | The atomic switch requires restarting the process. | Restart mechanism changes; deploy step is rewritten. |
| ASM-04 | The Pi's SQL Server database is reachable from the application, and a backup can be taken before a production deploy. | FR-20 depends on it. | FR-20 becomes a documented manual precondition only. |
| ASM-05 | The existing Umami instance at analytics.slpsoftware.nl remains available and its script origin can be added to the CSP. |
FR-16, FR-18. | Umami setup gains host work, as in the reference project. |
| ASM-06 | One Sentry project with environment tags is acceptable for both backend and frontend events of this CMS. | D-19. | Split into more projects; only configuration changes. |
| ASM-07 | Existing Gitea secrets for the Pi (PI_MAIN_* in the reference project) can be reused or replicated for this repository. |
FR-02. | New secrets are created; documented in FR-23. |
| ASM-08 | No environment holds API keys encrypted under the old file-based key ring. Confirmed by the user on 2026-07-28: the CMS is not deployed anywhere yet, so the first deploy of U2 meets an empty database. U2 therefore ships no one-time re-encryption path for pre-existing ciphertext. | U2 moved the Data Protection key ring from the filesystem into the database. Ciphertext written under the old ring cannot be decrypted by the new one — it fails with The key {id} was not found in the key ring. Observed locally on 2026-07-28 against rows written before U2, and resolved by deleting and re-registering. |
U6 must not deploy until either the affected rows are cleared and the instances re-registered, or U2 gains a migration that reads the old ring as a fallback and re-encrypts. This is a silent, destructive failure of master/slave communication — exactly the class NFR-02 exists to prevent. |
8. Open Items
| ID | Item | To be resolved |
|---|---|---|
TraceIdentifier versus W3C traceparent).Activity, with TraceIdentifier as the fallback when no Activity exists. Enabled through ActivityTrackingOptions plus IncludeScopes, so every entry from every category carries it without changing any call site. Chosen because it propagates across the master/slave HTTP boundary via traceparent — the one diagnostic question TraceIdentifier cannot answer — and because it is the same value ASP.NET Core's ProblemDetails already returns to the client. See construction/u4-observability/nfr-design/nfr-design-patterns.md Pattern 1. |
Closed | |
AvailabilityMiddleware.IsAdminBypass reads the JWT without validating its signature./health bypass since both touch the same middleware. |
Closed | |
| OPEN-03 | Exact patched versions for Microsoft.OpenApi and System.Security.Cryptography.Xml (FR-22) must be resolved and verified against the advisories. |
Construction |
| OPEN-04 | Whether production stays on the Pi long enough that FTPS is never built. D-02 requires only that the design allows it; the trigger for actually building it is a business decision. | Deferred by design |
9. Summary
This feature turns a manually deployed modular-monolith CMS into one with an automated, auditable pipeline, on hosting where nothing can be configured server-side.
24 functional requirements, 10 non-functional requirements, 32 traced decisions, 8 assumptions, 2 remaining open items, 4 documented security deviations.
(FR-24 added and OPEN-02 closed at Application Design on 2026-07-27. OPEN-01 closed at NFR Design on 2026-07-28. ASM-08 added on 2026-07-28 after the key-ring migration gap surfaced in local testing.)
The three requirements that carry the most risk if implemented carelessly:
- FR-08 with ASM-01 — the customer's public website must survive every deploy. Combining the
wwwroot/web/split with an atomic release switch protects it, but only ifweb/lives outside the swapped directory. - FR-12 — a persistent Data Protection key ring, without which a redeploy silently breaks master↔slave trust in a way that looks like a network problem.
- FR-04 — production must be unreachable by accident.
Three findings from Reverse Engineering are resolved as a by-product rather than left as debt: the migration asymmetry (FR-11), the environment-coupled SPA bundle (FR-13), and the ephemeral key ring (FR-12).