carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1# The readiness probe (isolated-exec design §8.5): a oneshot under the SAME
2# namespace text as the tenant templates (the test pins the block byte for
3# byte), run by carlos-instance-control at its start and every five
4# minutes. From inside it asserts what a tenant would see and prints a
5# verdict; the unit's StandardOutput=truncate: lands it where the control
6# service reads it. It exits 0 whatever the verdict: the verdict is the
7# output. Its instance directory is _probe, a name no host can have; the
8# tmpfiles lines create it.
9[Unit]
10Description=carlos instance isolation probe
11Requires=data-instances.mount run-carlos-instances.mount
12After=data-instances.mount run-carlos-instances.mount
13
14[Service]
15Type=oneshot
16ExecStart=/opt/carlos/carlos instance-probe
17User=carlos
18Group=carlos
19Slice=carlos-instances.slice
20StandardOutput=truncate:/run/carlos-control/probe.out
21StandardError=journal
22TimeoutStartSec=30
23# The instance's own two directories, the only writable data paths.
24BindPaths=/data/instances/_probe /run/carlos/instances/_probe
25ReadWritePaths=/data/instances/_probe /run/carlos/instances/_probe
26# The same two binds the instance templates make (#214 item 2), for the
27# name _probe: /opt/carlos/live/_probe is a link to ../app, and
28# /opt/carlos/app is whatever the source link points at, here the
29# platform binary standing in for a build. The control service writes
30# both sources before every probe run.
31BindReadOnlyPaths=/run/carlos-control/exe/_probe/app:/opt/carlos/app /run/carlos-control/exe/_probe/live:/opt/carlos/live
32
33# --- the namespace (titogo@.service, 2026-07-31 design, on systemd 257) ---
34ProtectSystem=strict
35TemporaryFileSystem=/data:ro
36TemporaryFileSystem=/run/carlos:ro
37# Every other tree a carlos-uid unit writes on the box is hidden the same
38# way (codex R9-2): the restore drills' cache under /var/cache, and
39# /var/lib as a class. An instance needs nothing from either.
40TemporaryFileSystem=/var/cache:ro
41TemporaryFileSystem=/var/lib:ro
42# The version store holds every app's build on the box, world-readable so
43# a unit can exec its own (#214 item 2). An instance sees none of it: an
44# empty /opt/carlos with the platform binary that ExecStart= and
45# ExecStopPost= run put back, and, outside this block because it is per
46# instance, its own build at /opt/carlos/live/<host>.
47TemporaryFileSystem=/opt/carlos:ro
48BindReadOnlyPaths=/opt/carlos/carlos
49# Root-written credentials a carlos-GROUP process may read live here:
50# /etc/carlos/relay-masters is root:carlos 0640 for carlos-relay.service,
51# and an instance's Group=carlos would read it too (codex R10-1). An
52# instance gets its configuration through its launch file and needs
53# nothing from /etc/carlos. /run/carlos-control is the root control
54# service's socket directory (design §5.2): hiding it here too means a
55# connect from inside any instance's namespace is impossible outright,
56# not merely refused by the peer check on the far end (final review,
57# plan 2, item 3).
58InaccessiblePaths=/etc/carlos /run/carlos-control
59ProtectHome=yes
60PrivateTmp=yes
61PrivateDevices=yes
62# PrivateDevices= bind-mounts the HOST's /dev/shm into the private /dev,
63# and every instance is uid carlos, so a sibling's shared memory would be
64# one open away. Each unit gets its own, charged to its own cgroup.
65TemporaryFileSystem=/dev/shm:mode=1777,nosuid,nodev
66# POSIX message queues and SysV IPC belong to an IPC namespace, and every
67# instance is uid carlos, so a sibling's queue would be one mq_open away
68# (#178). PrivateIPC= gives each unit its own namespace, and mq_open()
69# goes through that namespace's own internal mount. PrivateDevices=
70# carries the host's /dev/mqueue into the private /dev the way it carries
71# /dev/shm, and that mount would still list and open the host namespace's
72# queues, so it is replaced the same way.
73PrivateIPC=yes
74TemporaryFileSystem=/dev/mqueue:ro
75# /sys/fs/cgroup read-only: the edge's delegated subtree is carlos-owned,
76# and its cgroup.kill / cgroup.freeze would otherwise be one write away
77# for a same-uid tenant (codex R9-1).
78ProtectControlGroups=yes
79ProtectProc=invisible
80ProcSubset=pid
81PrivatePIDs=yes
82RestrictNamespaces=yes
83NoNewPrivileges=yes
84CapabilityBoundingSet=
85RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
86# Abstract unix sockets live in the network namespace, which instances
87# share with the box because they need outbound TCP, and the line above
88# cannot tell them from path sockets (#178). The trampoline scopes them
89# with Landlock before it execs the app (instanceexec.ScopeAbstractUnix):
90# an instance can connect only to abstract sockets its own processes
91# bound. A sibling can still bind a name first; that is a refusal of
92# service, never a connection.
93#
94# Egress (#214 item 3): the internet, and nothing on the box or private.
95# Loopback is where the console and other tenants' addr upstreams listen;
96# RFC 1918, ULA, link-local and CGNAT are the VPC, other boxes and the
97# cloud's internal services. IMDS stays named so the property is
98# greppable. A cgroup filter: it drops in both directions for every
99# socket the unit opens. The one thing an instance needs from these
100# ranges is the box's DNS resolver, so the control service re-opens
101# exactly the nameservers the instance sees with IPAddressAllow= on
102# carlos-instances.slice (an allow wins, and a slice's lists reach every
103# unit below it). The probe checks both halves.
104IPAddressDeny=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
105# An allow opens an address both ways and on every port, so a loopback
106# resolver can never be opened: a tenant could bind systemd-resolved's
107# 127.0.0.53 itself and reach every listener bound to all addresses
108# through it. Where resolved runs, instances resolve through its
109# upstreams instead, and the control service opens those. The "-" makes
110# this a no-op on a box without resolved.
111BindReadOnlyPaths=-/run/systemd/resolve/resolv.conf:/etc/resolv.conf
112ProtectKernelTunables=yes
113ProtectKernelModules=yes
114ProtectKernelLogs=yes
115ProtectClock=yes
116ProtectHostname=yes
117RemoveIPC=yes
118LockPersonality=yes
119RestrictRealtime=yes
120# No RestrictSUIDSGID=: systemd cannot read the mode inside openat2's
121# open_how, so it refuses openat2 outright (ENOSYS), and every instancefs
122# open in here is an openat2 (#126, first roll; systemd 257). What it
123# guarded is covered: with no capabilities an instance can only make a
124# set-id file owned by carlos, which gains nothing for another carlos
125# process, NoNewPrivileges= voids it in here, and no other uid can reach
126# one (the tenant trees are nosuid, PrivateTmp's sit under a root 0700
127# directory).
128MemoryDenyWriteExecute=yes
129SystemCallArchitectures=native
130
131# --- per-service resources; the tier lives on the host slice (§4.3) ---
132TasksMax=64
133MemoryMax=64M
134# Read back by the probe from its own cgroup (#214 item 1): proof that CPU
135# and IO settings take under carlos-instances.slice, where the control
136# service sets each host slice's from its tier.
137CPUQuota=50%
138CPUWeight=50
139IOWeight=50
140