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
before · 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>,36	},37	/// Add secret, data should be provided in stdin38	Add {39		/// Secret name40		name: String,41		/// Secret owners42		machine: String,43		/// Override secret if already present44		#[clap(long)]45		force: bool,46		#[clap(long)]47		public: Option<String>,48		#[clap(long)]49		public_file: Option<PathBuf>,50	},51	/// Read secret from remote host, requires sudo on said host52	Read {53		name: String,54		machine: String,55		#[clap(long)]56		plaintext: bool,57	},58	UpdateShared {59		name: String,6061		#[clap(long)]62		machines: Option<Vec<String>>,6364		#[clap(long)]65		add_machines: Vec<String>,66		#[clap(long)]67		remove_machines: Vec<String>,6869		/// Which host should we use to decrypt70		#[clap(long)]71		prefer_identities: Vec<String>,72	},73	Regenerate {74		/// Which host should we use to decrypt, in case if reencryption is required, without75		/// regeneration76		#[clap(long)]77		prefer_identities: Vec<String>,78	},79	List {},80}8182impl Secrets {83	pub async fn run(self, config: &Config) -> Result<()> {84		match self {85			Secrets::ForceKeys => {86				for host in config.list_hosts().await? {87					if config.should_skip(&host.name) {88						continue;89					}90					config.key(&host.name).await?;91				}92			}93			Secrets::AddShared {94				machines,95				name,96				force,97				public,98				public_file,99			} => {100				let recipients = futures::stream::iter(machines.iter())101					.then(|m| config.recipient(m))102					.try_collect::<Vec<_>>()103					.await?;104105				let secret = {106					let mut input = vec![];107					io::stdin().read_to_end(&mut input)?;108109					if input.is_empty() {110						input111					} else {112						let mut encrypted = vec![];113						let recipients = recipients114							.iter()115							.cloned()116							.map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)117							.collect();118						let mut encryptor = age::Encryptor::with_recipients(recipients)119							.expect("recipients provided")120							.wrap_output(&mut encrypted)?;121						io::copy(&mut Cursor::new(input), &mut encryptor)?;122						encryptor.finish()?;123						encrypted124					}125				};126127				if config.has_shared(&name) && !force {128					bail!("secret already defined");129				}130				config.replace_shared(131					name,132					FleetSharedSecret {133						owners: machines,134						secret: FleetSecret {135							created_at: Utc::now(),136							expires_at: None,137							secret,138							public: match (public, public_file) {139								(Some(v), None) => Some(v),140								(None, Some(v)) => Some(read_to_string(v).await?),141								(Some(_), Some(_)) => {142									bail!("only public or public_file should be set")143								}144								(None, None) => None,145							},146						},147					},148				);149			}150			Secrets::Add {151				machine,152				name,153				force,154				public,155				public_file,156			} => {157				let recipient = config.recipient(&machine).await?;158159				let secret = {160					let mut input = vec![];161					io::stdin().read_to_end(&mut input)?;162					if input.is_empty() {163						bail!("no data provided")164					}165166					let mut encrypted = vec![];167					let recipient = Box::new(recipient) as Box<dyn age::Recipient + Send>;168					let mut encryptor = age::Encryptor::with_recipients(vec![recipient])169						.expect("recipients provided")170						.wrap_output(&mut encrypted)?;171					io::copy(&mut Cursor::new(input), &mut encryptor)?;172					encryptor.finish()?;173					encrypted174				};175176				if config.has_secret(&machine, &name) && !force {177					bail!("secret already defined");178				}179				config.insert_secret(180					&machine,181					name,182					FleetSecret {183						created_at: Utc::now(),184						expires_at: None,185						secret,186						public: match (public, public_file) {187							(Some(v), None) => Some(v),188							(None, Some(v)) => Some(std::fs::read_to_string(v)?),189							(Some(_), Some(_)) => bail!("only public or public_file should be set"),190							(None, None) => None,191						},192					},193				);194			}195			// TODO: Instead of using sudo, decode secret on remote machine196			#[allow(clippy::await_holding_refcell_ref)]197			Secrets::Read {198				name,199				machine,200				plaintext,201			} => {202				let secret = config.host_secret(&machine, &name)?;203				if secret.secret.is_empty() {204					bail!("no secret {name}");205				}206				let data = config.decrypt_on_host(&machine, secret.secret).await?;207				if plaintext {208					let s = String::from_utf8(data).context("output is not utf8")?;209					print!("{s}");210				} else {211					println!("{}", z85::encode(&data));212				}213			}214			Secrets::UpdateShared {215				name,216				machines,217				mut add_machines,218				mut remove_machines,219				prefer_identities,220			} => {221				if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {222					bail!("no operation");223				}224225				let mut secret = config.shared_secret(&name)?;226				if secret.secret.secret.is_empty() {227					bail!("no secret");228				}229230				let initial_machines = secret.owners.clone();231				let mut target_machines = secret.owners.clone();232				info!("Currently encrypted for {initial_machines:?}");233234				// ensure!(machines.is_some() || !add_machines.is_empty() || )235				if let Some(machines) = machines {236					ensure!(237						add_machines.is_empty() && remove_machines.is_empty(),238						"can't combine --machines and --add-machines/--remove-machines"239					);240					let target = initial_machines.iter().collect::<HashSet<_>>();241					let source = machines.iter().collect::<HashSet<_>>();242					for removed in target.difference(&source) {243						remove_machines.push((*removed).clone());244					}245					for added in source.difference(&target) {246						add_machines.push((*added).clone());247					}248				}249250				for machine in &remove_machines {251					let mut removed = false;252					while let Some(pos) = target_machines.iter().position(|m| m == machine) {253						target_machines.swap_remove(pos);254						removed = true;255					}256					if !removed {257						warn!("secret is not enabled for {machine}");258					}259				}260				for machine in &add_machines {261					if target_machines.iter().any(|m| m == machine) {262						warn!("secret is already added to {machine}");263					} else {264						target_machines.push(machine.to_owned());265					}266				}267				if !remove_machines.is_empty() {268					warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");269				}270271				if target_machines.is_empty() {272					info!("no machines left for secret, removing it");273					config.remove_shared(&name);274					return Ok(());275				}276277				if target_machines == initial_machines {278					warn!("secret owners are already correct");279					return Ok(());280				}281282				let identity_holder = if !prefer_identities.is_empty() {283					prefer_identities284						.iter()285						.find(|i| initial_machines.iter().any(|s| s == *i))286				} else {287					secret.owners.first()288				};289				let Some(identity_holder) = identity_holder else {290					bail!("no available holder found");291				};292				let target_recipients = futures::stream::iter(&target_machines)293					.then(|m| async { config.key(m).await })294					.collect::<Vec<_>>()295					.await;296				let target_recipients =297					target_recipients.into_iter().collect::<Result<Vec<_>>>()?;298299				let encrypted = config300					.reencrypt_on_host(identity_holder, secret.secret.secret, target_recipients)301					.await?;302303				secret.owners = target_machines;304				secret.secret.secret = encrypted;305				config.replace_shared(name, secret);306			}307			Secrets::Regenerate { prefer_identities } => {308				{309					let expected_shared_set = config310						.list_configured_shared()311						.await?312						.into_iter()313						.collect::<HashSet<_>>();314					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();315					for removed in expected_shared_set.difference(&shared_set) {316						error!("secret needs to be generated: {removed}")317					}318				}319				let mut to_remove = Vec::new();320				for name in &config.list_shared() {321					info!("updating secret: {name}");322					let mut data = config.shared_secret(name)?;323					let expected_owners: Vec<String> = config324						.config_field325						.get_json_deep(["sharedSecrets", name, "expectedOwners"])326						.await?;327					if expected_owners.is_empty() {328						warn!("secret was removed from fleet config: {name}, removing from data");329						to_remove.push(name.to_string());330						continue;331					}332					let set = data.owners.iter().collect::<HashSet<_>>();333					let expected_set = expected_owners.iter().collect::<HashSet<_>>();334					let should_remove = set.difference(&expected_set).next().is_some();335					if set != expected_set {336						let owner_dependent: bool = config337							.config_field338							.get_json_deep(["sharedSecrets", name, "ownerDependent"])339							.await?;340						if !owner_dependent {341							warn!("reencrypting secret '{name}' for new owner set");342							// TODO: force regeneration343							if should_remove {344								warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");345							}346347							let identity_holder = if !prefer_identities.is_empty() {348								prefer_identities349									.iter()350									.find(|i| data.owners.iter().any(|s| s == *i))351							} else {352								data.owners.first()353							};354							let Some(identity_holder) = identity_holder else {355								bail!("no available holder found");356							};357358							let target_recipients = futures::stream::iter(&expected_owners)359								.then(|m| async { config.key(m).await })360								.collect::<Vec<_>>()361								.await;362							let target_recipients =363								target_recipients.into_iter().collect::<Result<Vec<_>>>()?;364365							let encrypted = config366								.reencrypt_on_host(367									identity_holder,368									data.secret.secret,369									target_recipients,370								)371								.await?;372373							data.secret.secret = encrypted;374							data.owners = expected_owners;375							config.replace_shared(name.to_owned(), data);376						} else {377							error!("secret '{name}' should be regenerated manually");378						}379					} else {380						info!("secret data is ok")381					}382				}383				for k in to_remove {384					config.remove_shared(&k);385				}386			}387			Secrets::List {} => {388				let _span = info_span!("loading secrets").entered();389				let configured = config.list_configured_shared().await?;390				#[derive(Tabled)]391				struct SecretDisplay {392					#[tabled(rename = "Name")]393					name: String,394					#[tabled(rename = "Owners")]395					owners: String,396				}397				let mut table = vec![];398				for name in configured.iter().cloned() {399					let config = config.clone();400					let expected_owners = config.shared_secret_expected_owners(&name).await?;401					let data = config.shared_secret(&name)?;402					let owners = data403						.owners404						.iter()405						.map(|o| {406							if expected_owners.contains(o) {407								o.green().to_string()408							} else {409								o.red().to_string()410							}411						})412						.collect::<Vec<_>>();413					table.push(SecretDisplay {414						owners: owners.join(", "),415						name,416					})417				}418				info!("loaded\n{}", Table::new(table).to_string())419			}420		}421		Ok(())422	}423}