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
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
+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
|
||||
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.
|
||||
|
||||
+44
-18
@@ -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:<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>
|
||||
JwtSettings__Secret=<secure-long-random-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:<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
|
||||
`<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
|
||||
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 <new-domain>;
|
||||
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 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
|
||||
```
|
||||
|
||||
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://<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
|
||||
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
|
||||
|
||||
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://<pi-main-lan-ip>: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://<pi-main-lan-ip>:5101;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user