left4me/deploy
mwiegand f6ca85fc6f
fix(deploy): chown left4me.db to left4me:left4me, not root:left4me
The v2 hardening tightened the DB to mode 0640 owned by root:left4me,
intending to block reads from the sandbox uid (l4d2-sandbox, not in the
left4me group). It did — but it also took away write access from the web
service itself, which runs as user left4me. With root owning the file,
left4me only had group-read; INSERTs into the jobs table failed with
"attempt to write a readonly database" and surfaced as a 500 on POST
/overlays/{id}/script.

Owner left4me + group left4me + mode 0640 keeps the same external
posture (l4d2-sandbox gets nothing via "other") while restoring the
web service's read+write access via "owner".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:09:47 +02:00
..
files feat(deploy): restrict script-sandbox egress to public internet only 2026-05-08 17:04:57 +02:00
templates/etc/left4me feat(deploy): add production-like test deployment 2026-05-06 19:30:10 +02:00
tests fix(deploy): chown left4me.db to left4me:left4me, not root:left4me 2026-05-08 17:09:47 +02:00
deploy-test-server.sh fix(deploy): chown left4me.db to left4me:left4me, not root:left4me 2026-05-08 17:09:47 +02:00
README.md docs(deploy): replace globals overlay description with script overlays 2026-05-08 15:56:24 +02:00

left4me Deployment

This directory contains the production-like test deployment for a Linux server. It installs the repository into a fixed host layout, configures a dedicated runtime user, installs systemd units, and wires the web app to host operations through privileged helper commands.

Target Layout

The deployment uses these paths:

  • /etc/left4me/host.env: host library environment configuration.
  • /etc/left4me/web.env: web app environment configuration.
  • /opt/left4me/.venv: Python virtual environment for deployed commands.
  • /opt/left4me: deployed repository contents.
  • /var/lib/left4me/left4me.db: SQLite database used by the web app.
  • /var/lib/left4me/installation: shared L4D2 installation.
  • /var/lib/left4me/overlays: overlay directories. Each overlay lives at ${overlay_id} under here.
  • /var/lib/left4me/workshop_cache: deduplicated cache of .vpk files downloaded for workshop overlays. One file per Steam item, named {steam_id}.vpk. Workshop overlays symlink into this tree.
  • /var/lib/left4me/global_overlay_cache: cache of non-Steam map archives and extracted .vpk files used by managed global map overlays.
  • /var/lib/left4me/instances: rendered instance specifications and per-instance state.
  • /var/lib/left4me/runtime: per-instance runtime mount directories.
  • /var/lib/left4me/tmp: temporary files used by deployment/runtime operations.
  • /usr/local/lib/systemd/system: global systemd unit files, including left4me-server@.service.
  • /usr/local/libexec/left4me: privileged helper commands, including left4me-systemctl, left4me-journalctl, and left4me-overlay (the latter mounts the per-instance kernel overlay in PID 1's mount namespace via nsenter).
  • /etc/sudoers.d/left4me: sudoers rules allowing the web/runtime commands to call the helpers non-interactively.

Static units are generated for /var/lib/left4me. If LEFT4ME_ROOT changes, regenerate and reinstall the unit files instead of reusing the existing static units.

Runtime User

The deployment creates and runs host operations as the dedicated runtime user:

  • Username: left4me
  • Home: /var/lib/left4me
  • Shell: /usr/sbin/nologin

Running A Test Deployment

Run the deployment from the repository root:

deploy/deploy-test-server.sh deploy-user@example-host

The SSH user must be able to run sudo on the target host. The deployment configures system packages, directories, environment files, helper scripts, sudoers rules, Python dependencies, and systemd units.

Admin Bootstrap

Set the bootstrap credentials in the environment when creating the first admin user:

LEFT4ME_ADMIN_USERNAME=admin \
LEFT4ME_ADMIN_PASSWORD='change-me' \
flask create-user "$LEFT4ME_ADMIN_USERNAME" --admin

Use a strong one-time password and rotate it after first login if needed.

Overlay References

Overlay references are relative paths below ${LEFT4ME_ROOT}/overlays. With the default deployment root, they resolve under /var/lib/left4me/overlays. New overlays use ${overlay_id} as their path; the digit-only form is the only one created by the web app.

Invalid references are rejected:

  • Absolute paths such as /srv/overlay.
  • Parent traversal such as ../other or competitive/../../base.
  • Empty path components such as competitive//base.
  • Symlink escapes that resolve outside ${LEFT4ME_ROOT}/overlays.

The web app currently supports two overlay surfaces:

  • workshop overlays (user-owned) — populated by downloading .vpk files from the public Steam Web API into ${LEFT4ME_ROOT}/workshop_cache/{steam_id}.vpk and creating absolute symlinks under ${LEFT4ME_ROOT}/overlays/{overlay_id}/left4dead2/addons/{steam_id}.vpk.
  • script overlays — populated by an arbitrary user-authored bash script that runs inside bubblewrap + systemd-run --scope as the unprivileged l4d2-sandbox UID, with the overlay directory bind-mounted RW at /overlay. Resource caps: 1h walltime, 4 GB RAM, 512 tasks, 200% CPU, 20 GB post-build disk cap.

Both the caches and the overlay directories are owned by the left4me runtime user; if the web service ever runs as a different uid, ensure it shares a group with the host process and that both trees are group-readable.