[Unit] Description=lchat router (TLS terminator + host router) After=network-online.target Wants=network-online.target [Service] # Optional overrides, e.g. LCHAT_FALLBACK_URL (where unknown hosts redirect # instead of 404 — pairs with a wildcard DNS record; see cloud-provisioner.md). EnvironmentFile=-/data/router.env ExecStart=/opt/lchat/lchat router --registry /data/registry.db --acme-cache /data/acme # add-instance triggers `systemctl reload lchat-router` to re-read the route table. ExecReload=/bin/kill -HUP $MAINPID Restart=always RestartSec=1 # --- hardening (see docs/instance-router.md § Hardening) --- User=lchat Group=lchat # The router is the ONLY unit that binds privileged ports (:80 + :443); this cap # lets it do so as a non-root user. Every other unit binds a unix socket only. AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/data /run/lchat PrivateTmp=yes PrivateDevices=yes ProtectClock=yes ProtectHostname=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes ProtectControlGroups=yes ProtectProc=invisible RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes # The Go binary never maps writable+executable memory, so this is safe (unlike the # Node sidecar, whose JIT needs it — see lchat-bot@.service). MemoryDenyWriteExecute=yes SystemCallFilter=@system-service SystemCallErrorNumber=EPERM SystemCallArchitectures=native MemoryMax=256M TasksMax=256 [Install] WantedBy=multi-user.target