From 16a01c19ddca28d1f5a92cd9422af01dcdf48207 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Wed, 21 Jul 2021 11:59:54 +0200 Subject: [PATCH] support new ENV vars to get secret values through a file or a command We want to allow passing a secret not only directly through the environment value, but also indirectly through a file path, an open file descriptor or a command that can write it to standard out. Signed-off-by: Thomas Lamprecht --- pbs-client/src/tools/key_source.rs | 9 +-- pbs-client/src/tools/mod.rs | 90 +++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/pbs-client/src/tools/key_source.rs b/pbs-client/src/tools/key_source.rs index 6dade75e..a3a2bf1a 100644 --- a/pbs-client/src/tools/key_source.rs +++ b/pbs-client/src/tools/key_source.rs @@ -343,13 +343,8 @@ pub(crate) unsafe fn set_test_default_master_pubkey(value: Result pub fn get_encryption_key_password() -> Result, Error> { // fixme: implement other input methods - use std::env::VarError::*; - match std::env::var("PBS_ENCRYPTION_PASSWORD") { - Ok(p) => return Ok(p.as_bytes().to_vec()), - Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"), - Err(NotPresent) => { - // Try another method - } + if let Some(password) = super::get_secret_from_env("PBS_ENCRYPTION_PASSWORD")? { + return Ok(password.as_bytes().to_vec()); } // 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 7b932b63..2c1c6e6c 100644 --- a/pbs-client/src/tools/mod.rs +++ b/pbs-client/src/tools/mod.rs @@ -1,5 +1,10 @@ //! Shared tools useful for common CLI clients. use std::collections::HashMap; +use std::fs::File; +use std::os::unix::io::FromRawFd; +use std::env::VarError::{NotUnicode, NotPresent}; +use std::io::{BufReader, BufRead}; +use std::process::Command; use anyhow::{bail, format_err, Context, Error}; use serde_json::{json, Value}; @@ -7,6 +12,7 @@ use xdg::BaseDirectories; use proxmox::{ api::schema::*, + api::cli::shellword_split, tools::fs::file_get_json, }; @@ -32,6 +38,80 @@ pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must .default(4096) .schema(); +/// Helper to read a secret through a environment variable (ENV). +/// +/// Tries the following variable names in order and returns the value +/// it will resolve for the first defined one: +/// +/// BASE_NAME => use value from ENV(BASE_NAME) directly as secret +/// BASE_NAME_FD => read the secret from the specified file descriptor +/// BASE_NAME_FILE => read the secret from the specified file name +/// BASE_NAME_CMD => read the secret from specified command first line of output on stdout +/// +/// Only return the first line of data (without CRLF). +pub fn get_secret_from_env(base_name: &str) -> Result, Error> { + + let firstline = |data: String| -> String { + match data.lines().next() { + Some(line) => line.to_string(), + None => String::new(), + } + }; + + let firstline_file = |file: &mut File| -> Result { + let reader = BufReader::new(file); + match reader.lines().next() { + Some(Ok(line)) => Ok(line), + Some(Err(err)) => Err(err.into()), + None => Ok(String::new()), + } + }; + + match std::env::var(base_name) { + Ok(p) => return Ok(Some(firstline(p))), + Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", base_name)), + Err(NotPresent) => {}, + }; + + let env_name = format!("{}_FD", base_name); + match std::env::var(&env_name) { + Ok(fd_str) => { + let fd: i32 = fd_str.parse() + .map_err(|err| format_err!("unable to parse file descriptor in ENV({}): {}", env_name, err))?; + let mut file = unsafe { File::from_raw_fd(fd) }; + return Ok(Some(firstline_file(&mut file)?)); + } + Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)), + Err(NotPresent) => {}, + } + + let env_name = format!("{}_FILE", base_name); + match std::env::var(&env_name) { + Ok(filename) => { + let mut file = std::fs::File::open(filename) + .map_err(|err| format_err!("unable to open file in ENV({}): {}", env_name, err))?; + return Ok(Some(firstline_file(&mut file)?)); + } + Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)), + Err(NotPresent) => {}, + } + + let env_name = format!("{}_CMD", base_name); + match std::env::var(&env_name) { + Ok(ref command) => { + let args = shellword_split(command)?; + let mut command = Command::new(&args[0]); + command.args(&args[1..]); + let output = pbs_tools::run_command(command, None)?; + return Ok(Some(firstline(output))); + } + Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", env_name)), + Err(NotPresent) => {}, + } + + Ok(None) +} + pub fn get_default_repository() -> Option { std::env::var("PBS_REPOSITORY").ok() } @@ -64,13 +144,7 @@ pub fn connect(repo: &BackupRepository) -> Result { fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result { let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); - use std::env::VarError::*; - let password = match std::env::var(ENV_VAR_PBS_PASSWORD) { - Ok(p) => Some(p), - Err(NotUnicode(_)) => bail!(format!("{} contains bad characters", ENV_VAR_PBS_PASSWORD)), - Err(NotPresent) => None, - }; - + let password = get_secret_from_env(ENV_VAR_PBS_PASSWORD)?; let options = HttpClientOptions::new_interactive(password, fingerprint); HttpClient::new(server, port, auth_id, options) @@ -80,7 +154,7 @@ fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result Value { let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok(); - let password = std::env::var(ENV_VAR_PBS_PASSWORD).ok(); + let password = get_secret_from_env(ENV_VAR_PBS_PASSWORD).unwrap_or(None); // ticket cache, but no questions asked let options = HttpClientOptions::new_interactive(password, fingerprint)