| 1 | [Unit] |
| 2 | Description=lchat public fleet hardening posture (one collect pass) |
| 3 | After=network-online.target |
| 4 | Wants=network-online.target |
| 5 | |
| 6 | [Service] |
| 7 | Type=oneshot |
| 8 | # Optional overrides: STATUS_BUCKET / STATUS_REGION (mirror status-publish.sh). |
| 9 | EnvironmentFile=-/data/status.env |
| 10 | # "+" = outside the sandbox: both ReadWritePaths must exist before |
| 11 | # ProtectSystem=strict mounts them, or the first-ever run fails to namespace. |
| 12 | ExecStartPre=+/usr/bin/mkdir -p /data/hardening /opt/lchat/status-pages |
| 13 | ExecStart=/opt/lchat/hardening-posture.sh |
| 14 | |
| 15 | # --- hardening (see docs/instance-router.md § Hardening) --- |
| 16 | # Runs as ROOT deliberately: the LOCAL box's own posture reads are root-only |
| 17 | # (sshd -T's effective config, ufw status, ip link), and /data/hardening + |
| 18 | # /opt/lchat/status-pages are its only writes — ProtectSystem=strict pins |
| 19 | # exactly those. It is fired by lchat-hardening.timer, never routed or |
| 20 | # listening, and publishes NO hostnames (see the script's header). The peer :22 |
| 21 | # probe is an outbound connect only. |
| 22 | ProtectSystem=strict |
| 23 | ReadWritePaths=/data/hardening /opt/lchat/status-pages |
| 24 | ProtectHome=yes |
| 25 | PrivateTmp=yes |
| 26 | PrivateDevices=yes |
| 27 | ProtectClock=yes |
| 28 | ProtectHostname=yes |
| 29 | ProtectKernelTunables=yes |
| 30 | ProtectKernelModules=yes |
| 31 | ProtectKernelLogs=yes |
| 32 | NoNewPrivileges=yes |
| 33 | # AF_NETLINK: `ufw status` and `ip link show wg0` are netlink queries — without |
| 34 | # it both probes fail silently and the box publishes false "firewall inactive"/ |
| 35 | # "wireguard absent" alarms (the dashboard's first real pass, 2026-07-19). |
| 36 | # Read-only in effect: the collector never issues netlink writes. |
| 37 | RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK |
| 38 | RestrictNamespaces=yes |
| 39 | RestrictRealtime=yes |
| 40 | LockPersonality=yes |
| 41 | MemoryDenyWriteExecute=yes |
| 42 | MemoryMax=256M |
| 43 | TasksMax=64 |
| 44 | |