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

difftreelog

refactor new secrets structure

Yaroslav Bolyukin2022-08-28parent: #aeb19ed.patch.diff
in: trunk

2 files changed

modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
before · cmds/install-secrets/src/main.rs
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)]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::parse();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}
modifiednixos/secrets.nixdiffbeforeafterboth
--- a/nixos/secrets.nix
+++ b/nixos/secrets.nix
@@ -5,9 +5,15 @@
 let
   sysConfig = config;
   secretType = types.submodule ({ config, ... }: {
-    config = {
-      path = mkOptionDefault "/run/secrets/${config._module.args.name}";
-      publicPath = mkOptionDefault (pkgs.writeText "pub-${config._module.args.name}" config.public);
+    config = rec {
+      path = warn "use .stableSecretPath instead of .path (at config.secrets.${config._module.args.name})" stableSecretPath;
+      stableSecretPath = mkOptionDefault "/run/secrets/secret-stable-${config._module.args.name}";
+      secretPath = mkOptionDefault "/run/secrets/secret-${config.secretHash}-${config._module.args.name}";
+      secretHash = mkOptionDefault (if config.secret != null then (builtins.hashString "sha1" config.secret) else "<missingno>");
+
+      stablePublicPath = mkOptionDefault "/run/secrets/public-stable-${config._module.args.name}";
+      publicPath = mkOptionDefault "/run/secrets/public-${config.publicHash}-${config._module.args.name}";
+      publicHash = mkOptionDefault (if config.public != null then (builtins.hashString "sha1" config.public) else "<missingno>");
     };
     options = {
       public = mkOption {
@@ -36,12 +42,40 @@
         default = sysConfig.users.users.${config.owner}.group;
       };
 
+      secretHash = mkOption {
+        type = types.str;
+        description = "Hash of .secret field";
+      };
+      publicHash = mkOption {
+        type = types.str;
+        description = "Hash of .public field";
+      };
+
       path = mkOption {
         type = types.str;
         description = "Path to the decrypted secret";
       };
+      stableSecretPath = mkOption {
+        type = types.str;
+        description = """
+          Use this, if target process supports re-reading of secret from disk,
+          and doesn't needs to be restarted when secret is updated in file
+        """;
+      };
+      secretPath = mkOption {
+        type = types.str;
+        description = "Path to decrypted secret, suffixed with contents hash";
+      };
+
+      stablePublicPath = mkOption {
+        type = types.str;
+        description = """
+          Use this, if target process supports re-reading of secret from disk,
+          and doesn't needs to be restarted when secret is updated in file
+        """;
+      };
       publicPath = mkOption {
-        type = types.package;
+        type = types.str;
         description = "Path to the public part of secret";
       };
     };