Feature/gitea deployment workflow #1
@@ -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
|
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.
|
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)
|
## 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).
|
- **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.
|
- **Environments**: local, test, production only.
|
||||||
|
|||||||
@@ -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
|
**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
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
+9
@@ -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
|
workflow never reads, writes or reasons about a port. Its only environment-specific inputs are the
|
||||||
deploy path and which systemd unit to restart.
|
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
|
**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
|
runtime is already installed on the Pi (ASM-03). Smaller artifact, faster upload over the same SSH
|
||||||
transport every other unit already relies on.
|
transport every other unit already relies on.
|
||||||
|
|||||||
+44
-18
@@ -136,7 +136,11 @@ Contents (fill in real values — this file is never read by the workflow, only
|
|||||||
below):
|
below):
|
||||||
```ini
|
```ini
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
ASPNETCORE_ENVIRONMENT=Production
|
||||||
ASPNETCORE_URLS=http://localhost:<port>
|
# 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:<port>
|
||||||
ConnectionStrings__DefaultConnection=Server=127.0.0.1;Port=3306;Database=SlpSoftware<Env>;Uid=<user>;Pwd=<password>
|
ConnectionStrings__DefaultConnection=Server=127.0.0.1;Port=3306;Database=SlpSoftware<Env>;Uid=<user>;Pwd=<password>
|
||||||
JwtSettings__Secret=<secure-long-random-secret>
|
JwtSettings__Secret=<secure-long-random-secret>
|
||||||
JwtSettings__Issuer=SlpModularCms
|
JwtSettings__Issuer=SlpModularCms
|
||||||
@@ -211,18 +215,28 @@ systemctl --user enable slpsoftware-test.service
|
|||||||
systemctl --user enable slpsoftware-production.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,
|
**Revised from the original design**: the reverse proxy that terminates TLS for these domains runs
|
||||||
routing to the matching local port. Two subsections: § 1.7.1 is the repeatable, from-scratch
|
on a **different, dedicated Pi** ("the proxy Pi"), not on this one ("pi-main", where
|
||||||
procedure for adding *any* new site or API to this Pi (this deployment's domains included, the
|
`gitea-workflow` and the release directories from §§ 1.2–1.4 live). The proxy Pi already handles
|
||||||
first time); § 1.7.2 is simply what that procedure produced for `slpsoftware.nl` — read it as the
|
SSL and forwards plain HTTP to pi-main. Two consequences that change earlier sections:
|
||||||
worked example, not a separate step.
|
|
||||||
|
|
||||||
#### 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:<port>`
|
||||||
|
in § 1.5, not `http://localhost:<port>`
|
||||||
|
- **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
|
Starting from nothing — no existing server block, no certificate — for a new domain
|
||||||
`<new-domain>` routing to a local port `<local-port>`:
|
`<new-domain>` that should route to `<backend-host>:<backend-port>` (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
|
**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
|
server block for the domain to attach to and to answer the HTTP-01 validation challenge; asking
|
||||||
@@ -233,7 +247,7 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name <new-domain>;
|
server_name <new-domain>;
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:<local-port>;
|
proxy_pass http://<backend-host>:<backend-port>;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
@@ -245,8 +259,9 @@ Save as `/etc/nginx/sites-available/<new-domain>`, symlink it into `sites-enable
|
|||||||
sudo nginx -t && sudo systemctl reload nginx
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm the domain's DNS `A`/`AAAA` record already points at this Pi before continuing — the
|
Confirm the domain's DNS `A`/`AAAA` record already points at **the proxy Pi** (not the backend)
|
||||||
HTTP-01 challenge in step 2 needs the domain to actually resolve here.
|
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
|
**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
|
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
|
```bash
|
||||||
curl -I https://<new-domain>/health # or whichever path this new site/API actually serves
|
curl -I https://<new-domain>/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
|
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
|
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:<backend-port>` (§ 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 <proxy-pi-lan-ip> to any port <backend-port> proto tcp
|
||||||
|
sudo ufw deny <backend-port>/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
|
#### 1.7.2 Current State for This Deployment
|
||||||
|
|
||||||
Running the procedure above for `test.slpsoftware.nl` and `slpsoftware.nl` produces (after
|
Running the procedure above (on the proxy Pi) for `test.slpsoftware.nl` and `slpsoftware.nl`,
|
||||||
certbot has added its blocks) server blocks equivalent to:
|
pointing at pi-main's LAN address, produces server blocks equivalent to:
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name test.slpsoftware.nl;
|
server_name test.slpsoftware.nl;
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:5100;
|
proxy_pass http://<pi-main-lan-ip>:5100;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
@@ -287,7 +313,7 @@ server {
|
|||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name slpsoftware.nl;
|
server_name slpsoftware.nl;
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:5101;
|
proxy_pass http://<pi-main-lan-ip>:5101;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user