Run the static prototype under systemd

The prototype was served by a bare `python3 -m http.server` in a foreground
shell. It has no restart policy, so it died silently and the public URL
returned a 502 from Caddy with valid DNS and a valid certificate already in
place. This unit makes the failure mode visible and recoverable.

umbratrans-web-prototype.service:
- Restart=on-failure with a 5s delay and a 120s/5-attempt start limit, so a
  crash loop stops retrying but a clean stop always starts again.
- Binds 0.0.0.0:8442 serving web/; Caddy keeps terminating TLS upstream.
- Sandboxed: runs unprivileged with an empty capability bounding set, no new
  privileges, read-only home and system, private tmp and devices, system call
  filtering, and no access to kernel or control-group interfaces.
- Journal output to the user journal for inspection.

PrivateUsers=yes is documented as deliberately omitted. It hides /etc/group
from the manager, so systemd cannot resolve supplementary groups and the unit
fails with status=216/GROUP. A probe unit confirmed the remaining sandbox
directives are all valid in user scope.

Verified: all three routes and the icon return 200 locally and the public
HTTPS URL returns 200. SIGKILL of the main PID recovered automatically within
the restart delay. `systemd-analyze verify` is clean.

Two follow-ups requiring root are recorded in web/README.md: the rootless
install must drop User= because a --user manager cannot setuid, and
`loginctl enable-linger umbra` is needed for the service to survive logout and
start at boot.
This commit is contained in:
umbra audio
2026-09-26 19:16:16 -07:00
parent b474995414
commit 6482921d0b
2 changed files with 105 additions and 3 deletions

View File

@@ -0,0 +1,64 @@
[Unit]
Description=UmbraTrans static UI prototype (HTTP on 8442, TLS terminated upstream by Caddy)
Documentation=file:///home/umbra/src/UmbraTrans/docs/umbratrans-ui-design.md
After=network-online.target
Wants=network-online.target
# A crash loop should stop trying rather than spin, but a clean stop (for
# example after `systemctl restart`) always starts again.
StartLimitIntervalSec=120
StartLimitBurst=5
[Service]
Type=simple
User=umbra
WorkingDirectory=/home/umbra/src/UmbraTrans/web
# The prototype is static content only. When the FastAPI application is
# promoted to serve this directory, replace this ExecStart and drop the
# --directory/--bind arguments; nothing else in this unit needs to change.
ExecStart=/usr/bin/python3 -m http.server 8442 --bind 0.0.0.0 --directory /home/umbra/src/UmbraTrans/web
Restart=on-failure
RestartSec=5s
# The process only needs to read its document root. It never writes.
UMask=0022
StandardOutput=journal
StandardError=journal
SyslogIdentifier=umbratrans-web-prototype
# --- Sandbox ---------------------------------------------------------------
# Nothing here needs root, a new privilege, a writable filesystem, or any
# Linux kernel interface. Removing those is what turns "a Python process
# serving files" into a process with very little to attack.
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
PrivateDevices=yes
PrivateTmp=yes
# PrivateUsers=yes is deliberately NOT set: it hides /etc/group from the
# service manager, so systemd cannot resolve supplementary groups and the
# unit fails with status=216/GROUP. The process is already unprivileged
# (User=umbra, empty capability bounding set), so user namespacing adds
# little and costs the ability to use Group=.
ProtectClock=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
ProtectHome=read-only
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
[Install]
WantedBy=multi-user.target

View File

@@ -4,17 +4,55 @@ This directory contains a local, mock-data prototype for review.
It does not connect to PostgreSQL, authenticate accounts, issue passwords, or change production data. The admin page demonstrates the intended flows only.
## Run locally
## Run under systemd
The prototype is served by `umbratrans-web-prototype.service`, which restarts
on failure and applies a restrictive sandbox (no root, no new privileges, empty
capability set, read-only home and system, system call filtering). It listens on
plain HTTP on `8442`; Caddy terminates TLS for
`https://umbratrans.bdmurphy.me/` and proxies here.
```bash
systemctl --user status umbratrans-web-prototype.service
systemctl --user restart umbratrans-web-prototype.service
journalctl --user -u umbratrans-web-prototype.service -f
```
The unit is tracked at `systemd/umbratrans-web-prototype.service`.
Two notes on that unit:
- A rootless install under `~/.config/systemd/user/` must drop `User=`. A
`--user` manager already runs as `umbra` and cannot setuid, so `User=`
produces `status=216/GROUP`. The tracked copy keeps `User=umbra` because it
is written for a system install under `/etc/systemd/system`.
- `Linger=no` for this account, so the service stops when the last session ends
and will not start at boot. `sudo loginctl enable-linger umbra` fixes that.
## Run in the foreground
Useful for iterating on the markup without touching the service.
```bash
cd /home/umbra/src/UmbraTrans
python3 -m http.server 8442 --bind 0.0.0.0 --directory web
```
Review URLs:
## Review URLs
- `http://<host>:8442/`
- `http://<host>:8442/workspace.html`
- `http://<host>:8442/uadmin/`
- `https://umbratrans.bdmurphy.me/` and `https://umbratrans.bdmurphy.me/uadmin/`
For production, put the application behind the approved TLS reverse proxy, restrict the listener/interface, and implement the authentication and authorization adapter described in the UI design document.
## Known limitation of the prototype server
`python3 -m http.server` lists directory contents for any path without an
`index.html`, so `/assets/` renders a file listing. It serves no secrets, but
it is one of the reasons the FastAPI application in `umbratrans_web/` will
replace this server rather than run alongside it. It also has no request size
limits, no timeouts, and no security headers.
For production, keep TLS at the approved reverse proxy, bind the application to
the required interface, and implement the authentication and authorization
adapter described in the UI design document.