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:
64
systemd/umbratrans-web-prototype.service
Normal file
64
systemd/umbratrans-web-prototype.service
Normal 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
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user