| 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] |
| 36 | Description=CARLOS app %i |
| 37 | After=network-online.target |
| 38 | |
| 39 | [Service] |
| 40 | DynamicUser=yes |
| 41 | StateDirectory=carlos-app/%i |
| 42 | ExecStart=/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. |
| 47 | TemporaryFileSystem=/opt/carlos:ro |
| 48 | BindReadOnlyPaths=/opt/carlos/live/%i |
| 49 | EnvironmentFile=-/etc/carlos/apps/%i.env |
| 50 | Restart=always |
| 51 | NoNewPrivileges=yes |
| 52 | ProtectSystem=strict |
| 53 | PrivateTmp=yes |
| 54 | ProtectProc=invisible |
| 55 | ProcSubset=pid |
| 56 | ProtectHome=yes |
| 57 | PrivateDevices=yes |
| 58 | ProtectKernelTunables=yes |
| 59 | ProtectKernelModules=yes |
| 60 | ProtectKernelLogs=yes |
| 61 | ProtectClock=yes |
| 62 | ProtectHostname=yes |
| 63 | RemoveIPC=yes |
| 64 | LockPersonality=yes |
| 65 | RestrictRealtime=yes |
| 66 | RestrictNamespaces=yes |
| 67 | CapabilityBoundingSet= |
| 68 | RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX |
| 69 | IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 |
| 70 | MemoryDenyWriteExecute=yes |
| 71 | MemoryMax=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). |
| 74 | MemoryHigh=460M |
| 75 | TasksMax=256 |
| 76 | |
| 77 | [Install] |
| 78 | WantedBy=multi-user.target |
| 79 | |