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.
65 lines
2.2 KiB
Desktop File
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
|