[Unit] Description=UmbraTrans account and organization service (loopback/LAN, TLS terminated upstream) Documentation=file:///home/umbra/src/UmbraTrans/docs/umbratrans-backend-design.md After=network-online.target Wants=network-online.target # `failed` must not be a resting state for this service. # # Two attempts to get this right, both wrong in the same way. # # The original was StartLimitIntervalSec=120 with a burst of 5 and RestartSec=5s, # which is twenty-five seconds of trying. On three separate boots something else # held port 8442 - a development shell serving web/, which this backend mounts # itself - the service exhausted the budget, and systemd then never tried again. # The deployment stayed down after the port was free, needing a person to notice. # # Widening it to 500s with a burst of 20 did not fix that. It only bought more # time: on the next boot the squatter outlasted twenty restarts and the unit # returned to the same permanent `failed`. The verification missed it because the # port was freed after three restarts, inside the window - so the test exercised # the easy case and the claim said "fixed". # # The lesson is that the rate limit is the wrong mechanism entirely, not that it # was set too low. Any finite budget can be exhausted by a fault that lasts longer # than the budget, and the failure is silent and permanent. # # So: interval 0 disables rate limiting entirely. This service retries for as long # as the fault lasts and recovers by itself the moment it clears. A genuine crash # loop logs every RestartSec instead of stopping - noisier, and vastly better than # a deployment that is quietly down. There is a `systemctl --user stop` for when # you mean it. StartLimitIntervalSec=0 StartLimitBurst=0 [Service] Type=simple WorkingDirectory=/home/umbra/src/UmbraTrans # Rootless unit: no User=, because the manager already runs as umbra and setting # it makes systemd try to resolve a user and fail with status=216/GROUP in this # environment. This is the copy to install under # ~/.config/systemd/user/; the system-unit copy in systemd/ keeps User=. # # The environment holds the DSN and the SMTP password, so it is read from a # 0600 file rather than from the unit or the command line, both of which are # world-readable in /proc. EnvironmentFile=/home/umbra/.config/umbratrans/umbratrans.env # The venv interpreter directly, not `uv run`. `uv run` wants to write a lock and # a build cache under ~/.cache, which ProtectHome=read-only forbids, so it failed # with "Read-only file system" and the unit restart-looped. A service has no # business depending on a developer tool's cache anyway: this way the process # starts from a fixed interpreter with nothing to resolve at boot, and a stale # cache cannot change what runs. ExecStart=/home/umbra/src/UmbraTrans/.venv/bin/python -m umbratrans_web serve --host 192.168.123.14 --port 8442 --repository postgres --allow-lan-bind --allow-production # Kept in step with ExecStart: a dependency change means re-running `uv sync` and # then `systemctl --user restart`, not editing this file. Restart=on-failure RestartSec=5s UMask=0077 StandardOutput=journal StandardError=journal SyslogIdentifier=umbratrans-web # The first-run setup token is written to this journal and nowhere else. It is # shown to whoever can read the journal, which is the point: control of the # service is what authorises creating the first administrator. # journalctl --user -u umbratrans-web | grep -A2 'FIRST-RUN SETUP' # --- Sandbox --------------------------------------------------------------- # Nothing here needs root, a new privilege, or a writable filesystem outside the # database socket directory. NoNewPrivileges=yes CapabilityBoundingSet= AmbientCapabilities= PrivateDevices=yes PrivateTmp=yes ProtectClock=yes ProtectControlGroups=yes ProtectHostname=yes ProtectKernelLogs=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectProc=invisible ProtectSystem=strict ProtectHome=read-only # The PostgreSQL socket lives under /run; the connection needs to reach it, and # an explicit read-write path is narrower than relaxing the whole tree. ReadWritePaths=/run/postgresql RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes MemoryDenyWriteExecute=yes SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallErrorNumber=EPERM [Install] WantedBy=default.target