diff --git a/aidlc-docs/features/gitea-deployment-workflow/aidlc-state.md b/aidlc-docs/features/gitea-deployment-workflow/aidlc-state.md index 4dd71a7..f45a7b2 100644 --- a/aidlc-docs/features/gitea-deployment-workflow/aidlc-state.md +++ b/aidlc-docs/features/gitea-deployment-workflow/aidlc-state.md @@ -123,6 +123,27 @@ and the renamed Identity tables); both `SlpModularCms.Api` and `SlpModularCms.Ap cleanly against it (`/health` → 200, `MigrateCoreDatabase()` logs "already up to date" on the second run); full backend suite re-confirmed at 372/372 passed, 0 build errors. +### Reverse Proxy Topology Correction (2026-07-29) + +Infrastructure Design's Q10 (`infrastructure-design.md` § 1) established that an nginx reverse +proxy exists — right about *that*, wrong about *where*: it runs on a **separate, dedicated Pi** +("the proxy Pi"), not on pi-main (where this deployment's release directories and systemd units +live). The proxy Pi terminates TLS and forwards plain HTTP to pi-main over the LAN. + +**Corrected**: +- `ASPNETCORE_URLS` binds `0.0.0.0`, not `localhost` (`deployment-instructions.md` § 1.5) — the + proxy Pi must reach Kestrel over the network, not loopback +- pi-main runs **no nginx and holds no certificates** for these domains at all — the entire + nginx/certbot procedure in `deployment-instructions.md` § 1.7 happens on the proxy Pi instead +- Added a **firewall requirement on pi-main** (§ 1.7.1 step 4): since Kestrel now listens on all + interfaces, the backend ports must be restricted to just the proxy Pi's address, or the TLS + termination the whole design depends on is trivially bypassable by anything else on the LAN + hitting pi-main directly + +Also corrected in `infrastructure-design.md` § 1 (erratum note, history preserved rather than +rewritten). No code changes — this is host topology and Operations documentation only, the +application itself has no opinion on where TLS terminates. + ## Scope Decisions (from feature-selection.md) - **Public website**: documentation/instructions only — where the website build lands in `wwwroot/`, how it coexists with `wwwroot/admin/`, and what a per-website workspace must deliver. The website's own build/deploy workflow stays out of scope (Q4 = A). - **Environments**: local, test, production only. diff --git a/aidlc-docs/features/gitea-deployment-workflow/audit.md b/aidlc-docs/features/gitea-deployment-workflow/audit.md index 6942473..71a3975 100644 --- a/aidlc-docs/features/gitea-deployment-workflow/audit.md +++ b/aidlc-docs/features/gitea-deployment-workflow/audit.md @@ -1388,3 +1388,14 @@ by Infrastructure Design. U5 carries REF-U3-01's Umami origin gate plus FR-21 an **Context**: OPERATIONS — Deployment Setup, nginx documentation generalized for reuse --- +## Deployment Setup — Reverse Proxy Runs on a Separate Pi + +**Timestamp**: 2026-07-29T00:40:00Z + +**User Input**: "OKee wacht. Ik heb een andere pi waar de reverse-proxy op staat voor mijn set-up. Die reverse proxy regelt de ssl al en stuurt daarna door. dus de pi-main zou geen ssl verbinding mee rhebben en zal al op poort 80 luisteren" + +**AI Response**: "Significant topology correction: the nginx reverse proxy runs on a separate, dedicated Pi, not on pi-main. Updated ASPNETCORE_URLS to bind 0.0.0.0 instead of localhost (deployment-instructions.md § 1.5), rewrote § 1.7 entirely: pi-main runs no nginx/certificates at all, the from-scratch nginx+certbot procedure now runs on the proxy Pi with proxy_pass targeting pi-main's LAN address, and added a firewall step (§ 1.7.1 step 4) restricting pi-main's backend ports to only the proxy Pi's address, since binding 0.0.0.0 without that would let anything on the LAN bypass the proxy's TLS entirely. Added a corresponding erratum to infrastructure-design.md § 1 (Q10 was right that nginx exists, wrong about where) and a full record in aidlc-state.md. No application code changes — pure host-topology/Operations documentation." + +**Context**: OPERATIONS — Deployment Setup, reverse-proxy topology corrected across Operations and Construction docs + +--- diff --git a/aidlc-docs/features/gitea-deployment-workflow/construction/u6-deploy-workflow/infrastructure-design/infrastructure-design.md b/aidlc-docs/features/gitea-deployment-workflow/construction/u6-deploy-workflow/infrastructure-design/infrastructure-design.md index edca9c6..43bad2a 100644 --- a/aidlc-docs/features/gitea-deployment-workflow/construction/u6-deploy-workflow/infrastructure-design/infrastructure-design.md +++ b/aidlc-docs/features/gitea-deployment-workflow/construction/u6-deploy-workflow/infrastructure-design/infrastructure-design.md @@ -17,6 +17,15 @@ That port assignment is a one-time host-setup fact (documented in Operations, FR workflow never reads, writes or reasons about a port. Its only environment-specific inputs are the deploy path and which systemd unit to restart. +> **Corrected at Deployment Setup, 2026-07-29**: Q10's answer was right that an nginx reverse +> proxy exists, but wrong about *where* — it runs on a **separate, dedicated Pi**, not on this one. +> Kestrel therefore binds `0.0.0.0`, not `localhost` (`deployment-instructions.md` § 1.5), and this +> Pi's firewall must restrict those ports to just the proxy Pi's address +> (`deployment-instructions.md` § 1.7.1 step 4) — otherwise the proxy's TLS termination is +> bypassable by anything else on the LAN. The "fixed, pre-configured local port per environment" +> statement above still holds; only "local" turned out to mean "local to this Pi's network +> interface," not "loopback." + **Publish target**: `linux-arm64`, **framework-dependent** (Q2 = A, Q5 = B) — assumes the .NET 10 runtime is already installed on the Pi (ASM-03). Smaller artifact, faster upload over the same SSH transport every other unit already relies on. diff --git a/aidlc-docs/features/gitea-deployment-workflow/operations/deployment/deployment-instructions.md b/aidlc-docs/features/gitea-deployment-workflow/operations/deployment/deployment-instructions.md index bf16bb4..7a8d96a 100644 --- a/aidlc-docs/features/gitea-deployment-workflow/operations/deployment/deployment-instructions.md +++ b/aidlc-docs/features/gitea-deployment-workflow/operations/deployment/deployment-instructions.md @@ -136,7 +136,11 @@ Contents (fill in real values — this file is never read by the workflow, only below): ```ini ASPNETCORE_ENVIRONMENT=Production -ASPNETCORE_URLS=http://localhost: +# 0.0.0.0, not localhost: the reverse proxy handling TLS for this domain runs on a SEPARATE Pi +# (§ 1.7), so it must reach Kestrel over the LAN, not loopback. See § 1.7's firewall note — binding +# to all interfaces means the port must be restricted to the proxy Pi's address, not left open to +# the whole LAN. +ASPNETCORE_URLS=http://0.0.0.0: ConnectionStrings__DefaultConnection=Server=127.0.0.1;Port=3306;Database=SlpSoftware;Uid=;Pwd= JwtSettings__Secret= JwtSettings__Issuer=SlpModularCms @@ -211,18 +215,28 @@ systemctl --user enable slpsoftware-test.service systemctl --user enable slpsoftware-production.service ``` -### 1.7 nginx Routing +### 1.7 TLS-Terminating Reverse Proxy — On a Separate Pi, Not This One -The existing reverse proxy (`infrastructure-design.md` § 1, § 4) needs a server block per domain, -routing to the matching local port. Two subsections: § 1.7.1 is the repeatable, from-scratch -procedure for adding *any* new site or API to this Pi (this deployment's domains included, the -first time); § 1.7.2 is simply what that procedure produced for `slpsoftware.nl` — read it as the -worked example, not a separate step. +**Revised from the original design**: the reverse proxy that terminates TLS for these domains runs +on a **different, dedicated Pi** ("the proxy Pi"), not on this one ("pi-main", where +`gitea-workflow` and the release directories from §§ 1.2–1.4 live). The proxy Pi already handles +SSL and forwards plain HTTP to pi-main. Two consequences that change earlier sections: -#### 1.7.1 Adding a New Domain From Scratch +- **Kestrel must be reachable over the LAN, not just loopback** — hence `ASPNETCORE_URLS=http://0.0.0.0:` + in § 1.5, not `http://localhost:` +- **pi-main runs no nginx and holds no certificates for these domains at all** — everything in this + section happens **on the proxy Pi**, except the firewall step at the end, which is on pi-main + +As in § 1.7.1–1.7.2 below: § 1.7.1 is the repeatable, from-scratch procedure for routing *any* new +domain through the proxy Pi to *any* backend (this deployment's domains included, the first time); +§ 1.7.2 is what that procedure produced for `slpsoftware.nl` — the worked example, not a separate +step. + +#### 1.7.1 Adding a New Domain From Scratch (on the proxy Pi) Starting from nothing — no existing server block, no certificate — for a new domain -`` routing to a local port ``: +`` that should route to `:` (a service on some other Pi +or container on the LAN — pi-main and its two ports, in this deployment's case): **Step 1 — plain HTTP block, no TLS yet.** Certbot's nginx plugin (step 2) needs a working HTTP server block for the domain to attach to and to answer the HTTP-01 validation challenge; asking @@ -233,7 +247,7 @@ server { listen 80; server_name ; location / { - proxy_pass http://127.0.0.1:; + proxy_pass http://:; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } @@ -245,8 +259,9 @@ Save as `/etc/nginx/sites-available/`, symlink it into `sites-enable sudo nginx -t && sudo systemctl reload nginx ``` -Confirm the domain's DNS `A`/`AAAA` record already points at this Pi before continuing — the -HTTP-01 challenge in step 2 needs the domain to actually resolve here. +Confirm the domain's DNS `A`/`AAAA` record already points at **the proxy Pi** (not the backend) +before continuing — the HTTP-01 challenge in step 2 needs the domain to resolve to whichever host +is answering on port 80, which is the proxy Pi. **Step 2 — request and install the certificate.** The nginx plugin edits the file from step 1 in place: it adds the `listen 443 ssl` block, the certificate/key paths, and (by default) an @@ -260,23 +275,34 @@ Certbot's own systemd timer handles renewal automatically — nothing further to ```bash curl -I https:///health # or whichever path this new site/API actually serves ``` -Confirm it resolves over HTTPS with a valid certificate and reaches the expected local port. +Confirm it resolves over HTTPS with a valid certificate and reaches the expected backend. Repeat steps 1–3 once per domain. This is the same procedure regardless of whether the new domain is another environment for this feature, a completely different project's API, or a plain static -site — nginx and certbot don't know or care what's listening on the local port they proxy to. +site — nginx and certbot don't know or care what's actually listening on the backend they proxy to. + +**Step 4 — restrict the backend port on pi-main (do this once the domain works end to end).** +Since Kestrel now listens on `0.0.0.0:` (§ 1.5), anything on the LAN can reach it +directly, bypassing the proxy Pi's TLS entirely, unless pi-main's firewall says otherwise. On +**pi-main**: +```bash +sudo ufw allow from to any port proto tcp +sudo ufw deny /tcp +``` +(Or the equivalent `nftables`/`iptables` rules if `ufw` isn't what this Pi uses — the point is: +only the proxy Pi's address may reach these ports, everything else is denied.) #### 1.7.2 Current State for This Deployment -Running the procedure above for `test.slpsoftware.nl` and `slpsoftware.nl` produces (after -certbot has added its blocks) server blocks equivalent to: +Running the procedure above (on the proxy Pi) for `test.slpsoftware.nl` and `slpsoftware.nl`, +pointing at pi-main's LAN address, produces server blocks equivalent to: ```nginx server { listen 443 ssl; server_name test.slpsoftware.nl; location / { - proxy_pass http://127.0.0.1:5100; + proxy_pass http://:5100; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } @@ -287,7 +313,7 @@ server { listen 443 ssl; server_name slpsoftware.nl; location / { - proxy_pass http://127.0.0.1:5101; + proxy_pass http://:5101; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }