# Milestone 0 host runtime and security plan Status: planning-only. Commands in this document are proposed for administrator review. None have been executed. ## Permissions-only access audit The audit inspected only existence, filesystem type, owner/mode, effective read/write/traverse permission, Docker context endpoints, and socket metadata. It did not read, print, copy, or inspect secret values, production databases, container metadata, or archive contents. | Boundary | Effective access for `sol6_vi` | Finding | |---|---|---| | `/var/www` | read/traverse, not write | Production tree names and readable files are exposed to the account | | `/var/www/lasereverything.net.db` and `app/` | read/traverse, not write | Production application and deployment configuration are readable | | production `.env.local` and BGBye `.env` | read, not write; mode `0644` | **Unsafe for a `sol6_vi`-owned rootless daemon**; values were not read | | `/var/lib/docker` | no read/write/traverse | Production Docker data is denied by host permissions | | `/var/lib/mysql` | no read/write/traverse | Production database storage is denied | | `/var/run/docker.sock` and `/run/docker.sock` | no read/write | Production daemon is denied; only the `default` context points to it | | `/srv/directus-archive` | no read/write/traverse | Root-only archive boundary works; timestamped child is inaccessible | | `/srv/codex-migration` | read/traverse, not write | Scrubbed migration source is available read-only | | `/srv/codex-secrets` | read/traverse at directory level, not write | Migration env is readable as intended; access must be narrowed for the new account to the one nonproduction file | | workspace | read/write | Current development tree is owned by `sol6_vi` | Conclusion: rootless Docker owned by `sol6_vi` would protect the production daemon but not production files already readable to that user. Any controller of that daemon could mount the readable production environment files. The production secret file modes also deserve independent remediation. ## Runtime account model comparison | Model | Advantages | Risks / cost | Decision | |---|---|---|---| | `sol6_vi` owns rootless Docker | Minimal provisioning; current workspace already writable | Can bind-mount world-readable production secrets and application files; mixes general Codex work with container authority | Rejected on this host | | dedicated `le_payload_dev` | Daemon owner has an explicit filesystem allow-list; project storage and cleanup are attributable; production paths can be proven inaccessible | Requires account, ACL, subordinate IDs, separate Git credentials/session, storage, and user service | **Recommended** | Future autonomous migration work should run in a Codex session whose OS identity is `le_payload_dev`. Do not grant `sol6_vi` generic access to the development daemon socket as a convenience; that weakens accountability. If orchestration must be initiated from another account, use a narrowly audited command wrapper or SSH into `le_payload_dev`, not a shared Docker group and not TCP exposure of the daemon. ## Exact proposed identity and filesystem model Proposed fixed identity: - user and primary group: `le_payload_dev`; - UID/GID: `1200` (verified unused at audit time; recheck immediately before creation); - shell/home: `/bin/bash`, `/home/le_payload_dev`; - no supplementary production groups, no `docker` group, no sudo/wheel membership; - subordinate UID and GID range: `165536:65536`, non-overlapping with the observed `sol6_ii:100000:65536` allocation; - dedicated project root: Btrfs subvolume `/srv/le-payload-dev`, owner `1200:1200`, mode `0700`; - workspace: `/srv/le-payload-dev/workspace/lasereverything.net.db`; - daemon data root: `/srv/le-payload-dev/runtime/docker`; - runtime socket: `/run/user/1200/docker.sock`; - Docker client/config: `/home/le_payload_dev/.docker` and `/home/le_payload_dev/.config/docker`; - Compose project prefix: `le_payload_dev`, enforced by wrapper preflight. Only the dedicated account receives: - read/write access to its workspace and runtime/storage subtree; - read/traverse ACL on the frozen scrubbed source only; - read ACL on `/srv/codex-secrets/le-payload-migration.env` only; - a separate nonproduction Git credential/deploy key with access limited to the application repository. It receives no ACL or group membership for `/var/www`, production database paths, production sockets/volumes, or `/srv/directus-archive`. ## Exact privileged prerequisites and commands These commands are a reviewable proposal for an administrator on Garuda/Arch Linux. Recheck UID/GID and subordinate ranges first. Do not paste secrets into the shell history. ```bash # 1. Recheck proposed IDs/ranges are unused. getent passwd 1200 getent group 1200 grep -E '^(le_payload_dev|[^:]+:165536:65536)$' /etc/subuid /etc/subgid # 2. Install rootless prerequisites. docker-rootless-extras is an Arch AUR # package and must be built/reviewed under the administrator's normal AUR policy. sudo pacman -S --needed rootlesskit slirp4netns fuse-overlayfs shadow # Then install the reviewed docker-rootless-extras AUR package by the host's # approved AUR procedure; do not curl a remote install script into a shell. # 3. Create the isolated identity. sudo groupadd --gid 1200 le_payload_dev sudo useradd --uid 1200 --gid le_payload_dev --create-home \ --home-dir /home/le_payload_dev --shell /bin/bash le_payload_dev sudo usermod --add-subuids 165536-231071 --add-subgids 165536-231071 le_payload_dev # 4. Create a quota-capable project subvolume and ownership boundary. sudo btrfs subvolume create /srv/le-payload-dev sudo chown 1200:1200 /srv/le-payload-dev sudo chmod 0700 /srv/le-payload-dev sudo -u le_payload_dev mkdir -p \ /srv/le-payload-dev/workspace \ /srv/le-payload-dev/runtime/docker \ /srv/le-payload-dev/data/{postgres,media,legacy-uploads,bgbye-models,tmp} \ /srv/le-payload-dev/artifacts/{browser,migration-reports} \ /srv/le-payload-dev/secrets # 5. Apply a 250 GiB ceiling to the project subvolume. sudo btrfs quota enable /srv sudo btrfs qgroup limit 250G /srv/le-payload-dev # 6. Grant only source traversal/read and one migration secret file. # Recheck the resolved `current` target before applying ACLs. readlink -f /srv/codex-migration/current sudo setfacl -m u:le_payload_dev:--x /srv/codex-migration sudo setfacl -R -m u:le_payload_dev:r-X /srv/codex-migration/source-20260710-175408 sudo setfacl -m u:le_payload_dev:--x /srv/codex-secrets sudo setfacl -m u:le_payload_dev:r-- /srv/codex-secrets/le-payload-migration.env # 7. Remediate production secret readability separately. # Root-owned Compose can still read mode 0600 files. Confirm the production # operator before applying because this changes production file permissions. sudo chown root:root /var/www/lasereverything.net.db/app/.env.local sudo chmod 0600 /var/www/lasereverything.net.db/app/.env.local sudo chown root:root /var/www/lasereverything.net.db/bgbye/.env sudo chmod 0600 /var/www/lasereverything.net.db/bgbye/.env # 8. As le_payload_dev, configure the rootless daemon with an explicit data root. sudo -iu le_payload_dev mkdir -p ~/.config/docker # Administrator writes ~/.config/docker/daemon.json as documented below, # then the dedicated user enables the packaged rootless user service. sudo -iu le_payload_dev systemctl --user enable --now docker.socket # 9. Optional only if reviewed stacks must survive logout. sudo loginctl enable-linger le_payload_dev ``` Proposed `/home/le_payload_dev/.config/docker/daemon.json`: ```json { "data-root": "/srv/le-payload-dev/runtime/docker", "live-restore": false, "log-driver": "local", "log-opts": { "max-size": "20m", "max-file": "5" } } ``` The exact Arch rootless unit name (`docker.service` versus `docker.socket`) must be verified from the reviewed package before enabling. Cgroup v2 is present; after provisioning, verify the rootless/security options and delegated controllers before relying on CPU, memory, or PID limits. ## Required positive and negative verification Run as `le_payload_dev`, printing only permissions/status: ```bash test ! -r /var/www/lasereverything.net.db/app/.env.local test ! -x /var/lib/docker test ! -x /var/lib/mysql test ! -r /var/run/docker.sock test ! -x /srv/directus-archive test -r /srv/codex-migration/source-20260710-175408/README.txt test ! -w /srv/codex-migration/source-20260710-175408/README.txt test -r /srv/codex-secrets/le-payload-migration.env test ! -w /srv/codex-secrets/le-payload-migration.env DOCKER_HOST=unix:///run/user/1200/docker.sock docker info --format '{{json .SecurityOptions}}' ``` The final line must show rootless security. The development context must resolve only to `/run/user/1200/docker.sock`; wrappers reject `default`, `/var/run/docker.sock`, TCP endpoints, SSH contexts, and any unknown endpoint. ## Production path and socket denial preflight Every stack/job wrapper will fail before Compose evaluation if: - effective UID is not the approved dedicated UID; - Docker endpoint is not exactly the approved rootless Unix socket; - Compose project name lacks the fixed nonproduction prefix; - any resolved bind source is `/var/www`, `/var/lib/docker`, `/var/lib/mysql`, `/srv/directus-archive`, `/var/run`, `/run/docker.sock`, or a descendant; - any non-finite application service receives `.migration.env`, `.migration-source`, a MariaDB source URL, or migration credentials; - destination database host/port is not the approved nonproduction target/service; - production-named secret scopes, SMTP hosts, OAuth variables, Ko-fi secrets, or webhook credentials appear in fixture/snapshot/rehearsal rendered Compose; - reset/purge targets a bind mount, external volume, unknown project label, or any path outside `/srv/le-payload-dev`. Tests render each Compose mode and inspect mounts, secrets, environment variable **names/classifications**, networks, contexts, labels, and volume ownership without printing values. ## Authoritative migration-source protection - The authoritative MariaDB remains read-only at the database privilege and filesystem layers. - `.migration-source` and `.migration.env` are mounted read-only only into finite `inventory`, `import`, and `validation` jobs. - Frontend, Payload, BGBye, proxy, Mailpit, fixture seed, package installation, build, lint, unit, integration, and ordinary browser-test services never receive those mounts or variables. - Legacy Directus restores the scrubbed SQL into a separate project-scoped writable MariaDB volume and copies uploads into `/srv/le-payload-dev/data/legacy-uploads` or a project volume. - Legacy Directus never mounts the source database storage or uploads, even read-only; the finite clone job is the only bridge. - Reset/purge operates only on labeled nonproduction destinations. It does not follow source symlinks and refuses all external/bind volumes. - `/srv/directus-archive` remains inaccessible and is never mentioned as a mount source in executable Compose configuration. ## Nonproduction storage plan The host currently has approximately 650 GiB available on the Btrfs filesystem containing `/srv` and `/home`. Proposed aggregate project quota: 250 GiB. | Path | Planning allowance | Ownership | Cleanup / backup | |---|---:|---|---| | `runtime/docker` | 80 GiB soft budget | `le_payload_dev` | prune only project/unused development images after digest recording; no backup | | `data/postgres` | 25 GiB | `le_payload_dev` / rootless mapped IDs | disposable; reset by labeled volume; no backup | | `data/media` | 15 GiB | fixed container UID mapped by rootless daemon | fixture disposable; snapshot copied data disposable; no authoritative backup role | | `data/legacy-uploads` | 10 GiB | rootless mapped service UID | copied from scrubbed source; disposable; never source of truth | | `data/bgbye-models` | 35 GiB | BGBye service/rootless mapping | checksummed cache; LRU/manual cleanup; redownloadable | | `data/tmp` | 10 GiB | service-specific mapped UID | tmpfs or age-based cleanup; no backup | | `artifacts/browser` | 5 GiB | `le_payload_dev` | TTL 7 days; snapshot artifacts treated as sensitive and never committed | | `artifacts/migration-reports` | 10 GiB | `le_payload_dev` | detailed reports TTL 30 days; committed summaries structural only | | workspace/metadata headroom | 10 GiB | `le_payload_dev` | Git remote is source backup; no personal exports committed | The listed soft budgets total 200 GiB. The 250 GiB hard qgroup ceiling reserves 50 GiB emergency headroom. Startup refuses when host free space is below 100 GiB, project usage exceeds 200 GiB, or required free space for a rehearsal cannot be reserved. The production filesystem is not used for spillover. ## Production ingress boundary Production will use an **internal application-gateway container behind TITANSERVER's existing shared host reverse proxy**. - The server-wide proxy remains host-managed, terminates public TLS, owns ports 80/443, ACME, and cross-application routing. - The repository-controlled gateway binds only to a dedicated host loopback port or private production network and routes frontend versus Payload internal services. - The host proxy forwards approved public hostnames to that one gateway and overwrites—not trusts from clients—the forwarding chain and scheme headers. - The application repository supplies a versioned host-proxy include/template and contract tests, but an operator explicitly reviews/renders/includes it. Containers never mount or edit host proxy configuration or certificates. - The gateway is not published directly to the internet. Payload admin exposure can be separately restricted at the host proxy. - Existing `forms.lasereverything.net` behavior remains under the shared ingress until cutover authorization. Local development uses its own gateway/TLS because it does not share production ingress. This design preserves server-wide ownership while keeping application routing rules testable and versioned. ## Local HTTPS and CA trust The local gateway uses a repository-pinned proxy image and an ignored, generated development CA to serve: - `app.le.localhost`; - `cms.le.localhost`; - `mail.le.localhost`; - optional `legacy.le.localhost`. All bind to loopback port 8443, avoiding privileged ports and host DNS changes (`*.localhost` resolves locally). Certificates and private keys live under `/srv/le-payload-dev/secrets/local-ca`, mode `0700/0600`, excluded from Git and build contexts. Automated Playwright tests may use a dedicated browser profile with its own trust database. Manual Chromium trust can be limited to `/home/le_payload_dev/.pki/nssdb` using the already installed `certutil`; system-wide CA trust is unnecessary. Importing the CA into even that user/browser profile is a host/user-state change and requires explicit approval. Removal deletes the certificate from that profile and the ignored CA directory. ## Mailpit and outbound protection Mailpit exists only in fixture, snapshot, rehearsal, and default staging modes: - internal Compose networks only; UI reaches the browser through the local gateway; - no public host bind and no production ingress route; - SMTP relay/forwarding and webhook forwarding disabled; - no production SMTP variables mounted; - tests query Mailpit's internal HTTP API and follow local HTTPS verification/recovery links; - registration, verification, recovery, supporter claim, and notification paths are covered. Defense in depth: 1. Mode-specific environment schema rejects production SMTP hostnames/sender domains, ports 25/465/587 as external endpoints, production credential variable scopes, OAuth secrets, Ko-fi production tokens, and webhook credentials. 2. Runtime services attach only to an `internal: true` application network in nonproduction. Mail is addressed to the internal `mailpit:1025` service. 3. Snapshot-derived recipient addresses are rejected or rewritten at the transport boundary to deterministic reserved-domain aliases. Only synthetic fixture accounts may be addressed directly. 4. No production credential exists in rendered Compose, container environment, secret mounts, or image history. 5. A finite `egress-test` runs from the exact frontend and Payload runtime network namespaces: Mailpit delivery must succeed; connection attempts to controlled external test endpoints on SMTP ports must fail; DNS/HTTP behavior is recorded separately. Firewall-level isolation will not be claimed until these tests pass under the selected rootless runtime. If Docker `internal` networking still permits an undesired host/external route, the implementation stops and proposes an approved rootless egress control rather than asserting safety. Tests use an administrator-approved controlled endpoint, never a real third-party SMTP server. Production remains unchanged: application services use the existing self-hosted SMTP infrastructure and real recipients through production-only secrets and network policy. Mailpit never replaces or modifies it. ## BGBye CPU/GPU profiles Milestone 0 starts with a deterministic CPU-compatible BGBye image/profile: - same FastAPI paths, methods endpoint shape, request fields, status codes, content types, health/readiness schema, and error contract as GPU; - one pinned CPU-capable segmentation model for real tiny-image processing in routine tests; - image upload/proxy limits, invalid method/file handling, alpha PNG output, concurrency serialization, temporary-file lifecycle, status polling, and FFmpeg contract can be exercised; - model artifacts are pinned/checksummed and readiness fails until loaded. CUDA-only validation is deferred to an opt-in GPU profile/staging gate: - NVIDIA runtime/device visibility and driver compatibility; - every production model/backend, CUDA placement, GPU lock/concurrency, memory cleanup/OOM behavior, performance envelope, and longer video processing; - production model-cache checksum and offline startup behavior. CPU and GPU images share server contract tests. Capability differences appear in explicit health/method metadata rather than changing endpoints. A production GPU image is released only after model preload/readiness, representative image/video output, cleanup, restart, and resource tests pass on GPU staging. Rootless GPU provisioning does not block the first stack. ## Rootless runtime rollback/removal Rollback must be run only after confirming the selected context/socket is the dedicated one: ```bash # As le_payload_dev: stop project resources, then the user daemon. DOCKER_HOST=unix:///run/user/1200/docker.sock docker compose \ --project-name le_payload_dev down --remove-orphans systemctl --user disable --now docker.socket docker.service # As administrator: disable optional lingering. sudo loginctl disable-linger le_payload_dev # Verify production daemon endpoint and data were never referenced by project metadata/logs. # Use the planning preflight/audit script; do not connect to or mutate production Docker. # Remove only the dedicated project subvolume after an explicit artifact review. sudo btrfs subvolume delete /srv/le-payload-dev # Remove subordinate allocations and account after the subvolume/runtime is gone. sudo usermod --del-subuids 165536-231071 --del-subgids 165536-231071 le_payload_dev sudo userdel --remove le_payload_dev sudo groupdel le_payload_dev 2>/dev/null || true ``` The dedicated Docker context lives only in the dedicated user's home and disappears with that home. If another operator created a client context pointing to the user socket, remove that context explicitly from that operator's client configuration. Production secret permission hardening is not rolled back merely because development is removed; it is an independent security correction. Source ACL entries can be removed with `setfacl -x u:le_payload_dev` before account deletion. Removal does not touch `/var/run/docker.sock`, production contexts, `/var/lib/docker`, `/var/lib/mysql`, `/var/www` application content, production volumes/networks, the frozen source, or `/srv/directus-archive`. Final proof compares before/after permission metadata and confirms: production socket remains denied, production service state is unchanged, no project mount referenced forbidden paths, the dedicated UID/subordinate mappings are gone, and only `/srv/le-payload-dev` resources were deleted.