[Unit] Description=RChat Server — E2EE WebSocket Relay (direct TLS on :443) Documentation=https://docs.r-chat.net After=network-online.target Wants=network-online.target # Never give up restarting (M-5): a burst of OOM kills under attack must not # leave the relay down until an operator notices. RestartSec paces it. StartLimitIntervalSec=0 # Direct deployment: rchat owns public port 443 and terminates TLS 1.3 itself, # with either built-in Let's Encrypt ([server.acme]) or a static certificate # ([server.tls]) in /etc/rchat/server.toml. See https://docs.r-chat.net/server/certificates/. # Published as /rchat-server.service on assets.r-chat.net; install.sh # installs it as /etc/systemd/system/rchat-server.service. # Behind a layer-4 front end (PROXY protocol v2) use rchat-server-proxy.service instead. # Check the sandbox with: systemd-analyze security rchat-server.service [Service] Type=simple User=rchat Group=rchat WorkingDirectory=/var/lib/rchat # No --no-tls: the server refuses to start unless TLS is configured. ExecStart=/usr/local/bin/rchat-server --config /etc/rchat/server.toml # Static certificate: SIGHUP re-reads cert_path/key_path without dropping # connections (a failed reload keeps the old cert and logs an error). # ACME: renewals hot-swap on their own; SIGHUP is a harmless no-op. ExecReload=/bin/kill -HUP $MAINPID # Bind :443 as a non-root user, and nothing else. AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE # /var/lib/rchat (0700) holds the ACME cache (/var/lib/rchat/acme) and the # encrypted message queue file. UMask keeps every file the service creates # owner-only. StateDirectory=rchat StateDirectoryMode=0700 UMask=0077 # Graceful shutdown TimeoutStopSec=30 KillSignal=SIGTERM # Restart policy Restart=on-failure RestartSec=5 # Back off between restarts, 5 s growing to 60 s over 10 restarts, so a # deterministic startup failure (bad config, missing key) does not spin # every 5 s forever. systemd >= 254; older versions log "Unknown key" and # keep the fixed RestartSec. RestartSteps=10 RestartMaxDelaySec=60 # Resource limits LimitNOFILE=1048576 LimitNPROC=65535 # No core files: the process holds the relay identity seed, the queue key # and per-boot tag keys (the relay also sets RLIMIT_CORE=0 and # PR_SET_DUMPABLE=0 itself). LimitCORE=0 # Backstops so a memory or thread blow-up stays inside this unit. The relay # bounds its own state by default: offline queue 256 MiB, knocks 16 MiB, # all outbound queues 1 GiB, all WebSocket buffers (messages still being # received, codec buffers) 512 MiB - about 1.8 GiB plus per-connection # overhead. Raise MemoryMax if you raise the offline-queue budgets. MemoryMax=3G TasksMax=4096 # Security hardening NoNewPrivileges=true ProtectSystem=strict ProtectHome=true # Writable: /var/lib/rchat (StateDirectory: ACME cache, # offline_queue.persist_path) and /run/rchat (RuntimeDirectory, a tmpfs: # offline_queue.persist_key_path). Nothing else: keep the paths in # /etc/rchat/server.toml under these two (the template does). ReadWritePaths=/var/lib/rchat RuntimeDirectory=rchat RuntimeDirectoryMode=0700 # Keep /run/rchat (and the queue key in it) across stops and restarts; the # tmpfs still clears it at reboot, which is the point of keeping the key # there (L-2). Without this, systemd empties it on every restart and # offline_queue.persist = true could never start. RuntimeDirectoryPreserve=yes # The offline-queue key for persist = true (offline_queue.persist_key_path): # 32 random bytes, mode 0600, created once per boot if missing. Harmless # when persistence is off. Written to a temporary name first, so a crash # never leaves a short key behind. ($$ is systemd's escape for a literal $.) ExecStartPre=/bin/sh -c 'umask 077; k=/run/rchat/queue.key; [ -s "$$k" ] || { dd if=/dev/urandom of="$$k.new" bs=32 count=1 2>/dev/null && mv -f "$$k.new" "$$k"; }' PrivateTmp=true # Only null/zero/random/urandom etc.; ring uses the getrandom(2) syscall. PrivateDevices=true ProtectClock=true ProtectHostname=true ProtectKernelTunables=true ProtectKernelModules=true ProtectKernelLogs=true ProtectControlGroups=true ProtectProc=invisible ProcSubset=pid RemoveIPC=true KeyringMode=private RestrictSUIDSGID=true RestrictRealtime=true RestrictNamespaces=true LockPersonality=true # rustls/ring and tokio do not JIT or map W+X memory. MemoryDenyWriteExecute=true # IPv4/IPv6 for clients and the outbound ACME client; AF_UNIX for journald # and local name-service lookups. RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged @resources SystemCallErrorNumber=EPERM # Logging StandardOutput=journal StandardError=journal SyslogIdentifier=rchat-server [Install] WantedBy=multi-user.target