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

difftreelog

source

cmds/install-secrets/src/main.rs7.1 KiBsourcehistory
1use std::{2	collections::{BTreeMap, HashMap},3	fs::{self, File},4	io::{self, Cursor, Read, Write},5	iter,6	os::unix::prelude::PermissionsExt,7	path::{Path, PathBuf},8	str::{from_utf8, FromStr},9};1011use age::{12	ssh::{Identity as SshIdentity, Recipient as SshRecipient},13	Decryptor, Encryptor, Identity, Recipient,14};15use anyhow::{anyhow, bail, ensure, Context, Result};16use clap::Parser;17use fleet_shared::SecretData;18use nix::unistd::{chown, Group, User};19use serde::Deserialize;20use tracing::{error, info_span};21use tracing_subscriber::{filter::LevelFilter, EnvFilter};2223#[derive(Parser)]24#[clap(author)]25enum Opts {26	/// Install secrets from json specification27	Install { data: PathBuf },28	/// Reencrypt secret using host key, outputting in fleet encoded string29	Reencrypt {30		#[clap(long)]31		secret: SecretData,32		#[clap(long)]33		targets: Vec<String>,34	},35	/// Decrypt secret using host key, outputting in fleet encoded string36	Decrypt {37		#[clap(long)]38		secret: SecretData,39		/// Shoult decoded output be printed as plaintext, instead of z85?40		#[clap(long)]41		plaintext: bool,42	},43}4445#[derive(Deserialize)]46#[serde(rename_all = "camelCase")]47struct Part {48	raw: SecretData,49	path: PathBuf,50	stable_path: PathBuf,51}5253#[derive(Deserialize)]54#[serde(rename_all = "camelCase")]55struct DataItem {56	group: String,57	mode: String,58	owner: String,59	root_path: Option<PathBuf>,6061	#[serde(flatten)]62	parts: BTreeMap<String, Part>,63}6465type Data = HashMap<String, DataItem>;6667fn decrypt(input: &SecretData, identity: &dyn Identity) -> Result<Vec<u8>> {68	ensure!(input.encrypted, "passed data is not encrypted!");69	let mut input = Cursor::new(&input.data);70	let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;71	let decryptor = match decryptor {72		Decryptor::Recipients(r) => r,73		Decryptor::Passphrase(_) => bail!("should be recipients"),74	};75	let mut decryptor = decryptor76		.decrypt(iter::once(identity as &dyn age::Identity))77		.context("failed to decrypt, wrong key?")?;7879	let mut decrypted = Vec::new();80	decryptor81		.read_to_end(&mut decrypted)82		.context("failed to decrypt")?;83	Ok(decrypted)84}85fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretData> {86	let recipients = targets87		.into_iter()88		.map(|t| {89			SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))90		})91		.collect::<Result<Vec<SshRecipient>>>()?;92	let recipients = recipients93		.into_iter()94		.map(|v| Box::new(v) as Box<dyn Recipient + Send>)95		.collect::<Vec<_>>();96	let mut encrypted = vec![];97	let mut encryptor = Encryptor::with_recipients(recipients)98		.expect("recipients provided")99		.wrap_output(&mut encrypted)100		.expect("constructor should not fail");101	io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");102	encryptor.finish().context("failed to finish encryption")?;103	Ok(SecretData {104		data: encrypted,105		encrypted: true,106	})107}108109fn init_part(identity: &dyn Identity, item: &DataItem, value: &Part) -> Result<()> {110	let stable_dir = value.stable_path.parent().expect("not root");111112	// Right now stable & non-stable data are both located in this dir.113	std::fs::create_dir_all(stable_dir)?;114115	let mut stable_temp =116		tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;117	let mut hashed = File::create(&value.path)?;118119	let private = value.raw.encrypted;120	let data = if private {121		decrypt(&value.raw, identity)?122	} else {123		value.raw.data.to_owned()124	};125126	hashed.write_all(&data)?;127	hashed.flush()?;128	stable_temp.write_all(&data)?;129	stable_temp.flush()?;130131	let mode = if private {132		fs::Permissions::from_mode(133			u32::from_str_radix(&item.mode, 8).context("failed to parse mode as octal")?,134		)135	} else {136		fs::Permissions::from_mode(0o444)137	};138	fs::set_permissions(stable_temp.path(), mode.clone()).context("stable temp mode")?;139	fs::set_permissions(&value.path, mode).context("hashed mode")?;140141	// Files are initially owned by root, thus making set mode first inaccessible to user, and then142	// altering user/group.143	if private {144		let user = User::from_name(&item.owner)145			.context("failed to get user")?146			.ok_or_else(|| anyhow!("user not found"))?;147		let group = Group::from_name(&item.group)148			.context("failed to get group")?149			.ok_or_else(|| anyhow!("group not found"))?;150151		chown(stable_temp.path(), Some(user.uid), Some(group.gid))152			.context("failed to apply user/group")?;153		chown(&value.path, Some(user.uid), Some(group.gid))154			.context("failed to apply user/group")?;155	}156157	stable_temp158		.persist(&value.stable_path)159		.context("stable persist")?;160	Ok(())161}162163fn init_secret(identity: &age::ssh::Identity, value: &DataItem) -> Result<()> {164	if let Some(root_path) = &value.root_path {165		if !fs::metadata(root_path).map(|m| m.is_dir()).unwrap_or(false) {166			fs::create_dir(root_path).context("failed to create secret directory")?;167		}168	}169	let mut errored = false;170	for (part_id, part) in value.parts.iter() {171		let _span = info_span!("part", part_id = part_id);172		if let Err(e) = init_part(identity, value, part) {173			error!("failed to init part {part_id}: {e}");174			errored = true;175		}176	}177178	ensure!(!errored, "some secret parts have failed to initialize");179	Ok(())180}181182fn host_identity() -> anyhow::Result<SshIdentity> {183	let identity = SshIdentity::from_buffer(184		&mut Cursor::new(185			fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,186		),187		None,188	)189	.context("failed to parse identity")?;190	Ok(identity)191}192193fn install(data: &Path) -> anyhow::Result<()> {194	let data = fs::read(data).context("failed to read secrets data")?;195	let data_str = from_utf8(&data).context("failed to read data to string")?;196	let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;197198	if !fs::metadata("/run/secrets")199		.map(|m| m.is_dir())200		.unwrap_or(false)201	{202		fs::create_dir("/run/secrets").context("failed to create secrets directory")?;203	}204205	let identity = host_identity()?;206207	let mut failed = false;208	for (name, value) in data {209		let _span = info_span!("init", name = name);210		if let Err(e) = init_secret(&identity, &value) {211			error!("secret failed to initialize: {e}");212			failed = true;213		}214	}215	if failed {216		bail!("one or more secrets failed");217	}218219	Ok(())220}221222fn main() -> anyhow::Result<()> {223	tracing_subscriber::fmt()224		.with_env_filter(225			EnvFilter::builder()226				.with_default_directive(LevelFilter::INFO.into())227				.from_env_lossy(),228		)229		.without_time()230		.with_target(false)231		.init();232233	let opts = Opts::parse();234235	match opts {236		Opts::Install { data } => install(&data),237		Opts::Reencrypt { secret, targets } => {238			let identity = host_identity()?;239			let decrypted = decrypt(&secret, &identity).context("during decryption")?;240			let encrypted = encrypt(&decrypted, targets).context("during re-encryption")?;241242			println!("{encrypted}");243			Ok(())244		}245		Opts::Decrypt { secret, plaintext } => {246			let identity = host_identity()?;247			let decrypted = decrypt(&secret, &identity).context("during decryption")?;248249			if plaintext {250				let s = String::from_utf8(decrypted).context("output is not utf8")?;251				print!("{s}");252			} else {253				println!(254					"{}",255					SecretData {256						data: decrypted,257						encrypted: false258					}259				);260			}261			Ok(())262		}263	}264}