carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1# One generation of an isolated exec-backed instance (zero-downtime
2# deploys design §7.6). The instance string is "<instance-id>:<generation>"
3# (%I), and the trampoline and the exit writer split it. systemd has no
4# specifier for part of an instance string, and ':' is BindPaths='s field
5# separator, so everything keyed by the host ID alone (the slice, the data
6# mounts) or by the generation (the executable mount, the log socket)
7# comes from a runtime drop-in the control service writes before each
8# start. Without that drop-in this unit has an empty /data and its
9# trampoline refuses, and the control service will not start it unless
10# systemd reports the drop-in's slice. carlos-instance@.service stays for
11# every box and binary that still runs it.
12[Unit]
13Description=carlos exec-backed instance generation %I
14# Stop-only coupling, as carlos-instance@.service (codex R1-7).
15BindsTo=carlos-edge.service
16Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
17After=carlos-edge.service data-instances.mount run-carlos-instances.mount run-carlos-logs.mount
18
19[Service]
20Type=simple
21ExecStart=/opt/carlos/carlos instance-exec run %I
22# The tenant runs in its own data directory (#256 D). %I here is
23# <id>:<n>, not the host's id, so the real directory comes from the
24# generation's runtime drop-in, which overrides this line. This one only
25# applies if the drop-in did not, and then the start is already refused
26# (the control service checks the drop-in's slice first), and /data is an
27# empty tmpfs, so the chdir fails closed.
28WorkingDirectory=/data/instances
29ExecStopPost=/opt/carlos/carlos instance-exit run %I
30User=carlos
31Group=carlos
32# The activator owns recycling; systemd must not race it.
33Restart=no
34KillMode=mixed
35TimeoutStopSec=20
36# The main process is the trampoline, which stays as the namespace's init
37# and runs the app as its child (#126): it forwards the SIGTERM and exits
38# 128+n when the app dies by signal n. These are SIGHUP, SIGINT, SIGPIPE
39# and SIGTERM, the deaths systemd counts as clean for a main process, so a
40# stop still leaves the unit inactive rather than failed.
41SuccessExitStatus=129 130 141 143
42StandardError=inherit
43
44# --- the namespace (titogo@.service, 2026-07-31 design, on systemd 257) ---
45ProtectSystem=strict
46TemporaryFileSystem=/data:ro
47TemporaryFileSystem=/run/carlos:ro
48# Every other tree a carlos-uid unit writes on the box is hidden the same
49# way (codex R9-2): the restore drills' cache under /var/cache, and
50# /var/lib as a class. An instance needs nothing from either.
51TemporaryFileSystem=/var/cache:ro
52TemporaryFileSystem=/var/lib:ro
53# The version store holds every app's build on the box, world-readable so
54# a unit can exec its own (#214 item 2). An instance sees none of it: an
55# empty /opt/carlos with the platform binary that ExecStart= and
56# ExecStopPost= run put back, and, outside this block because it is per
57# instance, its own build at /opt/carlos/live/<host>.
58TemporaryFileSystem=/opt/carlos:ro
59BindReadOnlyPaths=/opt/carlos/carlos
60# Root-written credentials a carlos-GROUP process may read live here:
61# /etc/carlos/relay-masters is root:carlos 0640 for carlos-relay.service,
62# and an instance's Group=carlos would read it too (codex R10-1). An
63# instance gets its configuration through its launch file and needs
64# nothing from /etc/carlos. /run/carlos-control is the root control
65# service's socket directory (design §5.2): hiding it here too means a
66# connect from inside any instance's namespace is impossible outright,
67# not merely refused by the peer check on the far end (final review,
68# plan 2, item 3).
69InaccessiblePaths=/etc/carlos /run/carlos-control
70ProtectHome=yes
71PrivateTmp=yes
72PrivateDevices=yes
73# PrivateDevices= bind-mounts the HOST's /dev/shm into the private /dev,
74# and every instance is uid carlos, so a sibling's shared memory would be
75# one open away. Each unit gets its own, charged to its own cgroup.
76TemporaryFileSystem=/dev/shm:mode=1777,nosuid,nodev
77# POSIX message queues and SysV IPC belong to an IPC namespace, and every
78# instance is uid carlos, so a sibling's queue would be one mq_open away
79# (#178). PrivateIPC= gives each unit its own namespace, and mq_open()
80# goes through that namespace's own internal mount. PrivateDevices=
81# carries the host's /dev/mqueue into the private /dev the way it carries
82# /dev/shm, and that mount would still list and open the host namespace's
83# queues, so it is replaced the same way.
84PrivateIPC=yes
85TemporaryFileSystem=/dev/mqueue:ro
86# /sys/fs/cgroup read-only: the edge's delegated subtree is carlos-owned,
87# and its cgroup.kill / cgroup.freeze would otherwise be one write away
88# for a same-uid tenant (codex R9-1).
89ProtectControlGroups=yes
90ProtectProc=invisible
91ProcSubset=pid
92PrivatePIDs=yes
93RestrictNamespaces=yes
94NoNewPrivileges=yes
95CapabilityBoundingSet=
96RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
97# Abstract unix sockets live in the network namespace, which instances
98# share with the box because they need outbound TCP, and the line above
99# cannot tell them from path sockets (#178). The trampoline scopes them
100# with Landlock before it execs the app (instanceexec.ScopeAbstractUnix):
101# an instance can connect only to abstract sockets its own processes
102# bound. A sibling can still bind a name first; that is a refusal of
103# service, never a connection.
104#
105# Egress (#214 item 3): the internet, and nothing on the box or private.
106# Loopback is where the console and other tenants' addr upstreams listen;
107# RFC 1918, ULA, link-local and CGNAT are the VPC, other boxes and the
108# cloud's internal services. IMDS stays named so the property is
109# greppable. A cgroup filter: it drops in both directions for every
110# socket the unit opens. The one thing an instance needs from these
111# ranges is the box's DNS resolver, so the control service re-opens
112# exactly the nameservers the instance sees with IPAddressAllow= on
113# carlos-instances.slice (an allow wins, and a slice's lists reach every
114# unit below it). The probe checks both halves.
115IPAddressDeny=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
116# An allow opens an address both ways and on every port, so a loopback
117# resolver can never be opened: a tenant could bind systemd-resolved's
118# 127.0.0.53 itself and reach every listener bound to all addresses
119# through it. Where resolved runs, instances resolve through its
120# upstreams instead, and the control service opens those. The "-" makes
121# this a no-op on a box without resolved.
122BindReadOnlyPaths=-/run/systemd/resolve/resolv.conf:/etc/resolv.conf
123ProtectKernelTunables=yes
124ProtectKernelModules=yes
125ProtectKernelLogs=yes
126ProtectClock=yes
127ProtectHostname=yes
128RemoveIPC=yes
129LockPersonality=yes
130RestrictRealtime=yes
131# No RestrictSUIDSGID=: systemd cannot read the mode inside openat2's
132# open_how, so it refuses openat2 outright (ENOSYS), and every instancefs
133# open in here is an openat2 (#126, first roll; systemd 257). What it
134# guarded is covered: with no capabilities an instance can only make a
135# set-id file owned by carlos, which gains nothing for another carlos
136# process, NoNewPrivileges= voids it in here, and no other uid can reach
137# one (the tenant trees are nosuid, PrivateTmp's sit under a root 0700
138# directory).
139MemoryDenyWriteExecute=yes
140SystemCallArchitectures=native
141
142# --- per-service resources; the tier lives on the host slice (§4.3) ---
143TasksMax=256
144# The kernel picks a tenant before it picks anything else in carlos.slice.
145OOMScoreAdjust=500
146# One OOM kill takes every process of THIS service (systemd sets
147# memory.oom.group on the service cgroup for OOMPolicy=kill). The sidecar
148# is a sibling service with its own family rule (§4.3).
149OOMPolicy=kill
150