morphit/ops/systemd/morphit-relay.service

88 lines
4.2 KiB
Desktop File

[Unit]
# Morphit relay — broadcasts user-signed chain ops with the relay active key,
# plus invite/push/challenge REST endpoints. Matches the morphit-ops
# deployment: monorepo at /opt/morphit, root-owned config, Docker Postgres.
#
# Unattended unlock of the encrypted active key (ADR-0010 §4):
# The relay active key is an encrypted envelope. Its passphrase is delivered
# as a systemd ENCRYPTED CREDENTIAL — never as plaintext on disk. systemd
# decrypts it at every start (host key / TPM); no prompt, fully unattended
# across reboots. This is REQUIRED — the unit refuses to start without it.
# Create the credential once and migrate, per docs/OPERATIONS.md.
#
# Install (one-time, as root) — the encrypted credential MUST exist first:
# echo -n 'your-relay-passphrase' | systemd-creds encrypt --name=relay_passphrase - /etc/morphit/relay_passphrase.cred
# cp ops/systemd/morphit-relay.service /etc/systemd/system/
# systemctl daemon-reload
# systemctl enable --now morphit-relay
# journalctl -u morphit-relay -f
Description=Morphit relay (broadcasts user-signed chain ops; REST API)
Documentation=https://git.agorise.net/agorise/morphit/src/branch/main/docs/OPERATIONS.md
After=network-online.target docker.service
Wants=network-online.target
# Reboot recovery (cp597): disable the start-rate-limit so a power-cut restart
# never latches this unit into a permanent 'failed' state while Postgres (a
# Docker container here) is still coming up — it keeps retrying every RestartSec
# until the DB is reachable, with no ceiling on attempts.
StartLimitIntervalSec=0
[Service]
Type=simple
# Matches the current deployment (root-owned tree + config). De-privilege by
# chowning config + data to a service user and setting User=/Group= here.
User=root
WorkingDirectory=/opt/morphit/apps/relay
# ENFORCED encrypted-at-rest passphrase via systemd-creds. Create it ONCE:
# echo -n 'your-relay-passphrase' | systemd-creds encrypt --name=relay_passphrase - /etc/morphit/relay_passphrase.cred
# systemd decrypts it at every start (host key / TPM) into a memory-backed,
# service-only file; the relay reads the passphrase from that FILE
# (MORPHIT_RELAY_ACTIVE_KEY_PASSPHRASE_FILE) so the secret never enters the
# process environment, and never touches persistent disk in plaintext. If the
# credential is missing this unit FAILS to start — intentionally; do not fall
# back to a plaintext passphrase in morphit.env. See docs/OPERATIONS.md.
LoadCredentialEncrypted=relay_passphrase:/etc/morphit/relay_passphrase.cred
# Source BOTH env layouts, each guarded with `[ -f ]`: the ops-cli layout
# (/opt/morphit/morphit.env + morphit.config.env) AND the Ansible layout
# (/etc/morphit/relay.env), plus the generate-once VAPID file. Whichever a
# given install produced is sourced; the other is simply absent. This is why
# the relay works identically whether it was set up by `morphit-ops init` or
# by the Ansible playbook (which writes /etc/morphit/relay.env).
ExecStart=/usr/bin/env bash -c 'set -a; for f in /opt/morphit/morphit.env /opt/morphit/morphit.config.env /etc/morphit/relay.env /etc/morphit/relay-vapid.env; do [ -f "$f" ] && . "$f"; done; export MORPHIT_RELAY_ACTIVE_KEY_PASSPHRASE_FILE="${CREDENTIALS_DIRECTORY}/relay_passphrase"; set +a; exec /opt/morphit/node_modules/.bin/tsx src/main.ts'
# With the env-var passphrase path, restart-on-crash actually succeeds now
# (no interactive prompt to hang on).
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=25
StandardOutput=journal
StandardError=journal
SyslogIdentifier=morphit-relay
# --- Hardening (safe with User=root) -------------------------------
NoNewPrivileges=yes
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictNamespaces=yes
SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
CapabilityBoundingSet=
AmbientCapabilities=
# A core dump would contain the decrypted WIF in memory.
LimitCORE=0
# Node's V8 JIT writes+executes in memory, so W^X must stay off.
MemoryDenyWriteExecute=no
MemoryMax=512M
TasksMax=256
[Install]
WantedBy=multi-user.target