Fixes the account model deploy setup got wrong

webadmin turned out to be FTP-only and belongs to whoever uploads
customer websites, not to this pipeline. Switched to a separate
deploy account with its own home-directory paths, well away from
where the other sites live, and made the persistent website symlink
cross into webadmin's territory instead of the other way around.
This commit is contained in:
2026-07-28 19:41:34 +02:00
parent cf80f827ae
commit 2f243f23c3
5 changed files with 157 additions and 20 deletions
@@ -14,12 +14,34 @@ assumes already exists — `deploy-scp.yaml` (U6) never creates any of it.
### 1.1 Prerequisites
- .NET 10 runtime installed on the Pi (ASM-03) — the publish is framework-dependent
(`infrastructure-design.md` § 1), so the Pi needs the runtime, not the full SDK
- SSH access for the deploy user, password auth for now (matches the reference project;
SSH-key migration remains a documented future step, same as there)
- `sqlcmd` installed, for the backup script (§ 4) — e.g. `mssql-tools18` / `unixodbc` on Debian-based
Raspberry Pi OS
### 1.2 Enable Lingering (INFRA-U6-01 — do this first, easy to forget)
### 1.2 Account Model (revised — `webadmin` is not the deploy account)
Clarified during Deployment Setup: `webadmin` is the FileZilla/SFTP account website-workspace
authors use to upload customer sites under `/mnt/storage1/www/html/` (`WEBSITE_WORKSPACE.md`'s
role) — it cannot SSH in, and should **stay** SFTP-only. `deploy-scp.yaml` needs a real SSH shell
(for `mkdir`, `ln -sfn`, `systemctl --user restart`, the backup script), which is a different kind
of access than FileZilla uses.
**Preferred**: create a separate, dedicated account for the deploy pipeline — e.g.
`slpmodularcms-deploy` (rename freely; nothing in the workflow hardcodes this name, it only reads
`PI_MAIN_USERNAME`). This keeps the two roles cleanly apart, matching your stated preference.
```bash
sudo useradd -m -s /bin/bash slpmodularcms-deploy
sudo passwd slpmodularcms-deploy
```
**Fallback, only if a new account genuinely isn't feasible** (e.g. a hosting provider that doesn't
allow arbitrary new system accounts): grant `webadmin` SSH shell access instead
(`sudo usermod -s /bin/bash webadmin`, plus enabling SSH password/key auth for it if currently
blocked at the `sshd_config` level). This merges the two roles onto one account — acceptable as a
fallback, but worth revisiting later, since it means a website-workspace author's FTP credential
would also be able to run shell commands on the Pi.
The rest of this document uses `<deploy-user>` — substitute whichever of the two you actually use.
### 1.3 Enable Lingering (INFRA-U6-01 — do this first, easy to forget)
```bash
sudo loginctl enable-linger <deploy-user>
```
@@ -29,15 +51,38 @@ common "Failed to connect to bus" error `systemctl --user` can throw when invoke
non-interactive SSH command — lingering keeps the user's systemd instance (and `XDG_RUNTIME_DIR`)
running independent of any login session.
### 1.3 Directory Skeleton
Run once per environment (`test`, `production`):
### 1.4 Directory Skeleton
Deliberately placed under `<deploy-user>`'s **own home directory**, not under
`/mnt/storage1/www/html/` — since it's now a separate account from `webadmin`, there is no reason
for the CMS's own release/current/shared structure to live anywhere near the other websites at all,
which directly avoids interfering with them (as you asked in Q2):
```bash
mkdir -p ~/apps/slpmodularcms-<env>/releases
mkdir -p ~/apps/slpmodularcms-<env>/shared/wwwroot-web
```
`current` is created by the first deploy itself (`ln -sfn`) — don't pre-create it.
### 1.4 Runtime Configuration File
**`shared/wwwroot-web` is the one exception** — it must resolve to wherever `webadmin` actually
uploads *this* customer's website via FileZilla, e.g. `/mnt/storage1/www/html/slpmodularcms-<env>/`
(adjust the exact folder name to whatever convention the other sites under `html/` already use, if
one exists). Rather than a plain directory, make it a symlink across accounts:
```bash
mkdir -p ~/apps/slpmodularcms-<env>
ln -s /mnt/storage1/www/html/slpmodularcms-<env> ~/apps/slpmodularcms-<env>/shared/wwwroot-web
```
`deploy-scp.yaml`'s existing logic (`releases/{ts}/wwwroot/web -> ../../../shared/wwwroot-web`)
needs no changes for this — it only ever resolves the symlink chain, it doesn't care how many hops
that chain has. What **does** need attention: `<deploy-user>` needs read + traverse permission on
`/mnt/storage1/www/html/slpmodularcms-<env>/` and its parent directories, which `webadmin` owns.
Simplest fix: put both accounts in a shared group (e.g. `webshared`), `chgrp -R webshared` that
folder, and make sure webadmin's FTP server creates new uploads group-readable (`g+rx`, not just
owner-readable) — a one-time permission setup, not something either pipeline touches per deploy.
### 1.5 Runtime Configuration File
One file per environment, **outside** the release directory so it survives every switch:
```bash
touch ~/apps/slpmodularcms-<env>/shared/env
@@ -63,11 +108,11 @@ occupies them. `ASPNETCORE_ENVIRONMENT=Production` is used for **both** environm
should be true for anything reachable at a real domain, including test.
**`SecurityHeaders__AllowedScriptOrigins__0` / `_AllowedConnectOrigins__0` must exactly match** the
Gitea variables `SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION` (see § 3) — REF-U5-01's CI
Gitea variables `SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION` (see § 1.9) — REF-U5-01's CI
gate only catches drift between the frontend build and that Gitea variable; it cannot see this file,
so keeping the two in sync is a manual discipline, not something enforced automatically.
### 1.5 systemd User Units
### 1.6 systemd User Units
Create `~/.config/systemd/user/slpmodularcms-test.service`:
```ini
[Unit]
@@ -96,7 +141,7 @@ systemctl --user enable slpmodularcms-test.service
systemctl --user enable slpmodularcms-production.service
```
### 1.6 nginx Routing
### 1.7 nginx Routing
The existing reverse proxy (`infrastructure-design.md` § 1, § 4) needs a server block per domain,
routing to the matching local port:
```nginx
@@ -123,7 +168,7 @@ server {
TLS certificate provisioning (e.g. certbot) is existing host operations, out of scope for this
feature — assumed already handled the same way the reference project's domains are.
### 1.7 Database Backup Credentials (§ 4 depends on this)
### 1.8 Database Backup Credentials (§ 4 depends on this)
```bash
touch ~/.config/slpmodularcms-db-backup.env
chmod 600 ~/.config/slpmodularcms-db-backup.env
@@ -134,21 +179,24 @@ DB_NAME=SlpModularCmsProduction
DB_USER=<a-login-with-backup-database-permission>
DB_PASSWORD=<password>
```
Kept **separate** from `shared/env` (§ 1.4) deliberately — the backup script needs its own
Kept **separate** from `shared/env` (§ 1.5) deliberately — the backup script needs its own
credential, ideally scoped to just `BACKUP DATABASE` permission rather than the application's own
data-access login.
### 1.8 Gitea Actions Variables and Secrets
Set once, in this repository's Gitea Actions settings:
### 1.9 Gitea Actions Variables and Secrets
Set once, in this repository's Gitea Actions settings. This is exactly where the real path lives —
`deploy-scp.yaml` never hardcodes it, it only reads `${{ vars.DEPLOY_PATH_TEST }}` /
`${{ vars.DEPLOY_PATH_PRODUCTION }}`, so changing the path later is a variable edit, not a workflow
change:
| Name | Kind | Value |
|---|---|---|
| `PI_MAIN_ADDRESS` | secret | the Pi's address |
| `PI_MAIN_PORT` | secret | SSH port |
| `PI_MAIN_USERNAME` | secret | deploy user |
| `PI_MAIN_PASSWORD` | secret | deploy user's password |
| `DEPLOY_PATH_TEST` | variable | `/home/<user>/apps/slpmodularcms-test` |
| `DEPLOY_PATH_PRODUCTION` | variable | `/home/<user>/apps/slpmodularcms-production` |
| `PI_MAIN_USERNAME` | secret | `<deploy-user>` — the new dedicated account (§ 1.2), **not** `webadmin` |
| `PI_MAIN_PASSWORD` | secret | `<deploy-user>`'s password |
| `DEPLOY_PATH_TEST` | variable | `/home/<deploy-user>/apps/slpmodularcms-test` |
| `DEPLOY_PATH_PRODUCTION` | variable | `/home/<deploy-user>/apps/slpmodularcms-production` |
| `SERVICE_NAME_TEST` | variable | `slpmodularcms-test.service` |
| `SERVICE_NAME_PRODUCTION` | variable | `slpmodularcms-production.service` |
| `HEALTH_CHECK_URL_TEST` | variable | `https://test.slpsoftware.nl/health` |
@@ -156,7 +204,7 @@ Set once, in this repository's Gitea Actions settings:
| `VITE_SENTRY_DSN` | variable | Sentry DSN (shared, not sensitive — safe in the client bundle) |
| `VITE_UMAMI_SCRIPT_URL` | variable | Umami script host (shared) |
| `VITE_UMAMI_WEBSITE_ID_TEST` / `_PRODUCTION` | variable | per-environment Umami website ID |
| `SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION` | variable | **must match** § 1.4's `SecurityHeaders__AllowedScriptOrigins__0` for that environment |
| `SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION` | variable | **must match** § 1.5's `SecurityHeaders__AllowedScriptOrigins__0` for that environment |
---
@@ -194,7 +242,7 @@ ENVIRONMENT="${1:?Usage: backup-slpmodularcms-db.sh <environment>}"
CREDENTIALS_FILE="$HOME/.config/slpmodularcms-db-backup.env"
if [[ ! -f "$CREDENTIALS_FILE" ]]; then
echo "Missing $CREDENTIALS_FILE — see deployment-instructions.md § 1.7" >&2
echo "Missing $CREDENTIALS_FILE — see deployment-instructions.md § 1.8" >&2
exit 1
fi
# shellcheck source=/dev/null
@@ -25,6 +25,14 @@ procedure.
Same physical host for both (per `infrastructure-design.md` § 1) — separated by directory,
`systemd --user` unit, and local port, never by anything the workflow manages directly.
**Account model (revised at Deployment Setup)**: the Pi's existing `webadmin` account is FileZilla/
SFTP-only, used by website-workspace authors uploading customer sites under
`/mnt/storage1/www/html/` (`WEBSITE_WORKSPACE.md`'s role) — it cannot SSH in and stays that way. The
deploy pipeline uses a **separate, dedicated** SSH-capable account instead
(`deployment-instructions.md` § 1.2), so the two roles never overlap on one credential. Deploy paths
live under that account's own home directory, not under `html/` — this was a deliberate choice to
avoid the CMS's release/current/shared structure interfering with the other websites hosted there.
## Rationale for What's Documented Here vs. Already Decided
| Already decided (Construction) | Documented here (Operations) |