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}
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}