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

difftreelog

source

cmds/install-secrets/src/main.rs4.7 KiBsourcehistory
1use age::Decryptor;2use anyhow::{anyhow, bail, Context, Result};3use log::{error, warn};4use nix::fcntl::{renameat2, RenameFlags};5use nix::sys::stat::Mode;6use nix::unistd::{chown, Group, User};7use serde::{Deserialize, Deserializer};8use std::fs::{self, DirBuilder};9use std::io::{self, Cursor, Read};10use std::iter;11use std::os::unix::prelude::PermissionsExt;12use std::str::from_utf8;13use std::{14	collections::HashMap,15	os::unix::fs::DirBuilderExt,16	path::{Path, PathBuf},17};18use structopt::StructOpt;1920#[derive(StructOpt)]21#[structopt(author)]22struct Opts {23	data: PathBuf,24}2526#[derive(Deserialize)]27struct DataItem {28	group: String,29	mode: String,30	owner: String,31	#[serde(deserialize_with = "from_z85")]32	secret: Option<Vec<u8>>,33}3435fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>36where37	D: Deserializer<'de>,38{39	use serde::de::Error;40	if let Some(v) = <Option<String>>::deserialize(deserializer)? {41		Ok(Some(42			z85::decode(&v).map_err(|err| Error::custom(err.to_string()))?,43		))44	} else {45		Ok(None)46	}47}4849type Data = HashMap<String, DataItem>;5051fn init_secret(52	identity: &age::ssh::Identity,53	dir: &Path,54	name: &str,55	value: DataItem,56) -> Result<()> {57	if value.secret.is_none() {58		return Ok(());59	}60	let secret = value.secret.as_ref().unwrap();6162	let mut path = dir.to_path_buf();63	path.push(name);64	if path.strip_prefix(&dir).is_err() {65		bail!("found escaping name");66	}6768	let secret_dir = path69		.parent()70		.expect("path is in tempdir, so it should have parent");7172	if secret_dir != dir {73		DirBuilder::new()74			.recursive(true)75			// o: xrw76			// g: xr77			// a: xr78			.mode(0o755)79			.create(80				path.parent()81					.expect("path is in tempdir, so it should have parent"),82			)83			.context("failed to create secret directory")?;84	}8586	let mode = Mode::from_bits(87		u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,88	)89	.context("failed to parse mode")?;90	let user = User::from_name(&value.owner)91		.context("failed to get user")?92		.ok_or_else(|| anyhow!("user not found"))?;93	let group = Group::from_name(&value.group)94		.context("failed to get group")?95		.ok_or_else(|| anyhow!("group not found"))?;96	let mut tempfile =97		tempfile::NamedTempFile::new_in(secret_dir).context("failed to create tempfile")?;98	// File is owned by root, and only root can modify it99100	let decrypted = {101		let mut input = Cursor::new(&secret);102		let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;103		let decryptor = match decryptor {104			Decryptor::Recipients(r) => r,105			Decryptor::Passphrase(_) => bail!("should be recipients"),106		};107		let mut decryptor = decryptor108			.decrypt(iter::once(identity as &dyn age::Identity))109			.context("failed to decrypt, wrong key?")?;110111		let mut decrypted = Vec::new();112		decryptor113			.read_to_end(&mut decrypted)114			.context("failed to decrypt")?;115		decrypted116	};117118	io::copy(&mut Cursor::new(decrypted), &mut tempfile)119		.context("failed to write decrypted file")?;120121	// Make file owned by specified user and group, then change mode122	chown(tempfile.path(), Some(user.uid), Some(group.gid))123		.context("failed to apply user/group")?;124	fs::set_permissions(tempfile.path(), fs::Permissions::from_mode(mode.bits())).unwrap();125	tempfile.persist(path).context("failed to persist")?;126127	Ok(())128}129130fn main() -> anyhow::Result<()> {131	env_logger::Builder::new()132		.filter_level(log::LevelFilter::Info)133		.init();134135	let opts = Opts::from_args();136	let data = fs::read(&opts.data).context("failed to read secrets data")?;137	let data_str = from_utf8(&data).context("failed to read data to string")?;138	let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;139140	let tempdir = tempfile::tempdir_in("/run/").context("failed to create secrets tempdir")?;141142	let identity = age::ssh::Identity::from_buffer(143		&mut Cursor::new(144			fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,145		),146		None,147	)148	.context("failed to parse identity")?;149150	let mut failed = false;151	for (name, value) in data {152		if let Err(e) = init_secret(&identity, tempdir.path(), &name, value) {153			error!(154				"{:?}",155				e.context(format!("failed to initialize secret {}", name))156			);157			failed = true;158		}159	}160	if failed {161		bail!("one or more secrets failed");162	}163164	if fs::metadata("/run/secrets")165		.map(|m| m.is_dir())166		.unwrap_or(false)167	{168		// Already linked169		renameat2(170			None,171			tempdir.path(),172			None,173			"/run/secrets",174			RenameFlags::RENAME_EXCHANGE,175		)176		.context("failed to exchange secret directories")?;177		if tempdir.close().is_err() {178			warn!("failed to unlink old secrets");179		}180	} else {181		// Link now182		let persisted = tempdir.into_path();183		fs::rename(&persisted, "/run/secrets").context("failed to link secret directory")?;184	}185	Ok(())186}