Commit graph

71 commits

Author SHA1 Message Date
mwiegand
1968684c03
fix(deploy): MountFlags=shared on web service for fuse mount propagation
ProtectSystem=full + ReadWritePaths implicitly give the unit a private
mount namespace (systemd needs to remount /usr read-only). The default
namespace propagation is slave, so mounts the worker creates inside
never reach the host. The gameserver units (started via systemctl,
each with their own namespace) then inherit a host that lacks the
overlay, and their CHDIR into /var/lib/left4me/runtime/<name>/merged
fails.

Set MountFlags=shared so mount events propagate from the worker's
namespace back to the host, then onward to gameserver units at their
unshare time.

Verified on test box: nsenter -t <gunicorn-pid> -m mount showed the
fuse-overlayfs mount inside the worker but plain mount on the host
did not, while web unit had ProtectSystem=full + ReadWritePaths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:01:24 +02:00
mwiegand
593611e194
fix(deploy): drop PrivateTmp on web service so fuse mounts propagate
PrivateTmp=true gives the unit a private mount namespace. The worker's
fuse-overlayfs mount lives only inside that namespace, so the host
cannot see it and the gameserver unit (started via systemctl, with its
own namespace inherited from the host) also cannot see it. The
gameserver unit then fails CHDIR on
/var/lib/left4me/runtime/<name>/merged/left4dead2.

The mount must land in the host namespace so the gameserver unit
inherits it at unshare time. Remaining hardening: dedicated user,
ProtectSystem=full, ReadWritePaths, sudoers allowlist limited to two
helper scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:57:43 +02:00
mwiegand
56b9523d88
fix(deploy): drop NoNewPrivileges on web service so FUSE mounts work
The job worker calls fusermount3 (setuid-root) to mount per-instance
FUSE overlays and sudo to invoke the privileged systemctl wrapper.
NoNewPrivileges=true blocks both, surfacing as
"fusermount3: mount failed: Operation not permitted" the first time a
server is started. Hardening is still enforced via dedicated user,
PrivateTmp, ProtectSystem=full, ReadWritePaths, and the narrow sudoers
allowlist limited to two helper scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:51:39 +02:00
mwiegand
d14ed9c117
feat(web): blueprint-prefilled create-server flow + empty-state CTA
- Per-row "Create server" link on /blueprints navigates to
  /servers?blueprint_id=<id>; that page validates the param against
  the user's owned blueprints, pre-selects the option, and auto-opens
  the create modal.
- /servers empty-blueprint state now shows an actionable
  "Create a blueprint first ->" link (styled like the primary button)
  pointing at /blueprints, replacing the silent disabled "+ Create"
  button + muted hint.
- Drop the "Reassign blueprint" form on the server detail page
  along with the unused POST /servers/<id> form route. The JSON
  PATCH /servers/<id> endpoint is retained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:47:33 +02:00
mwiegand
923a1840f4
feat(web): forms in modals, edit/delete on detail pages, port auto-assign
- Native <dialog> modal infra (CSS + ~30 LOC JS, no framework) used for
  create forms and delete confirmations.
- Index pages become listing-only: + Create button opens a modal; the
  broken blueprint Actions column and inline overlay edit cells are gone.
- Server detail gains a blueprint reassignment form; existing Delete
  button now opens a confirmation modal before tearing down the runtime.
- Blueprint detail gains a Delete button + confirmation modal (was
  unreachable from the UI before).
- New overlay detail page at /overlays/<id> with edit form, "Used by"
  blueprints list, and delete (admin only).
- Server create: port field is now optional; backend auto-assigns the
  next free port from LEFT4ME_PORT_RANGE_START/_END (default
  27015-27115). 409 on range exhaustion.
- New routes: POST /blueprints/<id>/delete (form sentinel matching
  overlays pattern), POST /servers/<id> (form-friendly blueprint
  reassign), GET /overlays/<id>.
- Server delete operation now redirects to /servers; overlay update
  redirects to /overlays/<id>.

Server rename remains unsupported pending an id-vs-name design pass for
l4d2host (the runtime directory is name-keyed; renaming would orphan
files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:30:33 +02:00
mwiegand
7d9939c71d
fix(deploy): exclude macOS AppleDouble files from deploy archive
When tar runs on macOS it embeds ._* resource-fork sidecars next to each
file. These ended up under l4d2web/alembic/versions/ on the target and
alembic tried to import them as migration modules, failing with
"source code string cannot contain null bytes". Set COPYFILE_DISABLE=1
and add an --exclude '._*' so the archive is portable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:58:29 +02:00
mwiegand
0210ecd301
config: allow SESSION_COOKIE_SECURE override and disable on test deploy
The HTTP-only test deployment binds gunicorn to 0.0.0.0:8000 with no TLS
terminator, so a hardcoded SESSION_COOKIE_SECURE=True breaks browser
login. Make it opt-out via env (default True outside TESTING) and set
SESSION_COOKIE_SECURE=false in the generated web.env so the test box
keeps working over HTTP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:56:48 +02:00
mwiegand
f81e839ba2
security: harden boundary inputs and production defaults
- validate instance names at the host lib and web boundary against
  [a-z0-9][a-z0-9_-]{0,63} to prevent path traversal via Server.name
- fail-closed on SECRET_KEY: load_config returns None when env unset,
  create_app raises if missing or "dev" outside TESTING
- close login timing oracle by hashing a dummy digest when the user
  is not found, equalizing response time
- set SESSION_COOKIE_SECURE outside TESTING
- delete_instance tolerates stop_service and fusermount3 failures so
  partially-initialized instances clean up without contract breaks;
  drops the is_mount() preflight that violated AGENTS.md
- document claim_next_job's single-process assumption
- clarify emit_step contract via docstring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:53:33 +02:00
mwiegand
3809f85795
fix: load environment variables for alembic upgrade in deploy script to ensure database url is set properly 2026-05-06 21:01:35 +02:00
mwiegand
441c1db79b
fix: change directory before running alembic upgrade in deploy script to avoid pyproject.toml permission issues 2026-05-06 21:00:59 +02:00
mwiegand
fa566db820
fix: apply alembic migrations automatically on deployment 2026-05-06 21:00:25 +02:00
mwiegand
c01359002a
fix: enable batch operations in alembic for sqlite unique constraints 2026-05-06 20:59:18 +02:00
mwiegand
7e5a8f89b5
docs: add server port constraint implementation plan 2026-05-06 20:53:50 +02:00
mwiegand
114b141e2a
test: add test for duplicate port constraint 2026-05-06 20:53:04 +02:00
mwiegand
fa002ce0f2
feat: enforce unique port constraint on servers 2026-05-06 20:52:46 +02:00
mwiegand
833ae318cf
fix(deploy): add venv to PATH in left4me-web systemd service 2026-05-06 20:45:37 +02:00
mwiegand
1604859f41
feat(host): add step logging to steam_install 2026-05-06 20:41:39 +02:00
mwiegand
005d2d8458
fix(host): enforce flush=True to prevent pipeline block buffering 2026-05-06 20:34:41 +02:00
mwiegand
27a905c22b
feat(web): add boundary log lines to job worker execution 2026-05-06 20:18:23 +02:00
mwiegand
38d04e8551
feat(host): emit steps during start, stop, and delete operations 2026-05-06 20:07:00 +02:00
mwiegand
d977098344
feat(host): emit steps during initialize_instance 2026-05-06 20:04:08 +02:00
mwiegand
700b5be6f8
feat(host): add _emit_step helper for lifecycle logging 2026-05-06 20:00:07 +02:00
mwiegand
ee144fad96
feat(l4d2-web): add server creation form 2026-05-06 19:41:04 +02:00
mwiegand
bbfc528354
feat(deploy): add production-like test deployment 2026-05-06 19:30:10 +02:00
mwiegand
de86139323
feat(l4d2): add l4d2ctl host command boundary 2026-05-06 16:35:20 +02:00
mwiegand
a347829608
feat(l4d2-web): add job pages and cancellation 2026-05-06 15:05:13 +02:00
mwiegand
91d042cf33
feat(l4d2-web): execute queued lifecycle jobs 2026-05-06 14:08:18 +02:00
mwiegand
df680f6226
fix(l4d2-web): reject encoded unsafe redirects 2026-05-06 13:24:04 +02:00
mwiegand
58fb8b2b63
fix(l4d2-web): harden auth redirect targets 2026-05-06 13:01:48 +02:00
mwiegand
deca2c9153
docs(l4d2-web): update auth contract 2026-05-06 12:55:38 +02:00
mwiegand
0aca36506f
feat(l4d2-web): add login page and safe redirects 2026-05-06 12:52:22 +02:00
mwiegand
942dada807
docs(l4d2-web): plan auth pages 2026-05-06 12:48:03 +02:00
mwiegand
84f325bb03
chore(l4d2-web): remove obsolete admin overlay template 2026-05-06 12:44:06 +02:00
mwiegand
08b32bb26f
docs(l4d2-web): design auth pages 2026-05-06 12:40:15 +02:00
mwiegand
07495f9319
docs(l4d2-web): update ui color contract 2026-05-06 12:10:15 +02:00
mwiegand
4b326736fe
feat(l4d2-web): add admin landing and system pages 2026-05-06 12:09:36 +02:00
mwiegand
feab09db07
feat(l4d2-web): add form-based blueprint editor 2026-05-06 12:09:08 +02:00
mwiegand
71004a9deb
feat(l4d2-web): add server pages and lifecycle forms 2026-05-06 12:08:19 +02:00
mwiegand
6559cf314e
feat(l4d2-web): consolidate overlay catalog page 2026-05-06 12:07:28 +02:00
mwiegand
881b6635f9
feat(l4d2-web): add neutral shell and theme tokens 2026-05-06 12:06:23 +02:00
mwiegand
98872727a7
chore(l4d2): add worktree constraint and update color contract 2026-05-06 11:57:04 +02:00
mwiegand
d090750a50
docs(l4d2-web): plan functional web ui 2026-05-06 11:34:23 +02:00
mwiegand
a766fe631d
chore: ignore code index files 2026-05-05 23:48:39 +02:00
mwiegand
288eda7c37
chore(l4d2): flatten component layout 2026-05-05 23:47:06 +02:00
mwiegand
7bac34c886
docs(l4d2): document host prerequisites 2026-05-05 23:30:34 +02:00
mwiegand
16412f37f2
fix(l4d2): stabilize host smoke lifecycle 2026-05-05 23:23:26 +02:00
mwiegand
2621b56627
chore: ignore local sqlite runtime artifacts 2026-04-23 01:24:53 +02:00
mwiegand
d76d72f37e
docs(l4d2-web): finalize blueprint-driven ui and deployment contracts 2026-04-23 01:23:17 +02:00
mwiegand
ec74563705
feat(l4d2-web): add csrf, rate limiting, and sqlite reliability settings 2026-04-23 01:19:29 +02:00
mwiegand
f9c98506bd
feat(l4d2-web): add live server logs and desired-vs-actual status model 2026-04-23 01:16:37 +02:00