[pbs-devel] [PATCH backup] pbs-client: read credentials from $CREDENTIALS_DIRECTORY
Maximiliano Sandoval
m.sandoval at proxmox.com
Wed Mar 26 10:41:45 CET 2025
Wolfgang Bumiller <w.bumiller at proxmox.com> writes:
>> const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
>> const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
>> +const ENV_VAR_PBS_ENCRYPTION_PASSWORD: &str = "PBS_ENCRYPTION_PASSWORD";
>> +
>> +/// Directory with system [credential]s. See systemd-creds(1).
>> +///
>> +/// [credential]: https://systemd.io/CREDENTIALS/
>> +const CREDENTIALS_DIRECTORY: &str = "CREDENTIALS_DIRECTORY";
>
> ^ The other env var consts have an `ENV_VAR_` prefix, so we should add
> that here for consistency.
>
>> +/// Credential name of the encryption password.
>> +const CRED_PBS_ENCRYPTION_PASSWORD: &str = "pbs-encryption-password";
>> +/// Credential name of the the password.
>> +const CRED_PBS_PASSWORD: &str = "pbs-password";
>>
>> pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.")
>> .format(&BACKUP_REPO_URL)
>> @@ -40,6 +50,43 @@ pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must
>> .default(4096)
>> .schema();
>>
>> +/// Retrieves a secret stored in a [credential] provided by systemd.
>> +///
>> +/// Returns `None` if the credential does not exist or on errors.
>> +///
>> +/// [credential]: https://systemd.io/CREDENTIALS/
>> +pub fn get_credential(cred_name: &str) -> Option<Vec<u8>> {
>
> I think *failure* to read an *existing* credential should actually be
> propagated up. We don't want to just ignore this.
>
>> + // This is fundamentally a fn that returns a Result, but in practice we
>> + // would ignore errors so we hide the result in the public API.
>> + fn get_credential_inner(cred_name: &str) -> Result<Option<Vec<u8>>, Error> {
>> + let Some(creds_dir) = std::env::var_os(CREDENTIALS_DIRECTORY) else {
>> + return Ok(None);
>> + };
>> + let path = std::path::Path::new(&creds_dir).join(cred_name);
>> +
>> + let mut file = File::open(&path)?;
>> + let mut buffer = vec![];
>> +
>> + // We read the whole contents without a BufRead. As per
>> + // systemd-creds(1): Credentials are limited-size binary or textual
>> + // objects.
>> + file.read_to_end(&mut buffer)?;
>> +
>> + Ok(Some(buffer))
>> + }
>> + get_credential_inner(cred_name)
>> + .inspect_err(|err| {
>> + if err
>> + .downcast_ref::<std::io::Error>()
>> + .is_some_and(|e| e.kind() != std::io::ErrorKind::NotFound)
>> + {
>> + proxmox_log::error!("{err:#}")
>> + }
>> + })
>> + .ok()
>> + .flatten()
>
> I think this entire fn could be reduced to a std::fs::read() call with
> the path included in the logged error, and maybe some more debug-logging
> so that with debug-logging the log doesnt' stay empty on eg. ENOENT.
>
> let Some(creds_dir) = std::env::var_os(ENV_VAR_CREDENTIALS_DIRECTORY) else {
> return Ok(None);
> }
> let path = Path::new(creds_dir).join(cred_name);
> debug!("attempting to use credential {cred_name:?} from {creds_dir:?}");
> match std::fs::read(&path) {
> Ok(data) => Ok(Some(data)),
> Err(err) if err.kind() == io::ErrorKind::NotFound => {
> debug!("no {cred_name:?} credential found in {creds_dir:?}");
> Ok(None)
> }
> Err(err) => Err(err),
> }
>
>> +}
>> +
>> /// Helper to read a secret through a environment variable (ENV).
>> ///
>> /// Tries the following variable names in order and returns the value
>> @@ -118,6 +165,34 @@ pub fn get_secret_from_env(base_name: &str) -> Result<Option<String>, Error> {
>> Ok(None)
>> }
>>
>> +/// Gets the backup server's password.
>> +///
>> +/// We first try reading from the `PBS_PASSWORD` environment variable, then we
>> +/// try reading from the `pbs-password` [credential]. We ignore errors on the
>> +/// latter.
>> +///
>> +/// [credential]: https://systemd.io/CREDENTIALS/
>> +pub fn get_password() -> Result<Option<String>, Error> {
>> + let password = get_secret_from_env(ENV_VAR_PBS_PASSWORD)?.or_else(|| {
>> + get_credential(CRED_PBS_PASSWORD).and_then(|bytes| String::from_utf8(bytes).ok())
>
> A potential UTF-8 error here should be logged here!
>
> I think the function would be a bit more readable with a simple
>
> if let Some(pw) = get_secret_from_env(...)? {
> return Ok(pw);
> }
> if let Some(pw) = get_credential(...)? {
> return String::from_utf8(pw)
> .map_err(|_| format_err!("non-utf8 password credential"));
> }
>
>> + });
>> + Ok(password)
>> +}
>> +
>> +/// Gets an encryption password.
>> +///
>> +/// We first try reading from the `PBS_ENCRYPTION_PASSWORD` environment
>> +/// variable, then we try reading from the `pbs-encryption-password`
>> +/// [credential]. We ignore errors on the latter.
>> +///
>> +/// [credential]: https://systemd.io/CREDENTIALS/
>> +pub fn get_encryption_password() -> Result<Option<Vec<u8>>, Error> {
>> + let password = get_secret_from_env(ENV_VAR_PBS_ENCRYPTION_PASSWORD)?
>> + .map(String::into_bytes)
>> + .or_else(|| get_credential(CRED_PBS_ENCRYPTION_PASSWORD));
>
> This and `get_password` can just use the same helper function. They both
> only support utf-8 strings anyway from what I can tell.
> So this would just be
>
> get_secret_impl(ENV_VAR_PBS_ENCRYPTION_PASSWORD, CRED_PBS_ENCRYPTION_PASSWORD)
> .map(String::into_bytes)
>
I didn't implement this last part, imho the API feels better in this
way.
v2 sent at
https://lore.proxmox.com/pbs-devel/20250326094138.75437-1-m.sandoval@proxmox.com/T/#t.
More information about the pbs-devel
mailing list