From ac572e64e5029cb1db16f05158915b6ef55ab7e7 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Fri, 3 Oct 2025 10:34:19 +0200 Subject: [PATCH] node: Support systemd credential for passphrase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While it is possible to pass the passphrase via the environment, e.g. `EnvironmentFile=` this is less secure than passing it via a file, because the environment is inherited down the process tree. Thus, allow using a systemd credential. The ID of the credential must be xyz.radicle.node.passphrase and is not user-configurable. Passing the passphrase via file is now possible with `LoadCredential=xyz.radicle.node.passphrase:` This requires just a bit of plumbing in `radicle-node`. Because this mechanism is more secure than using the environment variable `RAD_PASSPHRASE`, it takes priority. That is, if both the systemd credential is available, *and* the environment variable `RAD_PASSPHRASE` is set, the former is preferred. Heads-up: 1. The contents of the file must be valid UTF-8 (see documentation of `std::fs::read_to_string`). Assuming that the passphrase is at some point chosen by the user and typed on a keyboard, this does not seem like a severe restriction. 2. The contents of the file are not processed otherwise, i.e. line breaks (notably at the end of the file) are not stripped. The related `issue/8bd040e9de05e7fc27e373ebc1649ff4ad930e7a` asked for a very similar feature: Passing the passphrase via a file named by the value of the of the environment variable `RAD_PASSPHRASE_FILE`. It was also briefly discussed at . --- CHANGELOG.md | 14 +++++++++----- crates/radicle-node/src/main.rs | 23 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10039533..89d3661e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `rad issue` now uses `clap` to parse its command-line arguments. This affects error reporting as well as help output. - `radicle-node` now supports systemd Credentials (refer to - for more information) to load - the secret key, in addition to the commandline argument - `--secret` (higher priority than the credential) and the - configuration file (lower priority than the credential). - The identifier of the credential is "xyz.radicle.node.secret". + for more information) to load: + 1. The secret key, in addition to the commandline argument + `--secret` (higher priority than the credential) and the + configuration file (lower priority than the credential). + The identifier of the credential is "xyz.radicle.node.secret". + 2. The optional passphrase for the secret key, in addition to the + environment variable `RAD_PASSPHRASE` (lower priority than the + credential). + The identifier of the credential is "xyz.radicle.node.passphrase". ## Fixed Bugs diff --git a/crates/radicle-node/src/main.rs b/crates/radicle-node/src/main.rs index fac2fed5..9fcd4cc8 100644 --- a/crates/radicle-node/src/main.rs +++ b/crates/radicle-node/src/main.rs @@ -235,7 +235,28 @@ fn execute(options: Options) -> Result<(), ExecutionError> { log::info!(target: "node", "Version {} ({})", env!("RADICLE_VERSION"), env!("GIT_HEAD")); log::info!(target: "node", "Unlocking node keystore.."); - let passphrase = profile::env::passphrase(); + let passphrase = None; + + #[cfg(all(feature = "systemd", target_os = "linux"))] + let passphrase = passphrase.or_else(|| { + const ID: &str = "xyz.radicle.node.passphrase"; + match radicle_systemd::credential::path(ID) { + Err(err) => { + log::warn!(target: "node", "Failed to obtain path of the passphrase file via systemd credential with '{ID}': {err}"); + None + }, + Ok(Some(ref path)) => match std::fs::read_to_string(path) { + Ok(passphrase) => Some(passphrase.into()), + Err(err) => { + log::warn!(target: "node", "Failed to read passphrase from '{}': {err}", path.display()); + None + } + } + Ok(None) => None, + } + }); + + let passphrase = passphrase.or_else(profile::env::passphrase); let secret_path = options.secret;