# 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\SlpSoftware` al 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. - `wwwroot` restructuring 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 `ApplicationDbContext` migration 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.md` as 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 branch - `push` to `master` — validation plus test deploy - `workflow_dispatch` — with a `deploy_production` boolean input, default `false` It MUST run these gates, all blocking (D-10, D-11, D-12): 1. `dotnet build SlpModularCms.sln -c Release` 2. `dotnet test SlpModularCms.sln -c Release` 3. `dotnet list package --vulnerable --include-transitive` — fails the build on any advisory 4. `pnpm run build` in `frontend/` (includes `tsc -b`) 5. `pnpm test` in `frontend/` 6. `pnpm run lint` and `pnpm run format:check` in `frontend/` 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_`) 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's `index.html` - `/admin` and `/admin/**` non-file paths fall back to `wwwroot/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, including `index.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/v1` from 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 | |---|---|---| | ~~OPEN-01~~ | ~~**Correlation/request ID in logs** is required by SECURITY-03 but does not exist today. Needs a decision on mechanism (ASP.NET Core `TraceIdentifier` versus `W3C traceparent`).~~ **RESOLVED 2026-07-28** at NFR Design for U4: the **W3C trace ID** from the ambient `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 | | ~~OPEN-02~~ | ~~`AvailabilityMiddleware.IsAdminBypass` reads the JWT without validating its signature.~~ **RESOLVED 2026-07-27** at Application Design (Q12 = A): folded into this feature as **FR-24**, landing in the same unit as the `/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: 1. **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 if `web/` lives outside the swapped directory. 2. **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. 3. **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).