git.delta.rocks / jrsonnet / refs/commits / 590ae3fadf8f

difftreelog

source

cmds/install-secrets/src/main.rs4.8 KiBsourcehistory
1use age::Decryptor;2use anyhow::{anyhow, bail, Context, Result};3use clap::Parser;4use log::{error, warn};5use nix::fcntl::{renameat2, RenameFlags};6use nix::sys::stat::Mode;7use nix::unistd::{chown, Group, User};8use serde::{Deserialize, Deserializer};9use std::fs::{self, DirBuilder};10use std::io::{self, Cursor, Read};11use std::iter;12use std::os::unix::prelude::PermissionsExt;13use std::str::from_utf8;14use std::{15	collections::HashMap,16	os::unix::fs::DirBuilderExt,17	path::{Path, PathBuf},18};1920#[derive(Parser)]21#[clap(author)]22struct Opts {23	data: PathBuf,24}2526#[derive(Deserialize)]27#[serde(rename_all = "camelCase")]28struct DataItem {29	group: String,30	mode: String,31	owner: String,3233	#[serde(deserialize_with = "from_z85")]34	secret: Option<Vec<u8>>,35	public: String,3637	secret_hash: String,38	public_path: String,39}4041fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>42where43	D: Deserializer<'de>,44{45	use serde::de::Error;46	if let Some(v) = <Option<String>>::deserialize(deserializer)? {47		Ok(Some(48			z85::decode(&v).map_err(|err| Error::custom(err.to_string()))?,49		))50	} else {51		Ok(None)52	}53}5455type Data = HashMap<String, DataItem>;5657fn init_secret(58	identity: &age::ssh::Identity,59	dir: &Path,60	name: &str,61	value: DataItem,62) -> Result<()> {63	if value.secret.is_none() {64		return Ok(());65	}66	let secret = value.secret.as_ref().unwrap();6768	let mut path = dir.to_path_buf();69	path.push(name);70	if path.strip_prefix(&dir).is_err() {71		bail!("found escaping name");72	}7374	let secret_dir = path75		.parent()76		.expect("path is in tempdir, so it should have parent");7778	if secret_dir != dir {79		DirBuilder::new()80			.recursive(true)81			// o: xrw82			// g: xr83			// a: xr84			.mode(0o755)85			.create(86				path.parent()87					.expect("path is in tempdir, so it should have parent"),88			)89			.context("failed to create secret directory")?;90	}9192	let mode = Mode::from_bits(93		u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,94	)95	.context("failed to parse mode")?;96	let user = User::from_name(&value.owner)97		.context("failed to get user")?98		.ok_or_else(|| anyhow!("user not found"))?;99	let group = Group::from_name(&value.group)100		.context("failed to get group")?101		.ok_or_else(|| anyhow!("group not found"))?;102	let mut tempfile =103		tempfile::NamedTempFile::new_in(secret_dir).context("failed to create tempfile")?;104	// File is owned by root, and only root can modify it105106	let decrypted = {107		let mut input = Cursor::new(&secret);108		let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;109		let decryptor = match decryptor {110			Decryptor::Recipients(r) => r,111			Decryptor::Passphrase(_) => bail!("should be recipients"),112		};113		let mut decryptor = decryptor114			.decrypt(iter::once(identity as &dyn age::Identity))115			.context("failed to decrypt, wrong key?")?;116117		let mut decrypted = Vec::new();118		decryptor119			.read_to_end(&mut decrypted)120			.context("failed to decrypt")?;121		decrypted122	};123124	io::copy(&mut Cursor::new(decrypted), &mut tempfile)125		.context("failed to write decrypted file")?;126127	// Make file owned by specified user and group, then change mode128	chown(tempfile.path(), Some(user.uid), Some(group.gid))129		.context("failed to apply user/group")?;130	fs::set_permissions(tempfile.path(), fs::Permissions::from_mode(mode.bits())).unwrap();131	tempfile.persist(path).context("failed to persist")?;132133	Ok(())134}135136fn main() -> anyhow::Result<()> {137	env_logger::Builder::new()138		.filter_level(log::LevelFilter::Info)139		.init();140141	let opts = Opts::parse();142	let data = fs::read(&opts.data).context("failed to read secrets data")?;143	let data_str = from_utf8(&data).context("failed to read data to string")?;144	let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;145146	let tempdir = tempfile::tempdir_in("/run/").context("failed to create secrets tempdir")?;147148	let identity = age::ssh::Identity::from_buffer(149		&mut Cursor::new(150			fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,151		),152		None,153	)154	.context("failed to parse identity")?;155156	let mut failed = false;157	for (name, value) in data {158		if let Err(e) = init_secret(&identity, tempdir.path(), &name, value) {159			error!(160				"{:?}",161				e.context(format!("failed to initialize secret {}", name))162			);163			failed = true;164		}165	}166	if failed {167		bail!("one or more secrets failed");168	}169170	if fs::metadata("/run/secrets")171		.map(|m| m.is_dir())172		.unwrap_or(false)173	{174		// Already linked175		renameat2(176			None,177			tempdir.path(),178			None,179			"/run/secrets",180			RenameFlags::RENAME_EXCHANGE,181		)182		.context("failed to exchange secret directories")?;183		if tempdir.close().is_err() {184			warn!("failed to unlink old secrets");185		}186	} else {187		// Link now188		let persisted = tempdir.into_path();189		fs::rename(&persisted, "/run/secrets").context("failed to link secret directory")?;190	}191	Ok(())192}