node: Support systemd credential for passphrase

While it is possible to pass the passphrase via the environment, e.g.
`EnvironmentFile=<path to file that contains "RAD_PASSPHRASE=…">`
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:<path to file that contains 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
<https://radicle.zulipchat.com/#narrow/channel/369277-heartwood/topic/.60RAD_PASSPHRASE_FILE.60/with/529104447>.
This commit is contained in:
Lorenz Leutgeb 2025-10-03 10:34:19 +02:00 committed by Fintan Halpenny
parent ae39f24b58
commit ac572e64e5
2 changed files with 31 additions and 6 deletions

View File

@ -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. - `rad issue` now uses `clap` to parse its command-line arguments.
This affects error reporting as well as help output. This affects error reporting as well as help output.
- `radicle-node` now supports systemd Credentials (refer to - `radicle-node` now supports systemd Credentials (refer to
<https://systemd.io/CREDENTIALS> for more information) to load <https://systemd.io/CREDENTIALS> for more information) to load:
the secret key, in addition to the commandline argument 1. The secret key, in addition to the commandline argument
`--secret` (higher priority than the credential) and the `--secret` (higher priority than the credential) and the
configuration file (lower priority than the credential). configuration file (lower priority than the credential).
The identifier of the credential is "xyz.radicle.node.secret". 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 ## Fixed Bugs

View File

@ -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", "Version {} ({})", env!("RADICLE_VERSION"), env!("GIT_HEAD"));
log::info!(target: "node", "Unlocking node keystore.."); 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; let secret_path = options.secret;