b2e11ff1fe
Close the remaining "secrets touch a plaintext file" gap. The unattended restart mode now SEALS the operator-chosen wallet password to the host with `systemd-creds encrypt` (ciphertext at rest, read via LoadCredentialEncrypted, decrypted into a tmpfs credentials dir at each start), so no plaintext wallet key is written to disk. When systemd-creds is unavailable it falls back to the legacy root-owned 0400 plaintext file with a loud warning, and re-running `setup --reconfigure` once it is available upgrades to encryption. - wizard: seal via systemd-creds (injectable for tests), write the encrypted.conf drop-in, keep the manual.conf tmpfs path unchanged; reconfiguring a till that keeps no readable password on disk (encrypted or manual) now prompts for the existing password to reopen the wallet. - startup: loud deprecation warning when a money secret (GP_MNEMONIC, GP_WALLET_PASSWORD, GP_NSEC) is supplied INLINE rather than via a *_FILE or systemd credential; the value is never logged, only the variable name. - docs: README, .env.example, install.sh, and the systemd unit updated to the encrypted-at-rest default and the honest fallback/trade-off. Tests: gp-core config + gp-core/gp-server setup cover the inline-secret warning, the encrypted drop-in, the sealed-vs-plaintext shapes, and encrypted reconfigure. Full workspace suite green; fmt introduces no new drift; clippy -D warnings clean on the workspace crates.
126 lines
6.0 KiB
Desktop File
126 lines
6.0 KiB
Desktop File
# 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
|