| 1 | # The single-box role: `carlos agent` = edge (TLS terminator + host router) + |
| 2 | # hibernation activator + hourly release adoption in one process. Ported from |
| 3 | # Eleven's deploy/systemd/lchat-router.service, renamed, same hardening. |
| 4 | # Hibernating instances run as THIS unit's children (the activator); systemd- |
| 5 | # managed instances get their own units. |
| 6 | [Unit] |
| 7 | Description=carlos edge (TLS terminator, host router, activator, release adoption) |
| 8 | # The three nosymfollow mounts of the isolated-exec design (§3.2) are hard |
| 9 | # requirements: an edge that starts without them would start unprotected, |
| 10 | # so it does not start at all. |
| 11 | Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount |
| 12 | After=network-online.target data-instances.mount run-carlos-instances.mount run-carlos-logs.mount |
| 13 | Wants=network-online.target |
| 14 | |
| 15 | [Service] |
| 16 | # Optional host-wide config (0600, root-owned): CARLOS_HIBERNATE_*, |
| 17 | # CARLOS_DEPLOYMENT_BUCKET/_DIR, CARLOS_RELEASE_PUBKEY, CARLOS_RESERVED_SLUGS... |
| 18 | EnvironmentFile=-/etc/carlos/host.env |
| 19 | ExecStart=/opt/carlos/carlos agent |
| 20 | # `carlos add` writes are picked up by the registry fingerprint watcher within |
| 21 | # seconds; SIGHUP is the operator's explicit "reload now". |
| 22 | ExecReload=/bin/kill -HUP $MAINPID |
| 23 | Restart=always |
| 24 | RestartSec=1 |
| 25 | |
| 26 | # --- hardening (ported from lchat-router.service; see Eleven's |
| 27 | # docs/instance-router.md § Hardening) --- |
| 28 | User=carlos |
| 29 | Group=carlos |
| 30 | # The edge is the ONLY unit that binds privileged ports (:80 + :443); this cap |
| 31 | # lets it do so as a non-root user. Instances bind unix sockets only. |
| 32 | AmbientCapabilities=CAP_NET_BIND_SERVICE |
| 33 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE |
| 34 | NoNewPrivileges=yes |
| 35 | ProtectSystem=strict |
| 36 | ProtectHome=yes |
| 37 | ReadWritePaths=/data /run/carlos |
| 38 | PrivateTmp=yes |
| 39 | PrivateDevices=yes |
| 40 | ProtectClock=yes |
| 41 | ProtectHostname=yes |
| 42 | ProtectKernelTunables=yes |
| 43 | ProtectKernelModules=yes |
| 44 | ProtectKernelLogs=yes |
| 45 | # ProtectControlGroups is deliberately ABSENT: it would mount /sys/fs/cgroup |
| 46 | # read-only, and Delegate= (below) needs this unit's own subtree writable. |
| 47 | # The hardening that line bought is retained by ownership instead — plain |
| 48 | # DAC keeps everything OUTSIDE the chowned subtree read-only to this |
| 49 | # unprivileged user. |
| 50 | ProtectProc=invisible |
| 51 | RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX |
| 52 | RestrictNamespaces=yes |
| 53 | RestrictRealtime=yes |
| 54 | # No RestrictSUIDSGID=: systemd cannot read the mode inside openat2's |
| 55 | # open_how, so it refuses openat2 outright (ENOSYS), and the agent's every |
| 56 | # open under an instance directory, and every rename of the layout pass, |
| 57 | # is an openat2 (isolated-exec design R15; the templates lost the line |
| 58 | # first and this unit was found still carrying it, #126). R15's reasoning |
| 59 | # for the templates carries here, and the one capability this unit holds |
| 60 | # does not change it: a set-id file is a threat through CAP_CHOWN, |
| 61 | # CAP_FSETID or CAP_SETUID, none of which is in the bounding set, so |
| 62 | # anything the agent could mark set-id is owned by carlos and gains carlos |
| 63 | # only to a process that is not already carlos and is not under |
| 64 | # no_new_privs. The agent and its flat children run under |
| 65 | # NoNewPrivileges=; the nested templates hold no capabilities and see only |
| 66 | # their own directories; /run is nosuid, and the instance trees under |
| 67 | # /data are mounted nosuid. The agent passes no set-id bit anywhere |
| 68 | # (instancefs strips modes to their permission bits), so what is left is a |
| 69 | # compromised agent handing its own uid to a lower-privileged unit, which |
| 70 | # is no gain for an attacker that already has it. The agent checks openat2 |
| 71 | # in its own unit at start (instancefs.SelfCheck) and refuses nested wakes |
| 72 | # and the layout pass while the call is blocked, so this line coming back |
| 73 | # is loud, not a half-migrated host. |
| 74 | LockPersonality=yes |
| 75 | # Pure Go — the binary never maps writable+executable memory. |
| 76 | MemoryDenyWriteExecute=yes |
| 77 | SystemCallFilter=@system-service |
| 78 | SystemCallErrorNumber=EPERM |
| 79 | SystemCallArchitectures=native |
| 80 | # The outer wall (memory-pressure spec 2026-08-28 §2.2) used to be two |
| 81 | # MemoryHigh/MemoryMax lines here, bounding the agent plus its exec |
| 82 | # children. Instances are sibling units now (isolated-exec design §4.3), so |
| 83 | # the wall is carlos.slice's, which holds this unit and every instance and |
| 84 | # bounds exactly what it bounded before. The oom_score_adj story is |
| 85 | # unchanged: tenants carry OOMScoreAdjust=500 in their template. |
| 86 | Slice=carlos.slice |
| 87 | # systemd's default OOMPolicy=stop STOPS the whole service when the kernel |
| 88 | # OOM-kills ANY process in the unit's cgroup tree (memory.events is |
| 89 | # hierarchical, delegation or not). Restart=always would bring the edge |
| 90 | # back a second later and cycle every awake instance on the box. With |
| 91 | # continue, one tenant's kill is one tenant's kill; the activator recycles |
| 92 | # it on its next request. |
| 93 | OOMPolicy=continue |
| 94 | TasksMax=1024 |
| 95 | # Exec-backed instances run as this unit's children. Delegation hands the |
| 96 | # unit its own cgroup subtree — chowned to User=carlos, unmanaged by systemd |
| 97 | # below this point — so the agent can give each child its own cgroup for |
| 98 | # per-instance accounting (docs/superpowers/specs/ |
| 99 | # 2026-08-07-exec-child-accounting-design.md). The subtree still dies with |
| 100 | # the service: restart semantics are unchanged. |
| 101 | Delegate=cpu memory io pids |
| 102 | |
| 103 | [Install] |
| 104 | WantedBy=multi-user.target |
| 105 | |