carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1# The tenant template: one instance per route host (%i = the host, which
2# is also the live/<host> symlink the adopt pass repoints and the unit
3# the agent's restartRoute try-restarts).
4#
5# DynamicUser: systemd mints an ephemeral uid per service at start — apps
6# share no identity with each other or the platform, and nobody manages
7# user accounts. State lives in /var/lib/carlos-app/<host> (chowned to
8# the fresh uid each start); the app finds it via $STATE_DIRECTORY.
9# IMDS is fenced twice: IPAddressDeny below blocks the metadata address
10# for every tenant at the unit level (a box with no nftables gate — tito,
11# issue #351 — is otherwise wide open), and on hardened boxes the
12# harden-host skuid gate blocks dynamic uids by construction — only
13# allowlisted uids may mint host credentials. A host_role tenant's
14# drop-in re-opens the unit-level fence with IPAddressAllow (allow wins
15# over deny); the skuid gate still has the last word.
16#
17# Caveat: DynamicUser is per-unit, so an app whose processes must share
18# files across units (a router + instance family like titogo's) needs a
19# static app user instead — this template is for single-process tenants.
20#
21# Listener contract: with carlos-app@<host>.socket enabled, systemd passes
22# the bound unix socket in (LISTEN_FDS=1, fd 3) whenever this service
23# starts — socket-activated or systemctl-started alike — and the app must
24# serve on it. There is deliberately no Sockets= line and no CARLOS_SOCKET
25# env: fd inheritance is opt-in per tenant by enabling the socket unit, so
26# a TCP-only app on an Addr route never sees LISTEN_FDS and keeps binding
27# its own port (the fallback; see carlos-app@.socket for the perms story).
28#
29# Hibernation (routes.hibernate + backing=unit): the agent's activator
30# stops this service after the route's idle window and starts it, health-
31# gated, when a request arrives. Restart=always still owns crashes.
32#
33# Apps needing different arguments override ExecStart via a drop-in.
34
35[Unit]
36Description=CARLOS app %i
37After=network-online.target
38
39[Service]
40DynamicUser=yes
41StateDirectory=carlos-app/%i
42ExecStart=/opt/carlos/live/%i serve
43# The version store holds every app's build on the box (#214 item 2). This
44# unit sees an empty /opt/carlos with its own build put back at the path
45# ExecStart= names: PID 1 chases the live link when it binds it, so a
46# repoint takes effect at the next start, as it always has.
47TemporaryFileSystem=/opt/carlos:ro
48BindReadOnlyPaths=/opt/carlos/live/%i
49EnvironmentFile=-/etc/carlos/apps/%i.env
50Restart=always
51NoNewPrivileges=yes
52ProtectSystem=strict
53PrivateTmp=yes
54ProtectProc=invisible
55ProcSubset=pid
56ProtectHome=yes
57PrivateDevices=yes
58ProtectKernelTunables=yes
59ProtectKernelModules=yes
60ProtectKernelLogs=yes
61ProtectClock=yes
62ProtectHostname=yes
63RemoveIPC=yes
64LockPersonality=yes
65RestrictRealtime=yes
66RestrictNamespaces=yes
67CapabilityBoundingSet=
68RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
69IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128
70MemoryDenyWriteExecute=yes
71MemoryMax=512M
72# Throttle-and-reclaim before the kill — the same band the exec path's
73# memory.high gives (internal/cgroup applyLimit, 90% of max).
74MemoryHigh=460M
75TasksMax=256
76
77[Install]
78WantedBy=multi-user.target
79