[Unit] Description=RChat Server — E2EE WebSocket Relay (behind a layer-4 front end) 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 [Service] Type=simple User=rchat Group=rchat WorkingDirectory=/var/lib/rchat # Published as /rchat-server-proxy.service on assets.r-chat.net; the # docs install it as /etc/systemd/system/rchat-server.service. # This unit is for the layer-4 layout (rchat/config/server-behind-proxy.toml in # rchat-core): # a TCP passthrough front end on this host (HAProxy mode tcp with # send-proxy-v2, or caddy-l4) owns public :443 and forwards raw bytes, # prefixed with a PROXY protocol v2 header, to the relay on 127.0.0.1:8443. # The relay terminates TLS 1.3 itself with the [server.tls] certificate in # /etc/rchat/server.toml (protocol v2 binds key authentication to the # relay's own TLS session, so a TLS-terminating proxy cannot work). The # bind MUST stay on loopback (the relay refuses any other bind with # proxy_protocol = true). For a server that owns :443 itself use the direct # unit, published as rchat-server.service. # The relay parses the same untrusted frames as in the direct layout, so it # gets the same sandbox. Check it with: # systemd-analyze security rchat-server.service # # Use the release binary (the static musl build on Linux). # No --no-tls: the server refuses to start unless TLS is configured. ExecStart=/usr/local/bin/rchat-server \ --config /etc/rchat/server.toml \ --bind 127.0.0.1:8443 # Static certificate: SIGHUP re-reads cert_path/key_path without dropping # connections (a failed reload keeps the old cert and logs an error). ExecReload=/bin/kill -HUP $MAINPID # A loopback port above 1024 needs no capability at all. CapabilityBoundingSet= AmbientCapabilities= # /var/lib/rchat (0700) holds the relay identity key (relay_identity_path in # the template) and the encrypted message queue file (if enabled). # 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: 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. 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 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 MemoryDenyWriteExecute=true # Loopback TCP from the front end; AF_UNIX for journald and 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