carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# One isolated exec-backed instance (isolated-exec design §4.1). The
2# activator owns it exactly as it owned the exec child this replaces: lease,
3# restore, seed, replication, hibernation, control socket, logs. What
4# changed is that the process is a unit, still uid carlos, inside a mount
5# namespace in which nothing but the instance's own two directories exists,
6# on mounts where no symlink is ever followed. The instance name is the
7# escaped bounded identifier (%i); paths use the unescaped one (%I).
8[Unit]
9Description=carlos exec-backed instance %I (sidecar)
10# Stop-only coupling: when the edge stops (including the stop half of a
11# restart) this unit stops, and nothing starts it again but the agent.
12# PartOf= would propagate the RESTART and race the agent's own startup
13# (codex R1-7).
14BindsTo=carlos-edge.service
15Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
16After=carlos-edge.service data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
17
18[Service]
19Type=simple
20ExecStart=/opt/carlos/carlos instance-exec sidecar %I
21# Runs inside the same namespace, as carlos, after this invocation's
22# processes have been signalled: leaves an ADVISORY exit record in the
23# instance's own run directory (§6.5). Not "+": the "+" prefix drops User=
24# and the Private*/Protect* settings but keeps the explicit
25# TemporaryFileSystem=/BindPaths= mounts (codex R3-2), so nothing run from
26# here can see the host's /run/carlos anyway.
27ExecStopPost=/opt/carlos/carlos instance-exit sidecar %I
28User=carlos
29Group=carlos
30# One slice per host holds the instance, its seed and its sidecar, and
31# carries the tier (§4.3, §6.6). Slices referenced here are created on
32# demand; no per-host unit file exists.
33Slice=carlos-instances-%i.slice
34# The activator owns recycling: a crash stands the entry down and the next
35# request wakes it. systemd must not race that.
36Restart=no
37# SIGTERM to the app, SIGKILL to anything it left behind after
38# TimeoutStopSec; the activator's stop budget is 20s.
39KillMode=mixed
40TimeoutStopSec=20
41# The main process is the trampoline, which stays as the namespace's init
42# and runs the app as its child (#126): it forwards the SIGTERM and exits
43# 128+n when the app dies by signal n. These are SIGHUP, SIGINT, SIGPIPE
44# and SIGTERM, the deaths systemd counts as clean for a main process, so a
45# stop still leaves the unit inactive rather than failed.
46SuccessExitStatus=129 130 141 143
47# The log stream: stdout and stderr connect to the agent's per-role
48# listener (§6.4). PID 1 opens this as root, outside the namespace, on the
49# nosymfollow logs mount, at a path no tenant mount contains.
50StandardOutput=file:/run/carlos/logs/%I.sidecar.sock
51StandardError=inherit
52# The instance's own two directories, the only writable data paths.
53BindPaths=/data/instances/%I /run/carlos/instances/%I
54ReadWritePaths=/data/instances/%I /run/carlos/instances/%I
55# The working directory is the instance's own data directory, the one its
56# database is in (discussion #256 item D). A flat child ran with cwd /,
57# and an app writing relative paths (./data/blobs) wrote under /data;
58# without this line a unit's cwd is / on a read-only tmpfs and that app
59# exits at start with "read-only file system". systemd applies it after
60# the namespace is set up, so the bind above is what it chdirs into. Same
61# value in all three templates: the seed and the sidecar see the files
62# the instance made from the same place.
63WorkingDirectory=/data/instances/%I
64# Its own build, and no other app's (#214 item 2). The namespace block
65# empties /opt/carlos; these put back /opt/carlos/live/<host>, a link to
66# ../app, and /opt/carlos/app, which PID 1 binds by chasing the source
67# link to this host's live build at every start. The control service
68# writes both sources before it starts the unit.
69BindReadOnlyPaths=/run/carlos-control/exe/%I/app:/opt/carlos/app /run/carlos-control/exe/%I/live:/opt/carlos/live
70
71# --- the namespace (titogo@.service, 2026-07-31 design, on systemd 257) ---
72ProtectSystem=strict
73TemporaryFileSystem=/data:ro
74TemporaryFileSystem=/run/carlos:ro
75# Every other tree a carlos-uid unit writes on the box is hidden the same
76# way (codex R9-2): the restore drills' cache under /var/cache, and
77# /var/lib as a class. An instance needs nothing from either.
78TemporaryFileSystem=/var/cache:ro
79TemporaryFileSystem=/var/lib:ro
80# The version store holds every app's build on the box, world-readable so
81# a unit can exec its own (#214 item 2). An instance sees none of it: an
82# empty /opt/carlos with the platform binary that ExecStart= and
83# ExecStopPost= run put back, and, outside this block because it is per
84# instance, its own build at /opt/carlos/live/<host>.
85TemporaryFileSystem=/opt/carlos:ro
86BindReadOnlyPaths=/opt/carlos/carlos
87# Root-written credentials a carlos-GROUP process may read live here:
88# /etc/carlos/relay-masters is root:carlos 0640 for carlos-relay.service,
89# and an instance's Group=carlos would read it too (codex R10-1). An
90# instance gets its configuration through its launch file and needs
91# nothing from /etc/carlos. /run/carlos-control is the root control
92# service's socket directory (design §5.2): hiding it here too means a
93# connect from inside any instance's namespace is impossible outright,
94# not merely refused by the peer check on the far end (final review,
95# plan 2, item 3).
96InaccessiblePaths=/etc/carlos /run/carlos-control
97ProtectHome=yes
98PrivateTmp=yes
99PrivateDevices=yes
100# PrivateDevices= bind-mounts the HOST's /dev/shm into the private /dev,
101# and every instance is uid carlos, so a sibling's shared memory would be
102# one open away. Each unit gets its own, charged to its own cgroup.
103TemporaryFileSystem=/dev/shm:mode=1777,nosuid,nodev
104# POSIX message queues and SysV IPC belong to an IPC namespace, and every
105# instance is uid carlos, so a sibling's queue would be one mq_open away
106# (#178). PrivateIPC= gives each unit its own namespace, and mq_open()
107# goes through that namespace's own internal mount. PrivateDevices=
108# carries the host's /dev/mqueue into the private /dev the way it carries
109# /dev/shm, and that mount would still list and open the host namespace's
110# queues, so it is replaced the same way.
111PrivateIPC=yes
112TemporaryFileSystem=/dev/mqueue:ro
113# /sys/fs/cgroup read-only: the edge's delegated subtree is carlos-owned,
114# and its cgroup.kill / cgroup.freeze would otherwise be one write away
115# for a same-uid tenant (codex R9-1).
116ProtectControlGroups=yes
117ProtectProc=invisible
118ProcSubset=pid
119PrivatePIDs=yes
120RestrictNamespaces=yes
121NoNewPrivileges=yes
122CapabilityBoundingSet=
123RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
124# Abstract unix sockets live in the network namespace, which instances
125# share with the box because they need outbound TCP, and the line above
126# cannot tell them from path sockets (#178). The trampoline scopes them
127# with Landlock before it execs the app (instanceexec.ScopeAbstractUnix):
128# an instance can connect only to abstract sockets its own processes
129# bound. A sibling can still bind a name first; that is a refusal of
130# service, never a connection.
131#
132# Egress (#214 item 3): the internet, and nothing on the box or private.
133# Loopback is where the console and other tenants' addr upstreams listen;
134# RFC 1918, ULA, link-local and CGNAT are the VPC, other boxes and the
135# cloud's internal services. IMDS stays named so the property is
136# greppable. A cgroup filter: it drops in both directions for every
137# socket the unit opens. The one thing an instance needs from these
138# ranges is the box's DNS resolver, so the control service re-opens
139# exactly the nameservers the instance sees with IPAddressAllow= on
140# carlos-instances.slice (an allow wins, and a slice's lists reach every
141# unit below it). The probe checks both halves.
142IPAddressDeny=localhost multicast 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 169.254.0.0/16 169.254.169.254/32 172.16.0.0/12 192.0.0.0/24 192.168.0.0/16 198.18.0.0/15 240.0.0.0/4 64:ff9b::/96 fc00::/7 fd00:ec2::254/128 fe80::/10 fec0::/10
143# An allow opens an address both ways and on every port, so a loopback
144# resolver can never be opened: a tenant could bind systemd-resolved's
145# 127.0.0.53 itself and reach every listener bound to all addresses
146# through it. Where resolved runs, instances resolve through its
147# upstreams instead, and the control service opens those. The "-" makes
148# this a no-op on a box without resolved.
149BindReadOnlyPaths=-/run/systemd/resolve/resolv.conf:/etc/resolv.conf
150ProtectKernelTunables=yes
151ProtectKernelModules=yes
152ProtectKernelLogs=yes
153ProtectClock=yes
154ProtectHostname=yes
155RemoveIPC=yes
156LockPersonality=yes
157RestrictRealtime=yes
158# No RestrictSUIDSGID=: systemd cannot read the mode inside openat2's
159# open_how, so it refuses openat2 outright (ENOSYS), and every instancefs
160# open in here is an openat2 (#126, first roll; systemd 257). What it
161# guarded is covered: with no capabilities an instance can only make a
162# set-id file owned by carlos, which gains nothing for another carlos
163# process, NoNewPrivileges= voids it in here, and no other uid can reach
164# one (the tenant trees are nosuid, PrivateTmp's sit under a root 0700
165# directory).
166MemoryDenyWriteExecute=yes
167SystemCallArchitectures=native
168
169# --- per-service resources; the tier lives on the host slice (§4.3) ---
170TasksMax=256
171# The kernel picks a tenant before it picks anything else in carlos.slice.
172OOMScoreAdjust=500
173# One OOM kill takes every process of THIS service (systemd sets
174# memory.oom.group on the service cgroup for OOMPolicy=kill). The sidecar
175# is a sibling service with its own family rule (§4.3).
176OOMPolicy=kill
177