From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <pbs-devel-bounces@lists.proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9])
	by lore.proxmox.com (Postfix) with ESMTPS id 430801FF15C
	for <inbox@lore.proxmox.com>; Wed, 26 Mar 2025 11:10:48 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
	by firstgate.proxmox.com (Proxmox) with ESMTP id BB72A333EA;
	Wed, 26 Mar 2025 11:10:43 +0100 (CET)
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;
 d=fu-berlin.de; s=fub01; h=In-Reply-To:Content-Transfer-Encoding:Content-Type
 :MIME-Version:References:Reply-To:Message-ID:Subject:Cc:To:From:Date:Sender:
 Content-ID:Content-Description:Resent-Date:Resent-From:Resent-Sender:
 Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:
 List-Subscribe:List-Post:List-Owner:List-Archive;
 bh=US67lrPOIj68Vm43IDA7i0bLf+lJETh8bEm7FpENzn0=; t=1742983841; x=1743588641; 
 b=JxVTD2UQLgbFIYzJ3VoHM+XfgocZb8H7DG2ajxvBReJ4JLrd8OZyx2CJ6LR24SA7sqHb2IX/FeY
 8d4QPV4+ihGIEoQxCzuWoiyWMtFye42GSaV75q5AMkwKvIf8SYVbfQqat9vS5Xp0gA6dvLIV5rFVg
 /l+gX8Qt9IHpMS+YZsS7PhaKYHYUA+hDDOisadbWdMZL2CujmmKx3raUDAV0MtKP24OuOoSHpEJex
 HZb4j4cGwDFnYX3PLVzn6/PrN3zPWpGKE14/fab0VI+Lez8x6yIrqLPcR3REkrSqfziFXC61OCOVK
 lZjto6pvUu/4SRj6c6QHbmh8InAKb02poPEA==;
Date: Wed, 26 Mar 2025 11:06:30 +0100
From: =?iso-8859-1?Q?J=F6rg?= Behrmann <proxmox@behrmj87m.dialup.fu-berlin.de>
To: Maximiliano Sandoval <m.sandoval@proxmox.com>
Message-ID: <Z-PRpqR2dS9Q_Edv@physik.fu-berlin.de>
References: <20250324123542.3248210-1-m.sandoval@proxmox.com>
MIME-Version: 1.0
Content-Disposition: inline
In-Reply-To: <20250324123542.3248210-1-m.sandoval@proxmox.com>
X-Original-Sender: proxmox@behrmj87m.dialup.fu-berlin.de
X-Originating-IP: 160.45.66.12
X-ZEDAT-Hint: PO
X-SPAM-LEVEL: Spam detection results:  0
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DKIM_SIGNED               0.1 Message has a DKIM or DK signature,
 not necessarily valid
 DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature
 DMARC_PASS               -0.1 DMARC pass policy
 RCVD_IN_DNSWL_MED        -2.3 Sender listed at https://www.dnswl.org/,
 medium trust RCVD_IN_MSPIKE_H3       0.001 Good reputation (+3)
 RCVD_IN_MSPIKE_WL       0.001 Mailspike good senders
 SPF_HELO_PASS          -0.001 SPF: HELO matches SPF record
 SPF_PASS               -0.001 SPF: sender matches SPF record
Subject: Re: [pbs-devel] [PATCH backup] pbs-client: read credentials from
 $CREDENTIALS_DIRECTORY
X-BeenThere: pbs-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox Backup Server development discussion
 <pbs-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/>
List-Post: <mailto:pbs-devel@lists.proxmox.com>
List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe>
Reply-To: zedv@physik.fu-berlin.de,
 Proxmox Backup Server development discussion
 <pbs-devel@lists.proxmox.com>
Cc: pbs-devel@lists.proxmox.com
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
Errors-To: pbs-devel-bounces@lists.proxmox.com
Sender: "pbs-devel" <pbs-devel-bounces@lists.proxmox.com>

Hi!

Sorry to chime in from the sidelines, I just saw this patch set, which make=
s me
very happy.

It would be great if everything that can be set via envvar could be set, via
credential as well, most importantly I am thinking about the repository, si=
nce
systemd can accept credentials passed in via smbios type 11 strings.

There is a bug report against the PVE web UI open to be able to set arbitra=
ry
key pars for that [1]. This would allow to configure the pbs client for a VM
directly from the PVE web UI.

Another comment further down inline.

Thanks for this works!

best regards,
J=F6rg Behrmann

[1] https://bugzilla.proxmox.com/show_bug.cgi?id=3D5601

On Mon, Mar 24, 2025 at 01:35:42PM +0100, Maximiliano Sandoval wrote:
> Allows to load credentials passed down by systemd. A possible use-case
> is safely storing the server's password in a file encrypted by the
> systems TPM, e.g. via
> =

> ```
> systemd-ask-password -n | systemd-creds encrypt --name=3Dpbs-password - m=
y-api-token.cred
> ```
> =

> which then can be used via
> =

> ```
> systemd-run --pipe --wait --property=3DLoadCredentialEncrypted=3Dpbs-pass=
word:my-api-token.cred \
> proxmox-backup-client ...
> ```
> =

> or from inside a service.
> =

> Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
> ---
>  pbs-client/src/tools/key_source.rs |  4 +-
>  pbs-client/src/tools/mod.rs        | 81 ++++++++++++++++++++++++++++--
>  2 files changed, 80 insertions(+), 5 deletions(-)
> =

> diff --git a/pbs-client/src/tools/key_source.rs b/pbs-client/src/tools/ke=
y_source.rs
> index 7968e0c2a..94b86e8b6 100644
> --- a/pbs-client/src/tools/key_source.rs
> +++ b/pbs-client/src/tools/key_source.rs
> @@ -345,8 +345,8 @@ pub(crate) unsafe fn set_test_default_master_pubkey(v=
alue: Result<Option<Vec<u8>
>  pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> {
>      // fixme: implement other input methods
>  =

> -    if let Some(password) =3D super::get_secret_from_env("PBS_ENCRYPTION=
_PASSWORD")? {
> -        return Ok(password.as_bytes().to_vec());
> +    if let Some(password) =3D super::get_encryption_password()? {
> +        return Ok(password);
>      }
>  =

>      // If we're on a TTY, query the user for a password
> diff --git a/pbs-client/src/tools/mod.rs b/pbs-client/src/tools/mod.rs
> index 8068dc004..8b0392744 100644
> --- a/pbs-client/src/tools/mod.rs
> +++ b/pbs-client/src/tools/mod.rs
> @@ -2,7 +2,7 @@
>  use std::collections::HashMap;
>  use std::env::VarError::{NotPresent, NotUnicode};
>  use std::fs::File;
> -use std::io::{BufRead, BufReader};
> +use std::io::{BufRead, BufReader, Read};
>  use std::os::unix::fs::OpenOptionsExt;
>  use std::os::unix::io::FromRawFd;
>  use std::process::Command;
> @@ -28,6 +28,16 @@ pub mod key_source;
>  =

>  const ENV_VAR_PBS_FINGERPRINT: &str =3D "PBS_FINGERPRINT";
>  const ENV_VAR_PBS_PASSWORD: &str =3D "PBS_PASSWORD";
> +const ENV_VAR_PBS_ENCRYPTION_PASSWORD: &str =3D "PBS_ENCRYPTION_PASSWORD=
";
> +
> +/// Directory with system [credential]s. See systemd-creds(1).
> +///
> +/// [credential]: https://systemd.io/CREDENTIALS/
> +const CREDENTIALS_DIRECTORY: &str =3D "CREDENTIALS_DIRECTORY";
> +/// Credential name of the encryption password.
> +const CRED_PBS_ENCRYPTION_PASSWORD: &str =3D "pbs-encryption-password";
> +/// Credential name of the the password.
> +const CRED_PBS_PASSWORD: &str =3D "pbs-password";
>  =

>  pub const REPO_URL_SCHEMA: Schema =3D StringSchema::new("Repository URL.=
")
>      .format(&BACKUP_REPO_URL)
> @@ -40,6 +50,43 @@ pub const CHUNK_SIZE_SCHEMA: Schema =3D 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>> {
> +    // 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) =3D std::env::var_os(CREDENTIALS_DIRECTORY) =
else {
> +            return Ok(None);
> +        };
> +        let path =3D std::path::Path::new(&creds_dir).join(cred_name);
> +
> +        let mut file =3D File::open(&path)?;
> +        let mut buffer =3D vec![];
> +
> +        // We read the whole contents without a BufRead. As per
> +        // systemd-creds(1): Credentials are limited-size binary or text=
ual
> +        // 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() !=3D std::io::ErrorKind::NotFo=
und)
> +            {
> +                proxmox_log::error!("{err:#}")
> +            }
> +        })
> +        .ok()
> +        .flatten()
> +}
> +
>  /// 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) -> Resul=
t<Option<String>, Error> {
>      Ok(None)
>  }
>  =

> +/// Gets the backup server's password.
> +///
> +/// We first try reading from the `PBS_PASSWORD` environment variable, t=
hen we
> +/// try reading from the `pbs-password` [credential]. We ignore errors o=
n the
> +/// latter.
> +///
> +/// [credential]: https://systemd.io/CREDENTIALS/
> +pub fn get_password() -> Result<Option<String>, Error> {
> +    let password =3D get_secret_from_env(ENV_VAR_PBS_PASSWORD)?.or_else(=
|| {
> +        get_credential(CRED_PBS_PASSWORD).and_then(|bytes| String::from_=
utf8(bytes).ok())
> +    });
> +    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`

The name for credentials is pretty free form and dashes are the correct
namespacing for systemd units, but when grepping for SetCredential=3D and
LoadCredential=3D in the systemd codebase, you'll see that dots are the more
idiomatic way of namespacing credentials, this idiom has also spread to oth=
er
projects, e.g. util-linux (see the credential support in agetty).

The better name would therefore be pbs.encryption.password or
pbs.encryption-password or pbs.encryption_password, depending on what exact=
ly
the namespacing you want to communicate is.

> +/// [credential]. We ignore errors on the latter.
> +///
> +/// [credential]: https://systemd.io/CREDENTIALS/
> +pub fn get_encryption_password() -> Result<Option<Vec<u8>>, Error> {
> +    let password =3D get_secret_from_env(ENV_VAR_PBS_ENCRYPTION_PASSWORD=
)?
> +        .map(String::into_bytes)
> +        .or_else(|| get_credential(CRED_PBS_ENCRYPTION_PASSWORD));
> +    Ok(password)
> +}
> +
>  pub fn get_default_repository() -> Option<String> {
>      std::env::var("PBS_REPOSITORY").ok()
>  }
> @@ -181,7 +256,7 @@ fn connect_do(
>  ) -> Result<HttpClient, Error> {
>      let fingerprint =3D std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
>  =

> -    let password =3D get_secret_from_env(ENV_VAR_PBS_PASSWORD)?;
> +    let password =3D get_password()?;
>      let options =3D HttpClientOptions::new_interactive(password, fingerp=
rint).rate_limit(rate_limit);
>  =

>      HttpClient::new(server, port, auth_id, options)
> @@ -190,7 +265,7 @@ fn connect_do(
>  /// like get, but simply ignore errors and return Null instead
>  pub async fn try_get(repo: &BackupRepository, url: &str) -> Value {
>      let fingerprint =3D std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
> -    let password =3D get_secret_from_env(ENV_VAR_PBS_PASSWORD).unwrap_or=
(None);
> +    let password =3D get_password().unwrap_or(None);
>  =

>      // ticket cache, but no questions asked
>      let options =3D HttpClientOptions::new_interactive(password, fingerp=
rint).interactive(false);
> -- =

> 2.39.5
> =

> =

> =

> _______________________________________________
> pbs-devel mailing list
> pbs-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel

_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel