19 KiB
Milestone 0 host runtime and security plan
Status: Phase 2A completed and verified, 2026-07-12; Phase 2B is review-only. Nothing in this document authorizes Phase 2B execution. Phase 1 created only the inert le_app_codex identity and directory skeleton. Phase 2B systemd changes, namespace creation, firewall changes, user-manager startup, Docker startup, Codex authentication, repository/source copying, and production changes remain separately approval-gated.
This document supersedes the earlier runtime proposal. In particular, the project will not use a system service running dockerd with User=le_app_codex, will not enable lingering, and will not install the unmodified AUR docker-rootless-extras package.
Fixed production and trust boundaries
- Production remains rooted at
/var/www/lasereverything.net.db. - The complete archive remains root-only at
/srv/directus-archive/20260710-175408and is never moved, mounted, linked, copied, or exposed to Codex. - The contained nonproduction root is
/srv/le-app-codex. - Transitional
/srv/codex-work,/srv/codex-migration, and/srv/codex-secretsresources remain outside the contained environment until an independently approved copy-and-verify stage. - The rootful Docker and containerd sockets and data directories are inaccessible to the contained environment.
- The only accepted project daemon endpoint is
unix:///run/user/1200/docker.sock. - No Docker TCP/SSH endpoint, production context, shared Docker group, socket forwarding, or fallback to
/run/docker.sockor/var/run/docker.sockis accepted.
Confirmed Phase 1 state
Phase 1 created:
- user/group
le_app_codex, UID/GID1200, primary group only; - locked password,
/usr/bin/nologin, and no supplementary groups; - subordinate UID/GID allocation
165536:65536; - inert workspace skeleton under
/srv/le-app-codexwith root-owned boundaries and dedicated-user mutable children.
Phase 1 did not install packages, start user@1200.service, start a daemon or container, create a network namespace, change nftables, authenticate Codex, copy a repository or migration source, or touch production. Lingering was not enabled and remains prohibited. Do not rerun any Phase 1 artifact.
Correct runtime lifecycle
Docker is a genuine per-user service defined below /srv/le-app-codex/home/.config/systemd/user. Its lifetime is managed by the systemd user manager for UID 1200.
Root enforces containment around the complete user manager with instance-specific configuration for:
/etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf
/etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf
The user manager depends on the root-created network namespace and enters that namespace before any user unit starts. Consequently Codex, RootlessKit, dockerd, containerd, build processes, test jobs, and containers inherit the same root-enforced filesystem, network, device, and cgroup boundary.
Root starts and stops user@1200.service explicitly after inert acceptance tests pass. Lingering is prohibited and must remain disabled. The Docker user unit may be enabled for the user manager's controlled lifetime, but must not cause the manager to start independently.
A system unit with User=le_app_codex must not run dockerd. That model bypasses the supported per-user lifecycle and does not provide the required containment of Codex and every sibling user process.
Completed Phase 2A and pinned launcher
Phase 2A host maintenance is complete and supersedes the provisional package-maintenance gate. The host booted successfully on 7.1.3-zen1-2-zen; both standard and Zen 7.1.3 kernels and their NVIDIA DKMS modules are installed. rootlesskit 3.0.1-1, slirp4netns 1.3.4-1, libslirp 4.9.3-1, and openai-codex 0.144.1-2 are installed; /usr/bin/codex reports codex-cli 0.144.1.
Post-maintenance verification also established: Laser Everything HTTP 200; NoMachine removed; stale 6.19 DKMS state removed; Snapper repaired with valid Pacman pre/post snapshots; protected Directus archive checksum valid; retired Directus stopped with restart policy no; /root/server-compose-runner.sh no longer references lasereverything.net.forms; and no unexpected failed system units. The retained review/evidence package is under phase2a-host-maintenance/.
Do not install the unmodified AUR docker-rootless-extras package. Its install hook reloads global sysctls, which exceeds this project's boundary. Do not curl an installer into a shell.
Install only a reviewed Moby contrib/dockerd-rootless.sh matching the Docker engine installed by the completed Phase 2A maintenance. Docker is 29.6.1; the reviewed artifact is:
Moby tag: docker-v29.6.1
Source: contrib/dockerd-rootless.sh
Target: /usr/local/libexec/dockerd-rootless-29.6.1.sh
SHA-256: 904c9b9e35f6927c0a5e65afb4d35b6bc9eb1278c878044501281fc728c9be46
Owner/mode: root:root 0755
Docker was revalidated as Arch package 1:29.6.1-1, client 29.6.1 build 8900f1d330, daemon binary 29.6.1 build 8ec5ab355a. The upstream tag, source, and checksum were re-fetched and revalidated on 2026-07-12. Revalidate all four again immediately before installation. If Docker changes, this filename and checksum are invalid until a matching launcher is reviewed. A clean-chroot package containing only the pinned launcher is acceptable; it must have no global sysctl hook, setup tool, service enablement, or post-install execution.
User-manager filesystem and device containment
The proposed, not deployed, containment contract is:
# /etc/systemd/system/user@1200.service.d/50-le-app-codex-containment.conf
[Unit]
Requires=le-app-codex-netns.service
After=le-app-codex-netns.service
[Service]
NetworkNamespacePath=/run/netns/le-app-codex
ProtectSystem=strict
PrivateTmp=yes
TemporaryFileSystem=/srv:ro
BindPaths=/srv/le-app-codex:/srv/le-app-codex
TemporaryFileSystem=/var:ro
TemporaryFileSystem=/mnt:ro
TemporaryFileSystem=/media:ro
TemporaryFileSystem=/opt:ro
InaccessiblePaths=/var/www
InaccessiblePaths=/root
InaccessiblePaths=/home
InaccessiblePaths=/boot
InaccessiblePaths=/efi
InaccessiblePaths=/run/docker.sock
InaccessiblePaths=/var/run/docker.sock
InaccessiblePaths=/run/containerd/containerd.sock
InaccessiblePaths=/run/podman
InaccessiblePaths=/run/dbus/system_bus_socket
InaccessiblePaths=/run/systemd/private
InaccessiblePaths=/var/lib/docker
InaccessiblePaths=/var/lib/containerd
InaccessiblePaths=/var/lib/containers
InaccessiblePaths=/etc/docker
InaccessiblePaths=/etc/containers
InaccessiblePaths=/etc/ssh
InaccessiblePaths=/etc/NetworkManager/system-connections
InaccessiblePaths=/etc/wireguard
DevicePolicy=closed
DeviceAllow=/dev/fuse rw
DeviceAllow=/dev/net/tun rw
Expected visibility is read-only runtime content under /usr, /bin, and /lib*; selected nonsensitive read-only /etc; normal /proc pending acceptance tests; read-only /sys; a closed device policy with FUSE/TUN; /run/user/1200; a private /srv exposing only /srv/le-app-codex; empty private /var, /mnt, /media, and /opt; and private /tmp.
The following are intentionally deferred because they may break subordinate-ID mapping, user-manager behavior, cgroup delegation, or container creation:
NoNewPrivileges=yes
RestrictSUIDSGID=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
ProcSubset=pid
ProtectProc=invisible and hostname isolation remain acceptance-test candidates. Compatibility must not be assumed; they may remain only after the user manager, RootlessKit, Docker, Codex, and test containers pass with them. The final contract must prevent unrelated host-process inspection without breaking the supported runtime.
Resource boundary
The proposed, not deployed, aggregate boundary for the user manager and everything below it is:
# /etc/systemd/system/user-1200.slice.d/50-le-app-codex-resources.conf
[Slice]
CPUQuota=600%
MemoryHigh=48G
MemoryMax=64G
MemorySwapMax=16G
TasksMax=4096
These limits cover Codex, RootlessKit, dockerd, containerd, builds, jobs, and all containers. Validate generated properties and cgroup delegation before relying on the limits.
Hard disk-growth enforcement is deferred and is not a Phase 2B installation blocker. Btrfs quotas remain intentionally disabled. The repaired /.snapshots Btrfs subvolume and successful Snapper snapshots 1, 2, and 3 establish snapshot operation, not quota accounting. Do not enable quotas, run snapper setup-quota, trigger a rescan, or impose the earlier proposed 250 GiB qgroup limit incidentally. Mandatory filesystem visibility and resource controls constrain access and CPU/memory/tasks but do not cap growth within /srv/le-app-codex and are not a hard storage quota.
Soft planning budgets remain useful for monitoring: 80 GiB container runtime, 25 GiB PostgreSQL, 15 GiB media, 10 GiB legacy uploads, 35 GiB BGBye models, 10 GiB temporary data, 5 GiB browser artifacts, 10 GiB migration reports, and 10 GiB checkout/home metadata. Startup should fail when host/project free-space thresholds are unsafe, but soft checks do not replace a hard quota.
Rootless Docker user service
The proposed, not deployed, user unit is:
# /srv/le-app-codex/home/.config/systemd/user/docker.service
[Unit]
Description=Laser Everything rootless Docker
Documentation=https://docs.docker.com/engine/security/rootless/
[Service]
Type=notify
NotifyAccess=all
KillMode=mixed
Environment=HOME=/srv/le-app-codex/home
Environment=XDG_RUNTIME_DIR=/run/user/1200
Environment=DOCKER_HOST=unix:///run/user/1200/docker.sock
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=/run/user/1200/dockerd-rootless
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=builtin
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=true
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=true
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=true
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=false
ExecStart=/usr/local/libexec/dockerd-rootless-29.6.1.sh
ExecReload=/usr/bin/kill -s HUP $MAINPID
Restart=always
RestartSec=2
TimeoutStartSec=0
TimeoutStopSec=120
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
Delegate=yes
[Install]
WantedBy=default.target
The ExecStart path must be updated if the installed Docker release changes. Proposed daemon configuration at /srv/le-app-codex/home/.config/docker/daemon.json:
{
"data-root": "/srv/le-app-codex/container-runtime/docker",
"storage-driver": "fuse-overlayfs",
"live-restore": false,
"userland-proxy": false,
"ipv6": false,
"ip6tables": false,
"log-driver": "local",
"log-opts": {
"max-size": "20m",
"max-file": "5"
}
}
Docker client configuration lives only below /srv/le-app-codex/home/.docker. The named le-app-codex context must resolve exactly to unix:///run/user/1200/docker.sock. Wrappers reject the default context, rootful sockets, TCP/SSH endpoints, and unknown contexts.
IPv4-only network containment
Root creates and owns the persistent namespace /run/netns/le-app-codex. The complete user@1200.service enters it through NetworkNamespacePath; the user cannot replace the namespace or firewall policy.
Current read-only findings from the checksummed root inventory:
- the operator approved Quad9 IPv4
9.9.9.9and149.112.112.112; - the host resolver includes
192.168.10.1, but it remains denied and no LAN resolver exception is proposed; - no persistent
le-app-codexnetwork namespace currently exists; - no project-specific nftables rules are deployed;
- observed host public/NAT IPv4 is
74.67.173.56, with host/LAN/WireGuard addresses192.168.10.151,192.168.10.0/24,10.98.0.2, and10.98.0.0/24; - all 41 Docker networks and their IPv4/IPv6 IPAM ranges are retained in the Phase 2B inventory and covered by the private/IPv6 denials.
All host addresses, routes, LAN/VPN/WireGuard/management networks, public/NAT addresses, Docker bridges, resolver endpoints, nftables rules, ACLs, and mounts were inventoried by root on 2026-07-12 at 20:24 EDT. The complete checksummed record is retained with the Phase 2B artifacts. The preflight dynamically repeats this inventory immediately before installation and hard-stops on drift.
The namespace is IPv4-only: assign no IPv6 address or route and reject IPv6 on the project veth. Do not change host-global IPv6 or sysctls.
The nftables design is default-deny and must cover namespace egress and host-to-namespace ingress in FORWARD, veth-gateway traffic in host INPUT, established/related replies, UDP and TCP DNS, and RootlessKit port-publication attempts. Initially no inbound forwarding to the namespace is allowed; a container requesting -p must remain unreachable from the host, LAN, VPN, and public networks.
Permit only:
- established/related reply traffic;
- UDP/TCP DNS to explicitly approved resolver IPv4 addresses;
- TCP 80/443 to public destinations after exclusions.
Explicitly reject:
- namespace and host loopback ranges;
- host veth gateway except a separately approved DNS/proxy endpoint;
- all host addresses, LAN, management, VPN, and WireGuard addresses/subnets;
- every current and later-inventoried Docker/Podman/container bridge range;
- RFC1918 private ranges, carrier-grade NAT, link-local, metadata including
169.254.169.254, multicast, benchmark, documentation, reserved, and other non-public ranges; - every TITANSERVER public/NAT address supplied by the operator;
- TCP 25, 465, and 587;
- all IPv6 traffic.
Explicit coverage includes 192.168.10.151, 10.98.0.2, 74.67.173.56, and 2603:7080:7500:1279:75aa:d64d:4cf0:8f10, plus the inventoried network ranges. Public/NAT address drift is a fail-closed configuration-review condition before later namespace restarts or policy regeneration. Forge SSH is outside the sandbox; no TCP/22 or LAN exception is proposed.
Acceptance gates and order of operations
Every stage stops for review. No real migration data, nonproduction credentials, or Codex authentication enters the boundary until inert tests pass.
- Verify the completed Phase 2A state and do not rerun its package transaction.
- If a full host upgrade was selected, reboot and complete production health verification.
- Install only the version-matched, checksum-pinned Moby launcher.
- Install the resource slice and user-manager containment files without starting
user@1200.service. - Create the namespace without Docker; inventory generated properties, mounts, routes, addresses, and namespace identity.
- Install the reviewed nftables fragment without Docker and test positive DNS/HTTPS plus every negative route class.
- Run inert one-shot processes through the proposed user-manager namespace to validate filesystem,
/proc, devices, cgroups, symlink/bind-mount resistance, and network denial. - Only after the inert gates pass, root explicitly starts
user@1200.service. - Install/start the Docker user service and validate the exact socket, rootless security, data root, storage driver, logs, resource accounting, and port-publication denial.
- Run a disposable container through the same positive and negative tests, then tear it down.
- Only after runtime acceptance, choose and perform Codex authentication.
- Repository cloning, scrubbed-source copying, new secret generation, and Milestone 0 implementation are later separately reviewed stages.
Positive tests must prove that the user manager, Codex process, daemon, and disposable container can write only approved paths below /srv/le-app-codex, use only /run/user/1200/docker.sock, store Docker data only below container-runtime/docker, resolve through approved DNS, and reach approved public HTTPS endpoints.
Negative tests must prove they cannot list/read /var/www, /root, real /home, /srv/directus-archive, or transitional /srv/codex-*; open rootful runtime sockets or data; reach host/LAN/WireGuard/private/link-local/metadata/bridge/reserved destinations; use direct SMTP or IPv6; publish a port to the host/LAN; bypass controls via symlinks or bind mounts; inspect unrelated host processes; or escape resource limits.
Failure of any negative test is a hard stop, not a reason to weaken containment ad hoc.
Codex authentication decision
Authentication remains unresolved. The choices are ChatGPT device authentication or a dedicated API key created specifically for le_app_codex. Do not copy Codex configuration, auth.json, tokens, SSH keys, Docker configuration, or shell state from sol6_vi.
Use CODEX_HOME=/srv/le-app-codex/home/.codex. Authentication may occur only after the contained user manager, filesystem namespace, network namespace, firewall, resource slice, Codex binary, and acceptance tests are approved. Any auth.json must be a nonsymlink regular file owned by 1200:1200, mode 0600, and treated as a password.
Repository and migration-source admission
After runtime acceptance and separate approval:
- create a clean checkout of the reviewed remote
migration/le-app-databasebranch at/srv/le-app-codex/lasereverything.net.db; - do not copy
sol6_viSSH credentials; the current network design grants the contained environment no Forge SSH exception; - copy only the credential-scrubbed authoritative source into
database-source-readonlythrough an administrator-run, checksum-verified process; - never use the root archive as migration input;
- generate new nonproduction credentials below
nonproduction-secrets; never copy production or root-only administration credentials.
Transitional checkouts, databases, secrets, volumes, and source paths remain intact for at least 30 days and two successful rehearsals. Every later deletion requires separate approval.
Mail and production promotion boundaries
Mailpit is nonproduction-only, internal to Compose, has no relay/forwarding, and receives only synthetic or rewritten recipients. Namespace policy rejects direct SMTP on TCP 25/465/587. Production continues to use the existing SMTP service through production-only configuration.
Promotion is commit/release based:
/srv/le-app-codex/lasereverything.net.db
-> reviewed pushed commit and immutable image/release manifest
-> fixture -> snapshot -> rehearsal -> staging
-> clean checkout or verified release artifact
-> /var/www/lasereverything.net.db
Never recursively copy or rsync the nonproduction workspace into production. Codex stops for explicit approval before changing /var/www, public proxy/DNS/TLS, production SMTP, production databases, Directus, or transitional/archive resources.
Unresolved decisions checklist
- Codex authentication: device authentication or dedicated API key.
- Disk-growth enforcement: Btrfs quota policy or another separately approved mechanism.
- Whether deferred process/hostname hardening options pass acceptance tests.
- Any individually approved future host-to-namespace published endpoint; none is currently allowed.
Until these decisions are resolved, do not proceed beyond documentation and read-only verification.