# The single-box role: `carlos agent` = edge (TLS terminator + host router) + # hibernation activator + hourly release adoption in one process. Ported from # Eleven's deploy/systemd/lchat-router.service, renamed, same hardening. # Hibernating instances run as THIS unit's children (the activator); systemd- # managed instances get their own units. [Unit] Description=carlos edge (TLS terminator, host router, activator, release adoption) # The three nosymfollow mounts of the isolated-exec design (§3.2) are hard # requirements: an edge that starts without them would start unprotected, # so it does not start at all. Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount After=network-online.target data-instances.mount run-carlos-instances.mount run-carlos-logs.mount Wants=network-online.target [Service] # Optional host-wide config (0600, root-owned): CARLOS_HIBERNATE_*, # CARLOS_DEPLOYMENT_BUCKET/_DIR, CARLOS_RELEASE_PUBKEY, CARLOS_RESERVED_SLUGS... EnvironmentFile=-/etc/carlos/host.env ExecStart=/opt/carlos/carlos agent # `carlos add` writes are picked up by the registry fingerprint watcher within # seconds; SIGHUP is the operator's explicit "reload now". ExecReload=/bin/kill -HUP $MAINPID Restart=always RestartSec=1 # --- hardening (ported from lchat-router.service; see Eleven's # docs/instance-router.md § Hardening) --- User=carlos Group=carlos # The edge is the ONLY unit that binds privileged ports (:80 + :443); this cap # lets it do so as a non-root user. Instances bind unix sockets only. AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/data /run/carlos PrivateTmp=yes PrivateDevices=yes ProtectClock=yes ProtectHostname=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes # ProtectControlGroups is deliberately ABSENT: it would mount /sys/fs/cgroup # read-only, and Delegate= (below) needs this unit's own subtree writable. # The hardening that line bought is retained by ownership instead — plain # DAC keeps everything OUTSIDE the chowned subtree read-only to this # unprivileged user. ProtectProc=invisible RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=yes RestrictRealtime=yes # No RestrictSUIDSGID=: systemd cannot read the mode inside openat2's # open_how, so it refuses openat2 outright (ENOSYS), and the agent's every # open under an instance directory, and every rename of the layout pass, # is an openat2 (isolated-exec design R15; the templates lost the line # first and this unit was found still carrying it, #126). R15's reasoning # for the templates carries here, and the one capability this unit holds # does not change it: a set-id file is a threat through CAP_CHOWN, # CAP_FSETID or CAP_SETUID, none of which is in the bounding set, so # anything the agent could mark set-id is owned by carlos and gains carlos # only to a process that is not already carlos and is not under # no_new_privs. The agent and its flat children run under # NoNewPrivileges=; the nested templates hold no capabilities and see only # their own directories; /run is nosuid, and the instance trees under # /data are mounted nosuid. The agent passes no set-id bit anywhere # (instancefs strips modes to their permission bits), so what is left is a # compromised agent handing its own uid to a lower-privileged unit, which # is no gain for an attacker that already has it. The agent checks openat2 # in its own unit at start (instancefs.SelfCheck) and refuses nested wakes # and the layout pass while the call is blocked, so this line coming back # is loud, not a half-migrated host. LockPersonality=yes # Pure Go — the binary never maps writable+executable memory. MemoryDenyWriteExecute=yes SystemCallFilter=@system-service SystemCallErrorNumber=EPERM SystemCallArchitectures=native # The outer wall (memory-pressure spec 2026-08-28 §2.2) used to be two # MemoryHigh/MemoryMax lines here, bounding the agent plus its exec # children. Instances are sibling units now (isolated-exec design §4.3), so # the wall is carlos.slice's, which holds this unit and every instance and # bounds exactly what it bounded before. The oom_score_adj story is # unchanged: tenants carry OOMScoreAdjust=500 in their template. Slice=carlos.slice # systemd's default OOMPolicy=stop STOPS the whole service when the kernel # OOM-kills ANY process in the unit's cgroup tree (memory.events is # hierarchical, delegation or not). Restart=always would bring the edge # back a second later and cycle every awake instance on the box. With # continue, one tenant's kill is one tenant's kill; the activator recycles # it on its next request. OOMPolicy=continue TasksMax=1024 # Exec-backed instances run as this unit's children. Delegation hands the # unit its own cgroup subtree — chowned to User=carlos, unmanaged by systemd # below this point — so the agent can give each child its own cgroup for # per-instance accounting (docs/superpowers/specs/ # 2026-08-07-exec-child-accounting-design.md). The subtree still dies with # the service: restart semantics are unchanged. Delegate=cpu memory io pids [Install] WantedBy=multi-user.target