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
--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -1,52 +1,161 @@
 use std::{
-	fs,
-	io::{self, stdout, Cursor, Read, Write},
-	path::PathBuf,
+	env,
+	fs::{File, OpenOptions},
+	io::{copy, Read, Write},
 	str::FromStr,
 };
 
-use age::Recipient;
+use age::{
+	ssh::{ParseRecipientKeyError, Recipient as SshRecipient},
+	Encryptor, Recipient,
+};
 use anyhow::{anyhow, bail, ensure, Context, Result};
-use clap::Parser;
-use ed25519_dalek::SigningKey;
+use clap::{Parser, ValueEnum};
 use fleet_shared::SecretData;
 use rand::{
 	distributions::{Alphanumeric, DistString, Distribution, Uniform},
-	rngs::OsRng,
-	thread_rng, Rng,
+	thread_rng,
 };
 
-fn write_output(out: &str, data: impl AsRef<[u8]>, stdout_marker: &mut bool) -> Result<()> {
-	let data = data.as_ref();
-	if out == "-" {
-		let mut stdout = stdout();
-		if *stdout_marker {
-			stdout.write_all(&[b'\n'])?;
+fn write_output_file(out: &str) -> Result<File> {
+	let file = OpenOptions::new()
+		.create_new(true)
+		.write(true)
+		.open(out)
+		.with_context(|| format!("failed to open output {out:?}"))?;
+	Ok(file)
+}
+fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> {
+	let mut output = write_output_file(out)?;
+
+	let mut data = Vec::new();
+	copy(&mut input, &mut wrap_encoder(&mut data, encoding))?;
+
+	output.write_all(
+		SecretData {
+			data,
+			encrypted: false,
 		}
-		*stdout_marker = true;
-		stdout.write_all(data)?;
-	} else {
-		fs::write(out, data)?;
+		.to_string()
+		.as_bytes(),
+	)?;
+	Ok(())
+}
+fn write_private(
+	identities: &Identities,
+	out: &str,
+	mut input: impl Read,
+	encoding: OutputEncoding,
+) -> Result<()> {
+	let mut output = write_output_file(out)?;
+	let encryptor = make_encryptor(identities)?;
+
+	let mut data = Vec::new();
+	{
+		let mut encrypted_writer = encryptor.wrap_output(&mut data)?;
+		copy(
+			&mut input,
+			&mut wrap_encoder(&mut encrypted_writer, encoding),
+		)?;
+		encrypted_writer.finish()?;
 	};
+
+	output.write_all(
+		SecretData {
+			data,
+			encrypted: true,
+		}
+		.to_string()
+		.as_bytes(),
+	)?;
 	Ok(())
 }
 
+type Identities = Vec<SshRecipient>;
+fn load_identities() -> Result<Identities> {
+	let list = env::var("GENERATOR_HELPER_IDENTITIES");
+	let list = match list {
+		Ok(v) => v,
+		Err(env::VarError::NotPresent) => {
+			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");
+		}
+		Err(e) => bail!("somehow, identities list is not utf-8: {e}"),
+	};
+	let list = list.trim();
+	ensure!(!list.is_empty(), "no identities passed, can't encrypt data");
+	list.lines()
+		.map(age::ssh::Recipient::from_str)
+		.collect::<Result<Identities, ParseRecipientKeyError>>()
+		.map_err(|e| anyhow!("parse recipients: {e:?}"))
+}
+fn make_encryptor(r: &Identities) -> Result<Encryptor> {
+	Ok(Encryptor::with_recipients(
+		r.iter()
+			.map(|v| {
+				let coerced: Box<dyn Recipient + Send> = Box::new(v.clone());
+				coerced
+			})
+			.collect(),
+	)
+	.expect("list is not empty"))
+}
+fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {
+	fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {
+		Box::new(w)
+	}
+	match encoding {
+		OutputEncoding::Raw => coerce(w),
+		OutputEncoding::Base64 => {
+			use base64::engine::general_purpose::STANDARD;
+			let writer = base64::write::EncoderWriter::new(w, &STANDARD);
+			coerce(writer)
+		}
+	}
+}
+
+#[derive(Clone, Copy, ValueEnum, Default)]
+enum OutputEncoding {
+	/// Do not encode data, store as is.
+	#[default]
+	Raw,
+	/// Encode as base64 (with padding).
+	Base64,
+}
+
 #[derive(Parser)]
 enum Generate {
 	/// Generate public, private keys without wrapping, in standard ed25519 schema
 	/// (64 bytes private (due to merge with private), 32 bytes public)
 	Ed25519 {
+		#[arg(long, short = 'p')]
 		public: String,
+		#[arg(long, short = 's')]
 		private: String,
 		/// Private key should be just the private key (32 bytes), not standard private+public.
 		#[arg(long)]
 		no_embed_public: bool,
+		#[arg(long, short = 'e', value_enum, default_value_t)]
+		encoding: OutputEncoding,
+	},
+	/// Generate public, private keys without wrapping, in standard x25519 schema
+	/// (32 bytes private, 32 bytes public)
+	X25519 {
+		#[arg(long, short = 'p')]
+		public: String,
+		#[arg(long, short = 's')]
+		private: String,
+		#[arg(long, short = 'e', value_enum, default_value_t)]
+		encoding: OutputEncoding,
 	},
 	Password {
+		#[arg(long, short = 'o')]
 		output: String,
+		#[arg(long)]
 		size: usize,
 		#[arg(long, short = 'n')]
 		no_symbols: bool,
+		#[arg(long, short = 'e', value_enum, default_value_t)]
+		encoding: OutputEncoding,
 	},
 }
 
@@ -54,15 +163,17 @@
 enum Opts {
 	/// Encode public part from stdin.
 	Public {
-		#[arg(long)]
-		allow_empty: bool,
+		#[arg(long, short = 'o')]
+		output: String,
+		#[arg(long, short = 'e', value_enum, default_value_t)]
+		encoding: OutputEncoding,
 	},
 	/// Encrypt private part from stdin.
 	Private {
-		#[arg(long)]
-		allow_empty: bool,
-		#[arg(short = 'r')]
-		recipient: Vec<String>,
+		#[arg(long, short = 'o')]
+		output: String,
+		#[arg(long, short = 'e', value_enum, default_value_t)]
+		encoding: OutputEncoding,
 	},
 	/// Generate keys in well-known schemas.
 	///
@@ -70,99 +181,34 @@
 	/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.
 	#[command(subcommand)]
 	Generate(Generate),
-	// Generate {
-	// 	kind: GenerateKind,
-	// 	/// Different generators generate different number of files, you need to specify number of outputs corresponding to the generator.
-	// 	#[arg(short = 'o')]
-	// 	outputs: Vec<String>,
-	// },
 }
 
-fn parse_stdin() -> Result<Option<Vec<u8>>> {
-	let mut input = vec![];
-	io::stdin().read_to_end(&mut input)?;
-	if input.is_empty() {
-		Ok(None)
-	} else {
-		Ok(Some(input))
-	}
-}
-pub fn encrypt_secret_data(
-	recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,
-	data: Vec<u8>,
-) -> Option<SecretData> {
-	let mut encrypted = vec![];
-	let recipients = recipients
-		.into_iter()
-		.map(|v| Box::new(v) as Box<dyn Recipient + Send>)
-		.collect::<Vec<_>>();
-	let mut encryptor = age::Encryptor::with_recipients(recipients)?
-		.wrap_output(&mut encrypted)
-		.expect("in memory write");
-	io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");
-	encryptor.finish().expect("in memory flush");
-	Some(SecretData {
-		data: encrypted,
-		encrypted: true,
-	})
-}
-
 fn main() -> Result<()> {
 	let opts = Opts::parse();
 	// Assumed to be secure, seeded from secure OsRng+reseeded.
 	let mut rng = thread_rng();
 
 	match opts {
-		Opts::Public { allow_empty } => {
-			let stdin = parse_stdin()?;
-			if stdin.is_none() && !allow_empty {
-				bail!("empty stdin input is not allowed unless --allow-empty is set");
-			}
-			let stdin = stdin.unwrap_or_default();
-			io::stdout().write_all(
-				SecretData {
-					data: stdin,
-					encrypted: false,
-				}
-				.to_string()
-				.as_bytes(),
-			)?;
+		Opts::Public { output, encoding } => {
+			write_public(&output, std::io::stdin(), encoding)?;
 		}
-		Opts::Private {
-			allow_empty,
-			recipient,
-		} => {
-			let stdin = parse_stdin()?;
-			if stdin.is_none() && !allow_empty {
-				bail!("empty stdin input is not allowed unless --allow-empty is set");
-			}
-			let stdin = stdin.unwrap_or_default();
-			if recipient.is_empty() {
-				bail!("recipient list is empty");
-			}
-			let out = encrypt_secret_data(
-				recipient
-					.into_iter()
-					.map(|r| age::ssh::Recipient::from_str(&r))
-					.collect::<Result<Vec<age::ssh::Recipient>, age::ssh::ParseRecipientKeyError>>()
-					.map_err(|e| anyhow!("parse recipients: {e:?}"))?,
-				stdin,
-			)
-			.expect("got recipients");
-			io::stdout().write_all(out.to_string().as_bytes())?;
+		Opts::Private { output, encoding } => {
+			let recipients = load_identities()?;
+			write_private(&recipients, &output, std::io::stdin(), encoding)?;
 		}
 		Opts::Generate(gen) => {
-			let mut stdout_marker: bool = false;
 			match gen {
 				Generate::Ed25519 {
 					public,
 					private,
 					no_embed_public,
+					encoding,
 				} => {
-					let key = SigningKey::generate(&mut rng).to_keypair_bytes();
-
-					write_output(&public, &key[32..], &mut stdout_marker).context("public")?;
-					write_output(
+					let recipients = load_identities()?;
+					let key = ed25519_dalek::SigningKey::generate(&mut rng).to_keypair_bytes();
+					write_public(&public, &key[32..], encoding)?;
+					write_private(
+						&recipients,
 						&private,
 						&key[..{
 							if no_embed_public {
@@ -171,19 +217,31 @@
 								64
 							}
 						}],
-						&mut stdout_marker,
-					)
-					.context("private")?;
+						encoding,
+					)?;
+				}
+				Generate::X25519 {
+					public,
+					private,
+					encoding,
+				} => {
+					let recipients = load_identities()?;
+					let key = x25519_dalek::StaticSecret::random_from_rng(rng);
+					let public_key: x25519_dalek::PublicKey = (&key).into();
+					write_public(&public, public_key.as_bytes().as_slice(), encoding)?;
+					write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;
 				}
 				Generate::Password {
 					size,
 					no_symbols,
 					output,
+					encoding,
 				} => {
 					ensure!(
 						size >= 6,
 						"misconfiguration? password is shorter than 6 chars"
 					);
+					let recipients = load_identities()?;
 					let out = if no_symbols {
 						Alphanumeric.sample_string(&mut rng, size)
 					} else {
@@ -195,7 +253,7 @@
 							.map(|i| GEN_ASCII_SYMBOLS[i] as char)
 							.collect::<String>()
 					};
-					write_output(&output, out, &mut stdout_marker)?;
+					write_private(&recipients, &output, out.as_bytes(), encoding)?;
 				}
 			}
 		}
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
before · modules/fleet/secrets.nix
1{2  lib,3  fleetLib,4  config,5  ...6}:7with lib;8with fleetLib; let9  sharedSecret = with types; ({config, ...}: {10    freeformType = types.lazyAttrsOf unspecified;11    options = {12      expectedOwners = mkOption {13        type = nullOr (listOf str);14        description = ''15          List of hosts to encrypt secret for. null if managed by user (= via owners field from fleet.nix)1617          Secrets would be decrypted and stored to /run/secrets/$\{name} on owners18        '';19        default = null;20      };21      # TODO: Aren't those options may be just desugared to data/expectedData?22      regenerateOnOwnerAdded = mkOption {23        type = bool;24        description = ''25          Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted.2627          You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs.28        '';29      };30      regenerateOnOwnerRemoved = mkOption {31        default = config.regenerateOnOwnerAdded;32        type = bool;33        description = ''34          Should this secret be removed on owner removal, or it may be just reencrypted3536          Most probably its value should be equal to regenerateOnOwnerAdded, override only if you know what are you doing.37          Contrary to regenerateOnOwnerAdded, you may want to set this option to false, when host permissions are revoked38          in some other way than by this secret ownership, I.e by firewall/etc.39        '';40      };41      generator = mkOption {42        type = nullOr unspecified;43        description = "Derivation to evaluate for secret generation";44        default = null;45      };46      createdAt = mkOption {47        type = nullOr str;48        description = "When this secret was (re)generated";49        default = null;50      };51      expiresAt = mkOption {52        type = nullOr str;53        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";54        default = null;55      };5657      owners = mkOption {58        type = listOf str;59        description = ''60          For which owners this secret is currently encrypted,61          if not matches expectedOwners - then this secret is considered outdated, and62          should be regenerated/reencrypted.6364          Imported from fleet.nix65        '';66        default = [];67      };68    };69  });70  hostSecret = with types; {71    freeformType = types.lazyAttrsOf unspecified;72    options = {73      createdAt = mkOption {74        type = nullOr str;75        default = null;76      };77      expiresAt = mkOption {78        type = nullOr str;79        default = null;80      };81    };82  };83in {84  options = with types; {85    version = mkOption {86      type = str;87      default = "";88      internal = true;89    };90    sharedSecrets = mkOption {91      type = attrsOf (submodule sharedSecret);92      default = {};93      description = "Shared secrets";94    };95    hostSecrets = mkOption {96      type = attrsOf (attrsOf (submodule hostSecret));97      default = {};98      description = "Host secrets. Imported from fleet.nix";99      internal = true;100    };101  };102  config = {103    assertions =104      mapAttrsToList105      (name: secret: {106        assertion = secret.expectedOwners == null || builtins.sort (a: b: a < b) secret.owners == builtins.sort (a: b: a < b) secret.expectedOwners;107        message = "Shared secret ${name} is expected to be encrypted for ${builtins.toJSON secret.expectedOwners}, but it is encrypted for ${builtins.toJSON secret.owners}. Run fleet secrets regenerate to fix";108      })109      config.sharedSecrets;110    hosts = hostsToAttrs (host: {111      nixosModules = let112        # processPart113        processSecret = v:114          (removeAttrs v ["createdAt" "expiresAt" "expectedOwners" "owners" "regenerateOnOwnerAdded" "regenerateOnOwnerRemoved"])115          // {116            shared = true;117          };118      in [119        {120          secrets =121            (122              mapAttrs (_: processSecret)123              (filterAttrs (_: v: builtins.elem host v.owners) config.sharedSecrets)124            )125            // (mapAttrs (_: processSecret) (config.hostSecrets.${host} or {}));126        }127      ];128    });129    # TODO: Should this attribute be moved to `nixpkgs.overlays`?130    overlays = [131      (final: prev: let132        lib = final.lib;133        inherit (lib) strings concatMap;134        inherit (strings) escapeShellArgs;135      in {136        mkEncryptSecret = {137          rage ? prev.rage,138          recipients,139        }:140          prev.writeShellScript "encryptor" ''141            #!/bin/sh142            exec ${rage}/bin/rage ${escapeShellArgs (concatMap (r: ["-r" r]) recipients)} -e "$@"143          '';144        # TODO: Move to fleet145        # TODO: Merge both generators to one with consistent options syntax?146        # Impure generator is built on local machine, then built closure is copied to remote machine,147        # and then it is ran in inpure context, so that this generator may access HSMs and other things.148        mkImpureSecretGenerator = {149          script,150          # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD151          # (Some secrets-encryption-in-git/managed PKI solution is expected)152          impureOn ? null,153        }:154          (prev.writeShellScript "impureGenerator.sh" ''155            #!/bin/sh156            set -eu157158            # TODO: Provide tempdir from outside, to make it securely erasurable as needed?159            tmp=$(mktemp -d)160            cd $tmp161            # cd /var/empty162163            created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")164165            ${script}166167            if ! test -d $out; then168              echo "impure generator script did not produce expected \$out output"169              exit 1170            fi171172            echo -n $created_at > $out/created_at173            echo -n SUCCESS > $out/marker174          '')175          .overrideAttrs (old: {176            passthru = {177              inherit impureOn;178              generatorKind = "impure";179            };180          });181        # Pure generators are disabled for now182        mkSecretGenerator = {script}: final.mkImpureSecretGenerator {inherit script;};183184        # TODO: Implement consistent naming185        # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...186        # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.187        # mkSecretGenerator = {script}:188        #   (prev.writeShellScript "generator.sh" ''189        #     #!/bin/sh190        #     set -eu191        #     # TODO: make nix daemon build secret, not just the script.192        #     cd /var/empty193        #194        #     created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")195        #196        #     ${script}197        #     if ! test -d $out; then198        #       echo "impure generator script did not produce expected \$out output"199        #       exit 1200        #     fi201        #202        #     echo -n $created_at > $out/created_at203        #     echo -n SUCCESS > $out/marker204        #   '')205        #   .overrideAttrs (old: {206        #     passthru = {207        #       generatorKind = "pure";208        #     };209        #     # TODO: make nix daemon build secret, not just the script.210        #     # __impure = true;211        #   });212      })213    ];214  };215}
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
-  '';
-}