# Hardened systemd unit for the GoblinPay server on bare metal. # # The recommended path is the setup wizard, which writes everything this unit # reads (the env file and the 0400 wallet-password credential) and creates the # encrypted wallet for you: # sudo install -m0644 deploy/gp-server.service /etc/systemd/system/ # sudo gp-server setup # writes /etc/goblinpay.env + the secret file # sudo systemctl daemon-reload && sudo systemctl enable --now gp-server # # Unlike goblin-nip05d, this service holds a MONEY secret (the wallet password, # which decrypts the Grin seed at rest) and a wallet data directory. The # operator CHOOSES that password at setup (grin-wallet-faithful; it is never # auto-generated). It is passed as a systemd credential (read by PID1 as root, # exposed read-only to the dynamic service user) rather than left world-readable. # # Restart mode (the wizard asks; default UNATTENDED): # UNATTENDED: the chosen password is sealed to THIS host and the service # auto-restarts with no human. By default (systemd-creds present) it is # ENCRYPTED at rest: a `systemd-creds encrypt` ciphertext blob, decrypted # into a tmpfs credentials dir at each start, so no plaintext key is on disk. # If systemd-creds is unavailable it falls back to a root-owned 0400 plaintext # file (with a loud warning). Honest trade-off: a compromise of the RUNNING # host still means wallet compromise; keep the till a small hot float and # sweep to your own wallet regularly. # MANUAL — nothing is stored on disk; the operator re-enters the password after # every restart. The wizard drops in gp-server.service.d/manual.conf, which # repoints LoadCredential to a tmpfs path (/run/goblinpay/wallet_password) # the operator populates at each start via systemd-ask-password. Maximum # protection against disk/machine theft, at the cost of hands-on restarts. # # The Grin SEED is deliberately NOT a service credential: the wizard consumes it # once at setup to create the encrypted wallet, after which the wallet opens # with the password alone (init-once). The seed then lives only encrypted at # rest in the data dir and in the operator's written backup, never in this # service's environment. (To bootstrap a wallet WITHOUT the wizard, set # GP_MNEMONIC_FILE for the first boot only, then remove it.) [Unit] Description=GoblinPay — self-hostable, receive-only Grin payment server After=network-online.target Wants=network-online.target [Service] Type=exec # Throwaway unprivileged user allocated at runtime. For a stable data owner, # comment this out and set `User=goblinpay` (create the user first). DynamicUser=yes # Non-secret config (domain, node, tokens, webhook, relay URL). Read by systemd # as root, so a 0640 root:root file is fine even under DynamicUser. EnvironmentFile=/etc/goblinpay.env # The wallet password as a credential. systemd exposes it under # $CREDENTIALS_DIRECTORY (%d), readable only by the dynamic service user, and the # wallet is pointed at it via the *_FILE variant. This overrides any # GP_WALLET_PASSWORD_FILE in the env file above, so the service always reads the # credential copy at %d/gp_wallet_password. # # The wizard (`gp-server setup`) writes a drop-in that picks the delivery: # * UNATTENDED, ENCRYPTED (the secure default): gp-server.service.d/encrypted.conf # resets the line below and uses `LoadCredentialEncrypted=` to read a # host-sealed CIPHERTEXT blob (wallet_password.cred, made with # `systemd-creds encrypt`). No plaintext wallet password is on disk. # * UNATTENDED, plaintext fallback (only when systemd-creds is unavailable): # no drop-in; the line below reads the root-owned 0400 PLAINTEXT file. # * MANUAL: gp-server.service.d/manual.conf resets the line and reads # /run/goblinpay/wallet_password (tmpfs), which the operator populates by hand. # # The line below is the plaintext-fallback default so a by-hand install works out # of the box. To harden a by-hand install, seal the password and switch to the # encrypted directive: # printf '%s' 'YOUR-WALLET-PASSWORD' \ # | sudo systemd-creds encrypt --name=gp_wallet_password - \ # /etc/goblinpay/secrets/wallet_password.cred # # then, in a drop-in, replace the LoadCredential line below with: # # LoadCredentialEncrypted=gp_wallet_password:/etc/goblinpay/secrets/wallet_password.cred LoadCredential=gp_wallet_password:/etc/goblinpay/secrets/wallet_password Environment=GP_WALLET_PASSWORD_FILE=%d/gp_wallet_password # First-boot-only seed bootstrap WITHOUT the wizard (remove after the wallet # exists). The wizard makes this unnecessary. #LoadCredential=gp_mnemonic:/etc/goblinpay/secrets/mnemonic #Environment=GP_MNEMONIC_FILE=%d/gp_mnemonic # Optional: a NIP-49 encrypted Nostr identity (else a random one is generated # and persisted under the data dir on first start). Uncomment with its file: #LoadCredential=gp_ncryptsec:/etc/goblinpay/secrets/ncryptsec #Environment=GP_NCRYPTSEC_FILE=%d/gp_ncryptsec # Managed state at /var/lib/goblinpay: the SQLite db, the wallet files, and the # encrypted seed at rest. 0700 — only the service user may read it. StateDirectory=goblinpay StateDirectoryMode=0700 Environment=GP_DB_PATH=/var/lib/goblinpay/goblinpay.db Environment=GP_DATA_DIR=/var/lib/goblinpay/gp-data ExecStart=/usr/local/bin/gp-server Restart=on-failure RestartSec=2 # --- hardening --- NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes PrivateDevices=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes ProtectClock=yes ProtectHostname=yes RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes # If the Nym mixnet stack ever fails to start with a W^X error, comment this out. MemoryDenyWriteExecute=yes SystemCallArchitectures=native SystemCallFilter=@system-service SystemCallFilter=~@privileged @resources # Only the state directory is writable. ReadWritePaths=/var/lib/goblinpay # No raw sockets; only IP + unix. RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX [Install] WantedBy=multi-user.target