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

difftreelog

source

cmds/install-secrets/src/main.rs7.8 KiBsourcehistory
1use age::{ssh::Identity as SshIdentity, ssh::Recipient as SshRecipient, Decryptor};2use age::{Encryptor, Identity, Recipient};3use anyhow::{anyhow, bail, Context, Result};4use clap::Parser;5use nix::sys::stat::Mode;6use nix::unistd::{chown, Group, User};7use serde::{Deserialize, Deserializer};8use std::fmt::{self, Display};9use std::fs::{self, File};10use std::io::{self, Cursor, Read, Write};11use std::iter;12use std::os::unix::prelude::PermissionsExt;13use std::path::Path;14use std::str::{from_utf8, FromStr};15use std::{collections::HashMap, path::PathBuf};16use tracing::{error, info, info_span, warn};17use tracing_subscriber::filter::LevelFilter;18use tracing_subscriber::EnvFilter;1920#[derive(Clone, Debug)]21struct SecretWrapper(Vec<u8>);22impl Display for SecretWrapper {23	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {24		let encoded = z85::encode(&self.0);25		write!(f, "{encoded}")26	}27}28impl FromStr for SecretWrapper {29	type Err = z85::DecodeError;3031	fn from_str(s: &str) -> Result<Self, Self::Err> {32		z85::decode(s).map(Self)33	}34}35impl<'de> Deserialize<'de> for SecretWrapper {36	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>37	where38		D: Deserializer<'de>,39	{40		let v = String::deserialize(deserializer)?;41		let de = z85::decode(v).map_err(|err| serde::de::Error::custom(err.to_string()))?;42		Ok(Self(de))43	}44}4546#[derive(Parser)]47#[clap(author)]48enum Opts {49	/// Install secrets from json specification50	Install { data: PathBuf },51	/// Reencrypt secret using host key, outputting in z85 encoded string52	Reencrypt {53		#[clap(long)]54		secret: SecretWrapper,55		#[clap(long)]56		targets: Vec<String>,57	},58	/// Decrypt secret using host key, outputting in z85 encoded string59	Decrypt {60		#[clap(long)]61		secret: SecretWrapper,62		/// Shoult decoded output be printed as plaintext, instead of z85?63		#[clap(long)]64		plaintext: bool,65	},66}6768#[derive(Deserialize)]69#[serde(rename_all = "camelCase")]70struct DataItem {71	group: String,72	mode: String,73	owner: String,7475	secret: Option<SecretWrapper>,76	public: Option<String>,7778	public_path: PathBuf,79	stable_public_path: PathBuf,8081	secret_path: PathBuf,82	stable_secret_path: PathBuf,83}8485type Data = HashMap<String, DataItem>;8687fn decrypt(input: &SecretWrapper, identity: &dyn Identity) -> Result<Vec<u8>> {88	let mut input = Cursor::new(&input.0);89	let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;90	let decryptor = match decryptor {91		Decryptor::Recipients(r) => r,92		Decryptor::Passphrase(_) => bail!("should be recipients"),93	};94	let mut decryptor = decryptor95		.decrypt(iter::once(identity as &dyn age::Identity))96		.context("failed to decrypt, wrong key?")?;9798	let mut decrypted = Vec::new();99	decryptor100		.read_to_end(&mut decrypted)101		.context("failed to decrypt")?;102	Ok(decrypted)103}104fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretWrapper> {105	let recipients = targets106		.into_iter()107		.map(|t| {108			SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))109		})110		.collect::<Result<Vec<SshRecipient>>>()?;111	let recipients = recipients112		.into_iter()113		.map(|v| Box::new(v) as Box<dyn Recipient + Send>)114		.collect::<Vec<_>>();115	let mut encrypted = vec![];116	let mut encryptor = Encryptor::with_recipients(recipients)117		.expect("recipients provided")118		.wrap_output(&mut encrypted)119		.expect("constructor should not fail");120	io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");121	encryptor.finish().context("failed to finish encryption")?;122	Ok(SecretWrapper(encrypted))123}124125fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {126	if let Some(public) = &value.public {127		let mut hashed = File::create(&value.public_path)?;128		let stable_dir = value.stable_public_path.parent().expect("not root");129		let mut stable_temp =130			tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;131		hashed.write_all(public.as_bytes())?;132		stable_temp.write_all(public.as_bytes())?;133		stable_temp.flush()?;134		fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(0o444))135			.context("perm")?;136		fs::set_permissions(&value.public_path, fs::Permissions::from_mode(0o444))137			.context("perm")?;138139		stable_temp140			.persist(value.stable_public_path)141			.context("failed to persist")?;142	}143	if value.secret.is_none() {144		info!("no secret data found");145		return Ok(());146	}147	let secret = value.secret.as_ref().unwrap();148149	let mode = Mode::from_bits(150		u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,151	)152	.context("failed to parse mode")?;153	let user = User::from_name(&value.owner)154		.context("failed to get user")?155		.ok_or_else(|| anyhow!("user not found"))?;156	let group = Group::from_name(&value.group)157		.context("failed to get group")?158		.ok_or_else(|| anyhow!("group not found"))?;159160	let stable_dir = value.stable_secret_path.parent().expect("not root");161	let mut stable_temp =162		tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;163	let mut hashed = File::create(&value.secret_path)?;164165	// File is owned by root, and only root can modify it166	let decrypted = decrypt(secret, identity)?;167	if decrypted.is_empty() {168		warn!("secret is decoded as empty, something is broken?");169	}170171	io::copy(&mut Cursor::new(&decrypted), &mut stable_temp)172		.context("failed to write decrypted file")?;173	io::copy(&mut Cursor::new(decrypted), &mut hashed).context("failed to write decrypted file")?;174175	// Make file owned by specified user and group, then change mode176	chown(stable_temp.path(), Some(user.uid), Some(group.gid))177		.context("failed to apply user/group")?;178	chown(&value.secret_path, Some(user.uid), Some(group.gid))179		.context("failed to apply user/group")?;180	fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(mode.bits())).unwrap();181	fs::set_permissions(&value.secret_path, fs::Permissions::from_mode(mode.bits())).unwrap();182	stable_temp183		.persist(value.stable_secret_path)184		.context("failed to persist")?;185186	Ok(())187}188189fn host_identity() -> anyhow::Result<SshIdentity> {190	let identity = SshIdentity::from_buffer(191		&mut Cursor::new(192			fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,193		),194		None,195	)196	.context("failed to parse identity")?;197	Ok(identity)198}199200fn install(data: &Path) -> anyhow::Result<()> {201	let data = fs::read(data).context("failed to read secrets data")?;202	let data_str = from_utf8(&data).context("failed to read data to string")?;203	let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;204205	if !fs::metadata("/run/secrets")206		.map(|m| m.is_dir())207		.unwrap_or(false)208	{209		fs::create_dir("/run/secrets").context("failed to create secrets directory")?;210	}211212	let identity = host_identity()?;213214	let mut failed = false;215	for (name, value) in data {216		let _span = info_span!("init", name = name);217		if let Err(e) = init_secret(&identity, value) {218			error!("{e}");219			failed = true;220		}221	}222	if failed {223		bail!("one or more secrets failed");224	}225226	Ok(())227}228229fn main() -> anyhow::Result<()> {230	tracing_subscriber::fmt()231		.with_env_filter(232			EnvFilter::builder()233				.with_default_directive(LevelFilter::INFO.into())234				.from_env_lossy(),235		)236		.without_time()237		.with_target(false)238		.init();239240	let opts = Opts::parse();241242	match opts {243		Opts::Install { data } => install(&data),244		Opts::Reencrypt { secret, targets } => {245			let identity = host_identity()?;246			let decrypted = decrypt(&secret, &identity).context("during decryption")?;247			let encrypted = encrypt(&decrypted, targets).context("during re-encryption")?;248249			println!("{encrypted}");250			Ok(())251		}252		Opts::Decrypt { secret, plaintext } => {253			let identity = host_identity()?;254			let decrypted = decrypt(&secret, &identity).context("during decryption")?;255256			if plaintext {257				let s = String::from_utf8(decrypted).context("output is not utf8")?;258				print!("{s}");259			} else {260				println!("{}", SecretWrapper(decrypted));261			}262			Ok(())263		}264	}265}