Files
UmbraTrans/systemd/umbratrans-web-prototype.service
umbra audio 6482921d0b 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.
2026-09-26 19:16:16 -07:00

65 lines
2.2 KiB
Desktop File

[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