git.delta.rocks / jrsonnet / refs/commits / e85b4da8a439

difftreelog

feat add-shared --readd

Yaroslav Bolyukin2023-11-26parent: #97d9be6.patch.diff
in: trunk

1 file changed

modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
after · cmds/fleet/src/cmds/secrets/mod.rs
1use crate::{2	fleetdata::{FleetSecret, FleetSharedSecret},3	host::Config,4};5use anyhow::{bail, ensure, Context, Result};6use chrono::Utc;7use clap::Parser;8use futures::{StreamExt, TryStreamExt};9use owo_colors::OwoColorize;10use std::{11	collections::HashSet,12	io::{self, Cursor, Read},13	path::PathBuf,14};15use tabled::{Table, Tabled};16use tokio::fs::read_to_string;17use tracing::{error, info, info_span, warn};1819#[derive(Parser)]20pub enum Secrets {21	/// Force load keys for all defined hosts22	ForceKeys,23	/// Add secret, data should be provided in stdin24	AddShared {25		/// Secret name26		name: String,27		/// Secret owners28		machines: Vec<String>,29		/// Override secret if already present30		#[clap(long)]31		force: bool,32		#[clap(long)]33		public: Option<String>,34		#[clap(long)]35		public_file: Option<PathBuf>,3637		/// Secret with this name already exists, override its value while keeping the same owners.38		#[clap(long)]39		readd: bool,40	},41	/// Add secret, data should be provided in stdin42	Add {43		/// Secret name44		name: String,45		/// Secret owners46		machine: String,47		/// Override secret if already present48		#[clap(long)]49		force: bool,50		#[clap(long)]51		public: Option<String>,52		#[clap(long)]53		public_file: Option<PathBuf>,54	},55	/// Read secret from remote host, requires sudo on said host56	Read {57		name: String,58		machine: String,59		#[clap(long)]60		plaintext: bool,61	},62	UpdateShared {63		name: String,6465		#[clap(long)]66		machines: Option<Vec<String>>,6768		#[clap(long)]69		add_machines: Vec<String>,70		#[clap(long)]71		remove_machines: Vec<String>,7273		/// Which host should we use to decrypt74		#[clap(long)]75		prefer_identities: Vec<String>,76	},77	Regenerate {78		/// Which host should we use to decrypt, in case if reencryption is required, without79		/// regeneration80		#[clap(long)]81		prefer_identities: Vec<String>,82	},83	List {},84}8586impl Secrets {87	pub async fn run(self, config: &Config) -> Result<()> {88		match self {89			Secrets::ForceKeys => {90				for host in config.list_hosts().await? {91					if config.should_skip(&host.name) {92						continue;93					}94					config.key(&host.name).await?;95				}96			}97			Secrets::AddShared {98				mut machines,99				name,100				force,101				public,102				public_file,103				readd,104			} => {105				let exists = config.has_shared(&name);106				if exists && !force && !readd {107					bail!("secret already defined");108				}109				if readd {110					// Fixme: use clap to limit this usage111					ensure!(!force, "--force and --readd are not compatible");112					ensure!(exists, "secret doesn't exists");113					ensure!(114						machines.is_empty(),115						"you can't use machines argument for --readd"116					);117					let shared = config.shared_secret(&name)?;118					machines = shared.owners;119				}120121				let recipients = futures::stream::iter(machines.iter())122					.then(|m| config.recipient(m))123					.try_collect::<Vec<_>>()124					.await?;125126				let secret = {127					let mut input = vec![];128					io::stdin().read_to_end(&mut input)?;129130					if input.is_empty() {131						input132					} else {133						let mut encrypted = vec![];134						let recipients = recipients135							.iter()136							.cloned()137							.map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)138							.collect();139						let mut encryptor = age::Encryptor::with_recipients(recipients)140							.expect("recipients provided")141							.wrap_output(&mut encrypted)?;142						io::copy(&mut Cursor::new(input), &mut encryptor)?;143						encryptor.finish()?;144						encrypted145					}146				};147				config.replace_shared(148					name,149					FleetSharedSecret {150						owners: machines,151						secret: FleetSecret {152							created_at: Utc::now(),153							expires_at: None,154							secret,155							public: match (public, public_file) {156								(Some(v), None) => Some(v),157								(None, Some(v)) => Some(read_to_string(v).await?),158								(Some(_), Some(_)) => {159									bail!("only public or public_file should be set")160								}161								(None, None) => None,162							},163						},164					},165				);166			}167			Secrets::Add {168				machine,169				name,170				force,171				public,172				public_file,173			} => {174				let recipient = config.recipient(&machine).await?;175176				let secret = {177					let mut input = vec![];178					io::stdin().read_to_end(&mut input)?;179					if input.is_empty() {180						bail!("no data provided")181					}182183					let mut encrypted = vec![];184					let recipient = Box::new(recipient) as Box<dyn age::Recipient + Send>;185					let mut encryptor = age::Encryptor::with_recipients(vec![recipient])186						.expect("recipients provided")187						.wrap_output(&mut encrypted)?;188					io::copy(&mut Cursor::new(input), &mut encryptor)?;189					encryptor.finish()?;190					encrypted191				};192193				if config.has_secret(&machine, &name) && !force {194					bail!("secret already defined");195				}196				config.insert_secret(197					&machine,198					name,199					FleetSecret {200						created_at: Utc::now(),201						expires_at: None,202						secret,203						public: match (public, public_file) {204							(Some(v), None) => Some(v),205							(None, Some(v)) => Some(std::fs::read_to_string(v)?),206							(Some(_), Some(_)) => bail!("only public or public_file should be set"),207							(None, None) => None,208						},209					},210				);211			}212			// TODO: Instead of using sudo, decode secret on remote machine213			#[allow(clippy::await_holding_refcell_ref)]214			Secrets::Read {215				name,216				machine,217				plaintext,218			} => {219				let secret = config.host_secret(&machine, &name)?;220				if secret.secret.is_empty() {221					bail!("no secret {name}");222				}223				let data = config.decrypt_on_host(&machine, secret.secret).await?;224				if plaintext {225					let s = String::from_utf8(data).context("output is not utf8")?;226					print!("{s}");227				} else {228					println!("{}", z85::encode(&data));229				}230			}231			Secrets::UpdateShared {232				name,233				machines,234				mut add_machines,235				mut remove_machines,236				prefer_identities,237			} => {238				if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {239					bail!("no operation");240				}241242				let mut secret = config.shared_secret(&name)?;243				if secret.secret.secret.is_empty() {244					bail!("no secret");245				}246247				let initial_machines = secret.owners.clone();248				let mut target_machines = secret.owners.clone();249				info!("Currently encrypted for {initial_machines:?}");250251				// ensure!(machines.is_some() || !add_machines.is_empty() || )252				if let Some(machines) = machines {253					ensure!(254						add_machines.is_empty() && remove_machines.is_empty(),255						"can't combine --machines and --add-machines/--remove-machines"256					);257					let target = initial_machines.iter().collect::<HashSet<_>>();258					let source = machines.iter().collect::<HashSet<_>>();259					for removed in target.difference(&source) {260						remove_machines.push((*removed).clone());261					}262					for added in source.difference(&target) {263						add_machines.push((*added).clone());264					}265				}266267				for machine in &remove_machines {268					let mut removed = false;269					while let Some(pos) = target_machines.iter().position(|m| m == machine) {270						target_machines.swap_remove(pos);271						removed = true;272					}273					if !removed {274						warn!("secret is not enabled for {machine}");275					}276				}277				for machine in &add_machines {278					if target_machines.iter().any(|m| m == machine) {279						warn!("secret is already added to {machine}");280					} else {281						target_machines.push(machine.to_owned());282					}283				}284				if !remove_machines.is_empty() {285					warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");286				}287288				if target_machines.is_empty() {289					info!("no machines left for secret, removing it");290					config.remove_shared(&name);291					return Ok(());292				}293294				if target_machines == initial_machines {295					warn!("secret owners are already correct");296					return Ok(());297				}298299				let identity_holder = if !prefer_identities.is_empty() {300					prefer_identities301						.iter()302						.find(|i| initial_machines.iter().any(|s| s == *i))303				} else {304					secret.owners.first()305				};306				let Some(identity_holder) = identity_holder else {307					bail!("no available holder found");308				};309				let target_recipients = futures::stream::iter(&target_machines)310					.then(|m| async { config.key(m).await })311					.collect::<Vec<_>>()312					.await;313				let target_recipients =314					target_recipients.into_iter().collect::<Result<Vec<_>>>()?;315316				let encrypted = config317					.reencrypt_on_host(identity_holder, secret.secret.secret, target_recipients)318					.await?;319320				secret.owners = target_machines;321				secret.secret.secret = encrypted;322				config.replace_shared(name, secret);323			}324			Secrets::Regenerate { prefer_identities } => {325				{326					let expected_shared_set = config327						.list_configured_shared()328						.await?329						.into_iter()330						.collect::<HashSet<_>>();331					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();332					for removed in expected_shared_set.difference(&shared_set) {333						error!("secret needs to be generated: {removed}")334					}335				}336				let mut to_remove = Vec::new();337				for name in &config.list_shared() {338					info!("updating secret: {name}");339					let mut data = config.shared_secret(name)?;340					let expected_owners: Vec<String> = config341						.config_field342						.get_json_deep(["sharedSecrets", name, "expectedOwners"])343						.await?;344					if expected_owners.is_empty() {345						warn!("secret was removed from fleet config: {name}, removing from data");346						to_remove.push(name.to_string());347						continue;348					}349					let set = data.owners.iter().collect::<HashSet<_>>();350					let expected_set = expected_owners.iter().collect::<HashSet<_>>();351					let should_remove = set.difference(&expected_set).next().is_some();352					if set != expected_set {353						let owner_dependent: bool = config354							.config_field355							.get_json_deep(["sharedSecrets", name, "ownerDependent"])356							.await?;357						if !owner_dependent {358							warn!("reencrypting secret '{name}' for new owner set");359							// TODO: force regeneration360							if should_remove {361								warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");362							}363364							let identity_holder = if !prefer_identities.is_empty() {365								prefer_identities366									.iter()367									.find(|i| data.owners.iter().any(|s| s == *i))368							} else {369								data.owners.first()370							};371							let Some(identity_holder) = identity_holder else {372								bail!("no available holder found");373							};374375							let target_recipients = futures::stream::iter(&expected_owners)376								.then(|m| async { config.key(m).await })377								.collect::<Vec<_>>()378								.await;379							let target_recipients =380								target_recipients.into_iter().collect::<Result<Vec<_>>>()?;381382							let encrypted = config383								.reencrypt_on_host(384									identity_holder,385									data.secret.secret,386									target_recipients,387								)388								.await?;389390							data.secret.secret = encrypted;391							data.owners = expected_owners;392							config.replace_shared(name.to_owned(), data);393						} else {394							error!("secret '{name}' should be regenerated manually");395						}396					} else {397						info!("secret data is ok")398					}399				}400				for k in to_remove {401					config.remove_shared(&k);402				}403			}404			Secrets::List {} => {405				let _span = info_span!("loading secrets").entered();406				let configured = config.list_configured_shared().await?;407				#[derive(Tabled)]408				struct SecretDisplay {409					#[tabled(rename = "Name")]410					name: String,411					#[tabled(rename = "Owners")]412					owners: String,413				}414				let mut table = vec![];415				for name in configured.iter().cloned() {416					let config = config.clone();417					let expected_owners = config.shared_secret_expected_owners(&name).await?;418					let data = config.shared_secret(&name)?;419					let owners = data420						.owners421						.iter()422						.map(|o| {423							if expected_owners.contains(o) {424								o.green().to_string()425							} else {426								o.red().to_string()427							}428						})429						.collect::<Vec<_>>();430					table.push(SecretDisplay {431						owners: owners.join(", "),432						name,433					})434				}435				info!("loaded\n{}", Table::new(table).to_string())436			}437		}438		Ok(())439	}440}