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