git.delta.rocks / jrsonnet / refs/commits / 67bf612dbfd3

difftreelog

refactor use generator helper for built-in secret generators

Yaroslav Bolyukin2024-06-28parent: #b56a5a3.patch.diff
in: trunk

11 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -808,10 +808,12 @@
 dependencies = [
  "age",
  "anyhow",
+ "base64 0.22.1",
  "clap",
  "ed25519-dalek",
  "fleet-shared",
  "rand",
+ "x25519-dalek",
 ]
 
 [[package]]
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -40,9 +40,6 @@
 		/// Secret public part
 		#[clap(long)]
 		public: Option<String>,
-		/// How to name public secret part
-		#[clap(long, default_value = "public")]
-		public_name: String,
 		/// Load public part from specified file
 		#[clap(long)]
 		public_file: Option<PathBuf>,
@@ -55,14 +52,19 @@
 		#[clap(long)]
 		re_add: bool,
 
-		#[clap(default_value = "secret")]
-		part_name: String,
+		/// How to name public secret part
+		#[clap(long, short = 'p', default_value = "public")]
+		public_part: String,
+		/// How to name private secret part
+		#[clap(short = 's', long, default_value = "secret")]
+		part: String,
 	},
 	/// Add secret, data should be provided in stdin
 	Add {
 		/// Secret name
 		name: String,
-		/// Secret owners
+		/// Secret owner
+		#[clap(short = 'm', long)]
 		machine: String,
 		/// Override secret if already present
 		#[clap(long)]
@@ -70,41 +72,41 @@
 		/// Secret public part
 		#[clap(long)]
 		public: Option<String>,
-		/// How to name public secret part
-		#[clap(long, default_value = "public")]
-		public_name: String,
 		/// Load public part from specified file
 		#[clap(long)]
 		public_file: Option<PathBuf>,
 
-		#[clap(default_value = "secret")]
-		part_name: String,
+		/// How to name public secret part
+		#[clap(short = 'p', long, default_value = "public")]
+		public_part: String,
+		/// How to name private secret part
+		#[clap(short = 's', long, default_value = "secret")]
+		part: String,
 	},
 	/// Read secret from remote host, requires sudo on said host
 	Read {
 		name: String,
+		#[clap(short = 'm', long)]
 		machine: String,
 
-		#[clap(default_value = "secret")]
-		part_name: String,
+		/// Which private secret part to read
+		#[clap(short = 'p', long, default_value = "secret")]
+		part: String,
 	},
 	UpdateShared {
 		name: String,
 
-		#[clap(long)]
-		machines: Option<Vec<String>>,
+		#[clap(short = 'm', long)]
+		machine: Option<Vec<String>>,
 
 		#[clap(long)]
-		add_machines: Vec<String>,
+		add_machine: Vec<String>,
 		#[clap(long)]
-		remove_machines: Vec<String>,
+		remove_machine: Vec<String>,
 
 		/// Which host should we use to decrypt
 		#[clap(long)]
 		prefer_identities: Vec<String>,
-
-		#[clap(default_value = "secret")]
-		part_name: String,
 	},
 	Regenerate {
 		/// Which host should we use to decrypt, in case if reencryption is required, without
@@ -115,13 +117,15 @@
 	List {},
 	Edit {
 		name: String,
+		#[clap(short = 'm', long)]
 		machine: String,
-
-		#[clap(default_value = "secret")]
-		part: String,
 
 		#[clap(long)]
 		add: bool,
+
+		/// Which private secret part to read
+		#[clap(short = 'p', long, default_value = "secret")]
+		part: String,
 	},
 }
 
@@ -220,21 +224,18 @@
 	};
 	let on_pkgs = host.pkgs().await?;
 	let call_package = nix_go!(on_pkgs.callPackage);
-	let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);
+	let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);
 
 	let mut recipients = Vec::new();
 	for owner in owners {
 		let key = config.key(owner).await?;
 		recipients.push(key);
 	}
-	let encrypt = nix_go!(mk_encrypt_secret(Obj {
+	let generators = nix_go!(mk_secret_generators(Obj {
 		recipients: { recipients },
 	}));
 
-	let generator = nix_go!(call_package(generator)(Obj {
-		encrypt,
-		// rustfmt_please_newline
-	}));
+	let generator = nix_go!(call_package(generator)(generators));
 
 	let generator = generator.build().await?;
 	let generator = generator
@@ -305,6 +306,7 @@
 	}
 	let default_pkgs = &config.default_pkgs;
 	let default_call_package = nix_go!(default_pkgs.callPackage);
+	let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);
 	// Generators provide additional information in passthru, to access
 	// passthru we should call generator, but information about where this generator is supposed to build
 	// is located in passthru... Thus evaluating generator on host.
@@ -313,10 +315,10 @@
 	//
 	// I don't want to make modules always responsible for additional secret data anyway,
 	// so it should be in derivation, and not in the secret data itself.
-	let default_generator = nix_go!(default_call_package(generator)(Obj {
-		encrypt: { "exit 1" },
-		// rustfmt_please_newline
+	let generators = nix_go!(default_mk_secret_generators(Obj {
+		recipients: { <Vec<String>>::new() },
 	}));
+	let default_generator = nix_go!(default_call_package(generator)(generators));
 
 	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);
 
@@ -442,11 +444,11 @@
 				name,
 				force,
 				public,
-				public_name,
+				public_part: public_name,
 				public_file,
 				expires_at,
 				re_add,
-				part_name,
+				part: part_name,
 			} => {
 				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
 
@@ -500,9 +502,9 @@
 				name,
 				force,
 				public,
-				public_name,
+				public_part: public_name,
 				public_file,
-				part_name,
+				part: part_name,
 			} => {
 				if config.has_secret(&machine, &name) && !force {
 					bail!("secret already defined");
@@ -535,7 +537,7 @@
 			Secret::Read {
 				name,
 				machine,
-				part_name,
+				part: part_name,
 			} => {
 				let secret = config.host_secret(&machine, &name)?;
 				let Some(secret) = secret.parts.get(&part_name) else {
@@ -552,25 +554,24 @@
 			}
 			Secret::UpdateShared {
 				name,
-				machines,
-				add_machines,
-				remove_machines,
+				machine,
+				add_machine,
+				remove_machine,
 				prefer_identities,
-				part_name,
 			} => {
 				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
 
 				let secret = config.shared_secret(&name)?;
-				if secret.secret.parts.get(&part_name).is_none() {
+				if secret.secret.parts.values().all(|v| !v.raw.encrypted) {
 					bail!("no secret");
 				}
 
 				let initial_machines = secret.owners.clone();
 				let target_machines = parse_machines(
 					initial_machines.clone(),
-					machines,
-					add_machines,
-					remove_machines,
+					machine,
+					add_machine,
+					remove_machine,
 				)?;
 
 				if target_machines.is_empty() {
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -95,7 +95,7 @@
 		let out = cmd.run_string().await?;
 		let mut lines = out.split('\n');
 		if let Some(last) = lines.next_back() {
-			ensure!(last == "", "output of ls should end with newline");
+			ensure!(last.is_empty(), "output of ls should end with newline");
 		}
 		Ok(lines.map(ToOwned::to_owned).collect())
 	}
modifiedcmds/generator-helper/Cargo.tomldiffbeforeafterboth
--- a/cmds/generator-helper/Cargo.toml
+++ b/cmds/generator-helper/Cargo.toml
@@ -6,7 +6,9 @@
 [dependencies]
 age.workspace = true
 anyhow.workspace = true
+base64 = "0.22.1"
 clap.workspace = true
 ed25519-dalek = { version = "2.1", features = ["rand_core"] }
 fleet-shared.workspace = true
 rand = "0.8.5"
+x25519-dalek = "2.0.1"
modifiedcmds/generator-helper/src/main.rsdiffbeforeafterboth
before · cmds/generator-helper/src/main.rs
1use std::{2	fs,3	io::{self, stdout, Cursor, Read, Write},4	path::PathBuf,5	str::FromStr,6};78use age::Recipient;9use anyhow::{anyhow, bail, ensure, Context, Result};10use clap::Parser;11use ed25519_dalek::SigningKey;12use fleet_shared::SecretData;13use rand::{14	distributions::{Alphanumeric, DistString, Distribution, Uniform},15	rngs::OsRng,16	thread_rng, Rng,17};1819fn write_output(out: &str, data: impl AsRef<[u8]>, stdout_marker: &mut bool) -> Result<()> {20	let data = data.as_ref();21	if out == "-" {22		let mut stdout = stdout();23		if *stdout_marker {24			stdout.write_all(&[b'\n'])?;25		}26		*stdout_marker = true;27		stdout.write_all(data)?;28	} else {29		fs::write(out, data)?;30	};31	Ok(())32}3334#[derive(Parser)]35enum Generate {36	/// Generate public, private keys without wrapping, in standard ed25519 schema37	/// (64 bytes private (due to merge with private), 32 bytes public)38	Ed25519 {39		public: String,40		private: String,41		/// Private key should be just the private key (32 bytes), not standard private+public.42		#[arg(long)]43		no_embed_public: bool,44	},45	Password {46		output: String,47		size: usize,48		#[arg(long, short = 'n')]49		no_symbols: bool,50	},51}5253#[derive(Parser)]54enum Opts {55	/// Encode public part from stdin.56	Public {57		#[arg(long)]58		allow_empty: bool,59	},60	/// Encrypt private part from stdin.61	Private {62		#[arg(long)]63		allow_empty: bool,64		#[arg(short = 'r')]65		recipient: Vec<String>,66	},67	/// Generate keys in well-known schemas.68	///69	/// Note that this command is only intended to be used in fleet secret generator,70	/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.71	#[command(subcommand)]72	Generate(Generate),73	// Generate {74	// 	kind: GenerateKind,75	// 	/// Different generators generate different number of files, you need to specify number of outputs corresponding to the generator.76	// 	#[arg(short = 'o')]77	// 	outputs: Vec<String>,78	// },79}8081fn parse_stdin() -> Result<Option<Vec<u8>>> {82	let mut input = vec![];83	io::stdin().read_to_end(&mut input)?;84	if input.is_empty() {85		Ok(None)86	} else {87		Ok(Some(input))88	}89}90pub fn encrypt_secret_data(91	recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,92	data: Vec<u8>,93) -> Option<SecretData> {94	let mut encrypted = vec![];95	let recipients = recipients96		.into_iter()97		.map(|v| Box::new(v) as Box<dyn Recipient + Send>)98		.collect::<Vec<_>>();99	let mut encryptor = age::Encryptor::with_recipients(recipients)?100		.wrap_output(&mut encrypted)101		.expect("in memory write");102	io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");103	encryptor.finish().expect("in memory flush");104	Some(SecretData {105		data: encrypted,106		encrypted: true,107	})108}109110fn main() -> Result<()> {111	let opts = Opts::parse();112	// Assumed to be secure, seeded from secure OsRng+reseeded.113	let mut rng = thread_rng();114115	match opts {116		Opts::Public { allow_empty } => {117			let stdin = parse_stdin()?;118			if stdin.is_none() && !allow_empty {119				bail!("empty stdin input is not allowed unless --allow-empty is set");120			}121			let stdin = stdin.unwrap_or_default();122			io::stdout().write_all(123				SecretData {124					data: stdin,125					encrypted: false,126				}127				.to_string()128				.as_bytes(),129			)?;130		}131		Opts::Private {132			allow_empty,133			recipient,134		} => {135			let stdin = parse_stdin()?;136			if stdin.is_none() && !allow_empty {137				bail!("empty stdin input is not allowed unless --allow-empty is set");138			}139			let stdin = stdin.unwrap_or_default();140			if recipient.is_empty() {141				bail!("recipient list is empty");142			}143			let out = encrypt_secret_data(144				recipient145					.into_iter()146					.map(|r| age::ssh::Recipient::from_str(&r))147					.collect::<Result<Vec<age::ssh::Recipient>, age::ssh::ParseRecipientKeyError>>()148					.map_err(|e| anyhow!("parse recipients: {e:?}"))?,149				stdin,150			)151			.expect("got recipients");152			io::stdout().write_all(out.to_string().as_bytes())?;153		}154		Opts::Generate(gen) => {155			let mut stdout_marker: bool = false;156			match gen {157				Generate::Ed25519 {158					public,159					private,160					no_embed_public,161				} => {162					let key = SigningKey::generate(&mut rng).to_keypair_bytes();163164					write_output(&public, &key[32..], &mut stdout_marker).context("public")?;165					write_output(166						&private,167						&key[..{168							if no_embed_public {169								32170							} else {171								64172							}173						}],174						&mut stdout_marker,175					)176					.context("private")?;177				}178				Generate::Password {179					size,180					no_symbols,181					output,182				} => {183					ensure!(184						size >= 6,185						"misconfiguration? password is shorter than 6 chars"186					);187					let out = if no_symbols {188						Alphanumeric.sample_string(&mut rng, size)189					} else {190						// Alphabet of Alphanumberic + symbols191						const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";192						let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());193						(0..size)194							.map(|_| uniform.sample(&mut rng))195							.map(|i| GEN_ASCII_SYMBOLS[i] as char)196							.collect::<String>()197					};198					write_output(&output, out, &mut stdout_marker)?;199				}200			}201		}202	}203	Ok(())204}
after · cmds/generator-helper/src/main.rs
1use std::{2	env,3	fs::{File, OpenOptions},4	io::{copy, Read, Write},5	str::FromStr,6};78use age::{9	ssh::{ParseRecipientKeyError, Recipient as SshRecipient},10	Encryptor, Recipient,11};12use anyhow::{anyhow, bail, ensure, Context, Result};13use clap::{Parser, ValueEnum};14use fleet_shared::SecretData;15use rand::{16	distributions::{Alphanumeric, DistString, Distribution, Uniform},17	thread_rng,18};1920fn write_output_file(out: &str) -> Result<File> {21	let file = OpenOptions::new()22		.create_new(true)23		.write(true)24		.open(out)25		.with_context(|| format!("failed to open output {out:?}"))?;26	Ok(file)27}28fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> {29	let mut output = write_output_file(out)?;3031	let mut data = Vec::new();32	copy(&mut input, &mut wrap_encoder(&mut data, encoding))?;3334	output.write_all(35		SecretData {36			data,37			encrypted: false,38		}39		.to_string()40		.as_bytes(),41	)?;42	Ok(())43}44fn write_private(45	identities: &Identities,46	out: &str,47	mut input: impl Read,48	encoding: OutputEncoding,49) -> Result<()> {50	let mut output = write_output_file(out)?;51	let encryptor = make_encryptor(identities)?;5253	let mut data = Vec::new();54	{55		let mut encrypted_writer = encryptor.wrap_output(&mut data)?;56		copy(57			&mut input,58			&mut wrap_encoder(&mut encrypted_writer, encoding),59		)?;60		encrypted_writer.finish()?;61	};6263	output.write_all(64		SecretData {65			data,66			encrypted: true,67		}68		.to_string()69		.as_bytes(),70	)?;71	Ok(())72}7374type Identities = Vec<SshRecipient>;75fn load_identities() -> Result<Identities> {76	let list = env::var("GENERATOR_HELPER_IDENTITIES");77	let list = match list {78		Ok(v) => v,79		Err(env::VarError::NotPresent) => {80			bail!("gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities");81		}82		Err(e) => bail!("somehow, identities list is not utf-8: {e}"),83	};84	let list = list.trim();85	ensure!(!list.is_empty(), "no identities passed, can't encrypt data");86	list.lines()87		.map(age::ssh::Recipient::from_str)88		.collect::<Result<Identities, ParseRecipientKeyError>>()89		.map_err(|e| anyhow!("parse recipients: {e:?}"))90}91fn make_encryptor(r: &Identities) -> Result<Encryptor> {92	Ok(Encryptor::with_recipients(93		r.iter()94			.map(|v| {95				let coerced: Box<dyn Recipient + Send> = Box::new(v.clone());96				coerced97			})98			.collect(),99	)100	.expect("list is not empty"))101}102fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {103	fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {104		Box::new(w)105	}106	match encoding {107		OutputEncoding::Raw => coerce(w),108		OutputEncoding::Base64 => {109			use base64::engine::general_purpose::STANDARD;110			let writer = base64::write::EncoderWriter::new(w, &STANDARD);111			coerce(writer)112		}113	}114}115116#[derive(Clone, Copy, ValueEnum, Default)]117enum OutputEncoding {118	/// Do not encode data, store as is.119	#[default]120	Raw,121	/// Encode as base64 (with padding).122	Base64,123}124125#[derive(Parser)]126enum Generate {127	/// Generate public, private keys without wrapping, in standard ed25519 schema128	/// (64 bytes private (due to merge with private), 32 bytes public)129	Ed25519 {130		#[arg(long, short = 'p')]131		public: String,132		#[arg(long, short = 's')]133		private: String,134		/// Private key should be just the private key (32 bytes), not standard private+public.135		#[arg(long)]136		no_embed_public: bool,137		#[arg(long, short = 'e', value_enum, default_value_t)]138		encoding: OutputEncoding,139	},140	/// Generate public, private keys without wrapping, in standard x25519 schema141	/// (32 bytes private, 32 bytes public)142	X25519 {143		#[arg(long, short = 'p')]144		public: String,145		#[arg(long, short = 's')]146		private: String,147		#[arg(long, short = 'e', value_enum, default_value_t)]148		encoding: OutputEncoding,149	},150	Password {151		#[arg(long, short = 'o')]152		output: String,153		#[arg(long)]154		size: usize,155		#[arg(long, short = 'n')]156		no_symbols: bool,157		#[arg(long, short = 'e', value_enum, default_value_t)]158		encoding: OutputEncoding,159	},160}161162#[derive(Parser)]163enum Opts {164	/// Encode public part from stdin.165	Public {166		#[arg(long, short = 'o')]167		output: String,168		#[arg(long, short = 'e', value_enum, default_value_t)]169		encoding: OutputEncoding,170	},171	/// Encrypt private part from stdin.172	Private {173		#[arg(long, short = 'o')]174		output: String,175		#[arg(long, short = 'e', value_enum, default_value_t)]176		encoding: OutputEncoding,177	},178	/// Generate keys in well-known schemas.179	///180	/// Note that this command is only intended to be used in fleet secret generator,181	/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.182	#[command(subcommand)]183	Generate(Generate),184}185186fn main() -> Result<()> {187	let opts = Opts::parse();188	// Assumed to be secure, seeded from secure OsRng+reseeded.189	let mut rng = thread_rng();190191	match opts {192		Opts::Public { output, encoding } => {193			write_public(&output, std::io::stdin(), encoding)?;194		}195		Opts::Private { output, encoding } => {196			let recipients = load_identities()?;197			write_private(&recipients, &output, std::io::stdin(), encoding)?;198		}199		Opts::Generate(gen) => {200			match gen {201				Generate::Ed25519 {202					public,203					private,204					no_embed_public,205					encoding,206				} => {207					let recipients = load_identities()?;208					let key = ed25519_dalek::SigningKey::generate(&mut rng).to_keypair_bytes();209					write_public(&public, &key[32..], encoding)?;210					write_private(211						&recipients,212						&private,213						&key[..{214							if no_embed_public {215								32216							} else {217								64218							}219						}],220						encoding,221					)?;222				}223				Generate::X25519 {224					public,225					private,226					encoding,227				} => {228					let recipients = load_identities()?;229					let key = x25519_dalek::StaticSecret::random_from_rng(rng);230					let public_key: x25519_dalek::PublicKey = (&key).into();231					write_public(&public, public_key.as_bytes().as_slice(), encoding)?;232					write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;233				}234				Generate::Password {235					size,236					no_symbols,237					output,238					encoding,239				} => {240					ensure!(241						size >= 6,242						"misconfiguration? password is shorter than 6 chars"243					);244					let recipients = load_identities()?;245					let out = if no_symbols {246						Alphanumeric.sample_string(&mut rng, size)247					} else {248						// Alphabet of Alphanumberic + symbols249						const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";250						let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());251						(0..size)252							.map(|_| uniform.sample(&mut rng))253							.map(|i| GEN_ASCII_SYMBOLS[i] as char)254							.collect::<String>()255					};256					write_private(&recipients, &output, out.as_bytes(), encoding)?;257				}258			}259		}260	}261	Ok(())262}
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -67,6 +67,7 @@
       perSystem = {
         config,
         system,
+        pkgs,
         ...
       }: let
         # Can also be built for darwin, through it is not usual to deploy nixos systems from macos machines.
@@ -75,14 +76,14 @@
         # It is not possible to deploy any host from armv6/armv7 hardware, and I don't think it even makes sense.
         deployerSystems = ["aarch64-linux" "x86_64-linux"];
         deployerSystem = builtins.elem system deployerSystems;
-        pkgs = import nixpkgs {
-          inherit system;
-          overlays = [(rust-overlay.overlays.default)];
-        };
         lib = pkgs.lib;
         rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
         craneLib = (crane.mkLib pkgs).overrideToolchain rust;
       in {
+        _module.args.pkgs = import nixpkgs {
+          inherit system;
+          overlays = [(rust-overlay.overlays.default)];
+        };
         # Reference fleet package should be built with nightly rust, specified in rust-toolchain.toml.
         packages = lib.mkIf deployerSystem (let
           packages = import ./pkgs {
modifiedlib/fleetLib.nixdiffbeforeafterboth
--- a/lib/fleetLib.nix
+++ b/lib/fleetLib.nix
@@ -42,23 +42,46 @@
 
   mkPassword = {size ? 32}: {
     coreutils,
-    encrypt,
     mkSecretGenerator,
+    ...
   }:
     mkSecretGenerator {
       script = ''
         mkdir $out
+        gh generate password -o $out/secret --size ${toString size}
+      '';
+    };
 
-        ${coreutils}/bin/tr -dc 'A-Za-z0-9!?%=' < /dev/random \
-          | ${coreutils}/bin/head -c ${toString size} \
-          | ${encrypt} > $out/secret
+  mkEd25519 = {
+    noEmbedPublic ? false,
+    encoding ? null,
+  }: {mkSecretGenerator, ...}:
+    mkSecretGenerator {
+      script = ''
+        mkdir $out
+        gh generate ed25519 -p $out/public -s $out/secret \
+          ${lib.optionalString noEmbedPublic "--no-embed-public"} \
+          ${lib.optionalString (encoding != null) "--encoding=${encoding}"}
       '';
     };
 
+  mkGarage = {}: mkEd25519 {noEmbedPublic = true;};
+
+  mkX25519 = {encoding ? null}: {mkSecretGenerator, ...}:
+    mkSecretGenerator {
+      script = ''
+        mkdir $out
+        gh generate x25519 -p $out/public -s $out/secret \
+          ${lib.optionalString (encoding != null) "--encoding=${encoding}"}
+      '';
+    };
+
+  mkWireguard = {}: mkX25519 {encoding = "base64";};
+
   mkRsa = {size ? 4096}: {
     openssl,
-    encrypt,
     mkSecretGenerator,
+    ...
   }:
     mkSecretGenerator {
       script = ''
@@ -67,8 +90,8 @@
         ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
         ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
 
-        sudo cat rsa_private.key | ${encrypt} > $out/secret
-        sudo cat rsa_public.key > $out/public
+        cat rsa_private.key | gh private -o $out/secret
+        cat rsa_public.key | gh public -o $out/public
       '';
     };
 }
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -130,85 +130,81 @@
     overlays = [
       (final: prev: let
         lib = final.lib;
-        inherit (lib) strings concatMap;
-        inherit (strings) escapeShellArgs;
+        inherit (lib) strings;
+        inherit (strings) concatStringsSep;
       in {
-        mkEncryptSecret = {
-          rage ? prev.rage,
-          recipients,
-        }:
-          prev.writeShellScript "encryptor" ''
-            #!/bin/sh
-            exec ${rage}/bin/rage ${escapeShellArgs (concatMap (r: ["-r" r]) recipients)} -e "$@"
-          '';
-        # TODO: Move to fleet
-        # TODO: Merge both generators to one with consistent options syntax?
-        # Impure generator is built on local machine, then built closure is copied to remote machine,
-        # and then it is ran in inpure context, so that this generator may access HSMs and other things.
-        mkImpureSecretGenerator = {
-          script,
-          # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
-          # (Some secrets-encryption-in-git/managed PKI solution is expected)
-          impureOn ? null,
-        }:
-          (prev.writeShellScript "impureGenerator.sh" ''
-            #!/bin/sh
-            set -eu
+        mkSecretGenerators = {recipients}: rec {
+          # TODO: Merge both generators to one with consistent options syntax?
+          # Impure generator is built on local machine, then built closure is copied to remote machine,
+          # and then it is ran in inpure context, so that this generator may access HSMs and other things.
+          mkImpureSecretGenerator = {
+            script,
+            # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
+            # (Some secrets-encryption-in-git/managed PKI solution is expected)
+            impureOn ? null,
+          }:
+            (prev.writeShellScript "impureGenerator.sh" ''
+              #!/bin/sh
+              set -eu
+
+              export GENERATOR_HELPER_IDENTITIES="${concatStringsSep "\n" recipients}";
+              export PATH=${final.fleet-generator-helper}/bin:$PATH
 
-            # TODO: Provide tempdir from outside, to make it securely erasurable as needed?
-            tmp=$(mktemp -d)
-            cd $tmp
-            # cd /var/empty
+              # TODO: Provide tempdir from outside, to make it securely erasurable as needed?
+              tmp=$(mktemp -d)
+              cd $tmp
+              # cd /var/empty
 
-            created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
+              created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
 
-            ${script}
+              ${script}
 
-            if ! test -d $out; then
-              echo "impure generator script did not produce expected \$out output"
-              exit 1
-            fi
+              if ! test -d $out; then
+                echo "impure generator script did not produce expected \$out output"
+                exit 1
+              fi
 
-            echo -n $created_at > $out/created_at
-            echo -n SUCCESS > $out/marker
-          '')
-          .overrideAttrs (old: {
-            passthru = {
-              inherit impureOn;
-              generatorKind = "impure";
-            };
-          });
-        # Pure generators are disabled for now
-        mkSecretGenerator = {script}: final.mkImpureSecretGenerator {inherit script;};
+              echo -n $created_at > $out/created_at
+              echo -n SUCCESS > $out/marker
+            '')
+            .overrideAttrs (old: {
+              passthru = {
+                inherit impureOn;
+                generatorKind = "impure";
+              };
+            });
+          # Pure generators are disabled for now
+          mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;};
 
-        # TODO: Implement consistent naming
-        # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...
-        # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.
-        # mkSecretGenerator = {script}:
-        #   (prev.writeShellScript "generator.sh" ''
-        #     #!/bin/sh
-        #     set -eu
-        #     # TODO: make nix daemon build secret, not just the script.
-        #     cd /var/empty
-        #
-        #     created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
-        #
-        #     ${script}
-        #     if ! test -d $out; then
-        #       echo "impure generator script did not produce expected \$out output"
-        #       exit 1
-        #     fi
-        #
-        #     echo -n $created_at > $out/created_at
-        #     echo -n SUCCESS > $out/marker
-        #   '')
-        #   .overrideAttrs (old: {
-        #     passthru = {
-        #       generatorKind = "pure";
-        #     };
-        #     # TODO: make nix daemon build secret, not just the script.
-        #     # __impure = true;
-        #   });
+          # TODO: Implement consistent naming
+          # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...
+          # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.
+          # mkSecretGenerator = {script}:
+          #   (prev.writeShellScript "generator.sh" ''
+          #     #!/bin/sh
+          #     set -eu
+          #     # TODO: make nix daemon build secret, not just the script.
+          #     cd /var/empty
+          #
+          #     created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
+          #
+          #     ${script}
+          #     if ! test -d $out; then
+          #       echo "impure generator script did not produce expected \$out output"
+          #       exit 1
+          #     fi
+          #
+          #     echo -n $created_at > $out/created_at
+          #     echo -n SUCCESS > $out/marker
+          #   '')
+          #   .overrideAttrs (old: {
+          #     passthru = {
+          #       generatorKind = "pure";
+          #     };
+          #     # TODO: make nix daemon build secret, not just the script.
+          #     # __impure = true;
+          #   });
+        };
       })
     ];
   };
modifiedpkgs/default.nixdiffbeforeafterboth
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -2,6 +2,7 @@
   callPackage,
   craneLib,
 }: {
+  fleet = callPackage ./fleet.nix {inherit craneLib;};
   fleet-install-secrets = callPackage ./fleet-install-secrets.nix {inherit craneLib;};
-  fleet = callPackage ./fleet.nix {inherit craneLib;};
+  fleet-generator-helper = callPackage ./fleet-generator-helper.nix {inherit craneLib;};
 }
addedpkgs/fleet-generator-helper.nixdiffbeforeafterboth
--- /dev/null
+++ b/pkgs/fleet-generator-helper.nix
@@ -0,0 +1,13 @@
+{craneLib}:
+craneLib.buildPackage rec {
+  pname = "fleet-generator-helper";
+
+  src = craneLib.cleanCargoSource (craneLib.path ../.);
+  strictDeps = true;
+
+  cargoExtraArgs = "--locked -p ${pname}";
+
+  postInstall = ''
+    ln -s $out/bin/${pname} $out/bin/gh
+  '';
+}
deletedpkgs/generator-helper.nixdiffbeforeafterboth
--- a/pkgs/generator-helper.nix
+++ /dev/null
@@ -1,13 +0,0 @@
-{craneLib}:
-craneLib.buildPackage rec {
-  pname = "fleet-generator-helper";
-
-  src = craneLib.cleanCargoSource (craneLib.path ../.);
-  strictDeps = true;
-
-  cargoExtraArgs = "--locked -p ${pname}";
-
-  postInstall = ''
-    mv bin/${pname} bin/genhelper
-  '';
-}