git.delta.rocks / jrsonnet / refs/commits / 837e795f702e

difftreelog

feat reencrypt secret on remote server

Yaroslav Bolyukin2023-05-01parent: #d5e1b5f.patch.diff
in: trunk

8 files changed

modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -1,15 +1,8 @@
-use std::{collections::BTreeSet, time::Duration};
+use std::collections::BTreeSet;
 
-use crate::{command::CommandExt, host::Config};
-use anyhow::{bail, ensure, Result};
+use crate::host::Config;
+use anyhow::{ensure, Result};
 use clap::Parser;
-use nixlike::format_nix;
-use serde_json::{json, Value};
-use tokio::{
-	fs::{self, File},
-	io::AsyncWriteExt,
-	process::Command,
-};
 
 #[derive(Parser)]
 pub struct Info {
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -12,6 +12,7 @@
 	iter,
 	path::PathBuf,
 };
+use tokio::fs::read_to_string;
 use tracing::{info, warn};
 
 #[derive(Parser)]
@@ -50,19 +51,30 @@
 	Read {
 		name: String,
 		machine: String,
+		#[clap(long)]
+		plaintext: bool,
 	},
 	UpdateShared {
 		name: String,
 
+		#[clap(long)]
 		machines: Option<Vec<String>>,
 
+		#[clap(long)]
 		add_machines: Vec<String>,
+		#[clap(long)]
 		remove_machines: Vec<String>,
 
 		/// Which host should we use to decrypt
+		#[clap(long)]
+		prefer_identities: Vec<String>,
+	},
+	Regenerate {
+		/// Which host should we use to decrypt, in case if reencryption is required, without
+		/// regeneration
+		#[clap(long)]
 		prefer_identities: Vec<String>,
 	},
-	Regenerate,
 }
 
 impl Secrets {
@@ -110,11 +122,10 @@
 					}
 				};
 
-				let mut data = config.data_mut();
-				if data.shared_secrets.contains_key(&name) && !force {
+				if config.has_shared(&name) && !force {
 					bail!("secret already defined");
 				}
-				data.shared_secrets.insert(
+				config.replace_shared(
 					name,
 					FleetSharedSecret {
 						owners: machines,
@@ -123,7 +134,7 @@
 							secret,
 							public: match (public, public_file) {
 								(Some(v), None) => Some(v),
-								(None, Some(v)) => Some(std::fs::read_to_string(v)?),
+								(None, Some(v)) => Some(read_to_string(v).await?),
 								(Some(_), Some(_)) => {
 									bail!("only public or public_file should be set")
 								}
@@ -159,12 +170,11 @@
 					encrypted
 				};
 
-				let mut data = config.data_mut();
-				let host_secrets = data.host_secrets.entry(machine).or_default();
-				if host_secrets.contains_key(&name) && !force {
+				if config.has_secret(&machine, &name) && !force {
 					bail!("secret already defined");
 				}
-				host_secrets.insert(
+				config.insert_secret(
+					&machine,
 					name,
 					FleetSecret {
 						expire_at: None,
@@ -180,34 +190,22 @@
 			}
 			// TODO: Instead of using sudo, decode secret on remote machine
 			#[allow(clippy::await_holding_refcell_ref)]
-			Secrets::Read { name, machine } => {
-				let data = config.data();
-
-				let Some(host_secrets) = data.host_secrets.get(&machine) else {
-                    bail!("no secrets for machine {machine}");
-                };
-				let Some(secret) = host_secrets.get(&name) else {
-                    bail!("machine {machine} has no secret {name}");
-                };
+			Secrets::Read {
+				name,
+				machine,
+				plaintext,
+			} => {
+				let secret = config.host_secret(&machine, &name)?;
 				if secret.secret.is_empty() {
 					bail!("no secret {name}");
 				}
-				let identity = config.identity(&machine).await?;
-				let decryptor = Decryptor::new(Cursor::new(&secret.secret))?;
-				let decryptor = match decryptor {
-					Decryptor::Recipients(r) => r,
-					Decryptor::Passphrase(_) => bail!("should be recipients"),
-				};
-				let mut decryptor = decryptor
-					.decrypt(iter::once(&identity as &dyn age::Identity))
-					.context("failed to decrypt, wrong key?")?;
-
-				let mut decrypted = Vec::new();
-				decryptor
-					.read_to_end(&mut decrypted)
-					.context("failed to decrypt")?;
-				// secret.secret
-				std::io::stdout().lock().write_all(&decrypted)?;
+				let data = config.decrypt_on_host(&machine, secret.secret).await?;
+				if plaintext {
+					let s = String::from_utf8(data).context("output is not utf8")?;
+					print!("{s}");
+				} else {
+					println!("{}", z85::encode(&data));
+				}
 			}
 			Secrets::UpdateShared {
 				name,
@@ -216,20 +214,18 @@
 				mut remove_machines,
 				prefer_identities,
 			} => {
-				let mut data = config.data_mut();
 				if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {
 					bail!("no operation");
 				}
 
-				let Some(mut secret) = data.shared_secrets.get_mut(&name) else {
-                    bail!("no shared secret {name}");
-                };
+				let mut secret = config.shared_secret(&name)?;
 				if secret.secret.secret.is_empty() {
 					bail!("no secret");
 				}
 
 				let initial_machines = secret.owners.clone();
 				let mut target_machines = secret.owners.clone();
+				info!("Currently encrypted for {initial_machines:?}");
 
 				// ensure!(machines.is_some() || !add_machines.is_empty() || )
 				if let Some(machines) = machines {
@@ -254,23 +250,31 @@
 						removed = true;
 					}
 					if !removed {
-						bail!("secret is not enabled for {machine}");
+						warn!("secret is not enabled for {machine}");
 					}
 				}
 				for machine in &add_machines {
 					if target_machines.iter().any(|m| m == machine) {
 						warn!("secret is already added to {machine}");
+					} else {
+						target_machines.push(machine.to_owned());
 					}
 				}
-				if remove_machines.is_empty() {
+				if !remove_machines.is_empty() {
 					warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");
 				}
+
 				if target_machines.is_empty() {
 					info!("no machines left for secret, removing it");
-					data.shared_secrets.remove(&name);
+					config.remove_shared(&name);
 					return Ok(());
 				}
 
+				if target_machines == initial_machines {
+					warn!("secret owners are already correct");
+					return Ok(());
+				}
+
 				let identity_holder = if !prefer_identities.is_empty() {
 					prefer_identities
 						.iter()
@@ -282,51 +286,34 @@
                     bail!("no available holder found");
                 };
 				let target_recipients = futures::stream::iter(&target_machines)
-					.flat_map(|m| futures::stream::once(config.recipient(m)))
+					.then(|m| async { config.key(m).await })
 					.collect::<Vec<_>>()
-					.await
-					.into_iter()
-					.map(|v| v.map(|v| Box::new(v) as Box<dyn age::Recipient + Send>))
-					.collect::<Result<Vec<_>>>()?;
+					.await;
+				let target_recipients =
+					target_recipients.into_iter().collect::<Result<Vec<_>>>()?;
 
-				let identity = config.identity(identity_holder).await?;
-				let decryptor = Decryptor::new(Cursor::new(&secret.secret.secret))?;
-				let decryptor = match decryptor {
-					Decryptor::Recipients(r) => r,
-					Decryptor::Passphrase(_) => bail!("should be recipients"),
-				};
-				let mut decryptor = decryptor
-					.decrypt(iter::once(&identity as &dyn age::Identity))
-					.context("failed to decrypt, wrong key?")?;
+				let encrypted = config
+					.reencrypt_on_host(&identity_holder, secret.secret.secret, target_recipients)
+					.await?;
 
-				let mut decrypted = Vec::new();
-				decryptor
-					.read_to_end(&mut decrypted)
-					.context("failed to decrypt")?;
-
-				let mut encrypted = vec![];
-				let mut encryptor = Encryptor::with_recipients(target_recipients)
-					.expect("recipients provided")
-					.wrap_output(&mut encrypted)?;
-				io::copy(&mut Cursor::new(decrypted), &mut encryptor)?;
-				encryptor.finish()?;
-
+				secret.owners = target_machines;
 				secret.secret.secret = encrypted;
+				config.replace_shared(name, secret);
 			}
-			Secrets::Regenerate => {
-				// config.data_mut().shared_secrets
+			Secrets::Regenerate { prefer_identities } => {
 				{
 					let expected_shared_set =
 						config.shared_config_attr_names("sharedSecrets").await?;
 					let expected_shared_set = expected_shared_set.iter().collect::<HashSet<_>>();
-					let shared_set = config.data();
-					let shared_set = shared_set.shared_secrets.keys().collect::<HashSet<_>>();
+					let shared_set = config.list_shared();
+					let shared_set = shared_set.iter().collect::<HashSet<_>>();
 					for removed in expected_shared_set.difference(&shared_set) {
 						warn!("secret needs to be generated: {removed}")
 					}
 				}
 				let mut to_remove = Vec::new();
-				for (name, data) in &config.data().shared_secrets {
+				for name in &config.list_shared() {
+					let mut data = config.shared_secret(name)?;
 					let expected_owners: Vec<String> = config
 						.shared_config_attr(&format!("sharedSecrets.\"{name}\".expectedOwners"))
 						.await?;
@@ -337,12 +324,54 @@
 					}
 					let set = data.owners.iter().collect::<HashSet<_>>();
 					let expected_set = expected_owners.iter().collect::<HashSet<_>>();
+					let should_remove = set.difference(&expected_set).next().is_some();
 					if set != expected_set {
 						warn!("reconfiguring owners for {name}");
+						let generator: Option<String> = config
+							.shared_config_attr(&format!("sharedSecrets.\"{name}\".generator"))
+							.await?;
+						// TODO: if !.owner_dependent
+						if let Some(str) = generator {
+							todo!("regenerate")
+						} else {
+							if should_remove {
+								warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");
+							}
+
+							let identity_holder = if !prefer_identities.is_empty() {
+								prefer_identities
+									.iter()
+									.find(|i| data.owners.iter().any(|s| s == *i))
+							} else {
+								data.owners.first()
+							};
+							let Some(identity_holder) = identity_holder else {
+								bail!("no available holder found");
+							};
+
+							let target_recipients = futures::stream::iter(&expected_owners)
+								.then(|m| async { config.key(m).await })
+								.collect::<Vec<_>>()
+								.await;
+							let target_recipients =
+								target_recipients.into_iter().collect::<Result<Vec<_>>>()?;
+
+							let encrypted = config
+								.reencrypt_on_host(
+									&identity_holder,
+									data.secret.secret,
+									target_recipients,
+								)
+								.await?;
+
+							data.secret.secret = encrypted;
+							data.owners = expected_owners;
+							config.replace_shared(name.to_owned(), data);
+						}
 					}
 				}
 				for k in to_remove {
-					config.data_mut().shared_secrets.remove(&k);
+					config.remove_shared(&k);
 				}
 			}
 		}
modifiedcmds/fleet/src/fleetdata.rsdiffbeforeafterboth
--- a/cmds/fleet/src/fleetdata.rs
+++ b/cmds/fleet/src/fleetdata.rs
@@ -1,8 +1,7 @@
-use anyhow::{bail, Result};
+use anyhow::Result;
 use chrono::{DateTime, Utc};
 use nixlike::format_nix;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use serde_json::{json, Value};
 use std::collections::BTreeMap;
 use tempfile::TempDir;
 use tokio::{
@@ -11,8 +10,6 @@
 	process::Command,
 };
 
-use crate::command::CommandExt;
-
 #[derive(Serialize, Deserialize, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct HostData {
@@ -34,16 +31,18 @@
 	pub host_secrets: BTreeMap<String, BTreeMap<String, FleetSecret>>,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Clone)]
 #[serde(rename_all = "camelCase")]
+#[must_use]
 pub struct FleetSharedSecret {
 	pub owners: Vec<String>,
 	#[serde(flatten)]
 	pub secret: FleetSecret,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Clone)]
 #[serde(rename_all = "camelCase")]
+#[must_use]
 pub struct FleetSecret {
 	#[serde(default)]
 	#[serde(skip_serializing_if = "Option::is_none")]
@@ -75,6 +74,8 @@
 		.and_then(|string| z85::decode(string).map_err(|err| Error::custom(err.to_string())))
 }
 
+/// Isn't used yet
+#[allow(dead_code)]
 pub async fn dummy_flake() -> Result<TempDir> {
 	let data_str = fs::read_to_string("fleet.nix").await?;
 
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -8,15 +8,15 @@
 	sync::Arc,
 };
 
-use anyhow::Result;
+use anyhow::{Result, bail, Context};
 use clap::{ArgGroup, Parser};
 use serde::de::DeserializeOwned;
-use tempfile::{NamedTempFile, TempDir};
+use tempfile::NamedTempFile;
 use tokio::process::Command;
 
 use crate::{
 	command::CommandExt,
-	fleetdata::{dummy_flake, FleetData},
+	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},
 };
 
 pub struct FleetConfigInternals {
@@ -125,13 +125,93 @@
 			.await
 	}
 
-	pub fn data(&self) -> Ref<FleetData> {
+	pub(super) fn data(&self) -> Ref<FleetData> {
 		self.data.borrow()
 	}
-	pub fn data_mut(&self) -> RefMut<FleetData> {
+	pub(super) fn data_mut(&self) -> RefMut<FleetData> {
 		self.data.borrow_mut()
 	}
 
+	pub fn list_shared(&self) -> Vec<String> {
+		let data = self.data();
+		data.shared_secrets.keys().cloned().collect()
+	}
+	pub fn has_shared(&self, name: &str) -> bool {
+		let data = self.data();
+		data.shared_secrets.contains_key(name)
+	}
+	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {
+		let mut data = self.data_mut();
+		data.shared_secrets.insert(name.to_owned(), shared);
+	}
+	pub fn remove_shared(&self, secret: &str) {
+		let mut data = self.data_mut();
+		data.shared_secrets.remove(secret);
+	}
+
+	pub fn list_secrets(&self, host: &str) -> Vec<String> {
+		let data = self.data();
+		let Some(host_secrets) = data.host_secrets.get(host) else {
+			return Vec::new(); 
+		};
+		host_secrets.keys().cloned().collect()
+	}
+	pub fn has_secret(&self, host: &str, secret: &str) -> bool {
+		let data = self.data();
+		let Some(host_secrets) = data.host_secrets.get(host) else {
+			return false; 
+		};
+		host_secrets.contains_key(secret)
+	}
+	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {
+		let mut data = self.data_mut();
+		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();
+		host_secrets.insert(secret, value);
+	}
+
+	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>>{
+		let data = z85::encode(&data);
+		let encoded = self.command_on(host, "fleet-install-secrets", true)
+			.arg("decrypt")
+			.arg("--secret")
+			.arg(data).run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();
+		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)
+	}
+	pub async fn reencrypt_on_host(&self, host: &str, data: Vec<u8>, targets: Vec<String>) -> Result<Vec<u8>>{
+		let data = z85::encode(&data);
+		let mut recmd = self.command_on(host, "fleet-install-secrets", true);
+		recmd
+			.arg("reencrypt")
+			.arg("--secret")
+			.arg(format!("\"{}\"", data.replace('$', "\\$")));
+		for target in targets {
+			recmd.arg("--targets");
+			recmd.arg(format!("\"{target}\""));
+		}
+		let encoded = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();
+		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)
+	}
+
+	#[must_use]
+	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {
+		let data = self.data();
+		let Some(host_secrets) = data.host_secrets.get(host) else {
+            bail!("no secrets for machine {host}");
+        };
+		let Some(secret) = host_secrets.get(secret) else {
+            bail!("machine {host} has no secret {secret}");
+        };
+		Ok(secret.clone())
+	}
+	#[must_use]
+	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {
+		let data = self.data();
+		let Some(secret) = data.shared_secrets.get(secret) else {
+			bail!("no shared secret {secret}");
+		};
+		Ok(secret.clone())
+	}
+
 	pub fn save(&self) -> Result<()> {
 		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;
 		let data = nixlike::serialize(&self.data() as &FleetData)?;
modifiedcmds/fleet/src/keys.rsdiffbeforeafterboth
--- a/cmds/fleet/src/keys.rs
+++ b/cmds/fleet/src/keys.rs
@@ -36,15 +36,6 @@
 		}
 	}
 	/// Insecure, requires root
-	pub async fn identity(&self, host: &str) -> anyhow::Result<age::ssh::Identity> {
-		warn!("Loading private key for {host}");
-		let key = self
-			.command_on(host, "cat", true)
-			.arg("/etc/ssh/ssh_host_ed25519_key")
-			.run_string()
-			.await?;
-		Ok(age::ssh::Identity::from_buffer(key.as_bytes(), None)?)
-	}
 	pub async fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {
 		let key = self.key(host).await?;
 		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
1use age::Decryptor;1use age::{ssh::Identity as SshIdentity, ssh::Recipient as SshRecipient, Decryptor};
2use age::{Encryptor, Identity, Recipient};
2use anyhow::{anyhow, bail, Context, Result};3use anyhow::{anyhow, bail, Context, Result};
3use clap::Parser;4use clap::Parser;
4use log::{error, info, warn};5use log::{error, info, warn};
5use nix::sys::stat::Mode;6use nix::sys::stat::Mode;
6use nix::unistd::{chown, Group, User};7use nix::unistd::{chown, Group, User};
7use serde::{Deserialize, Deserializer};8use serde::{Deserialize, Deserializer};
9use std::fmt::{self, Display};
8use std::fs::{self, File};10use std::fs::{self, File};
9use std::io::{self, Cursor, Read, Write};11use std::io::{self, Cursor, Read, Write};
10use std::iter;12use std::iter;
11use std::os::unix::prelude::PermissionsExt;13use std::os::unix::prelude::PermissionsExt;
14use std::path::Path;
12use std::str::from_utf8;15use std::str::{from_utf8, FromStr};
13use std::{collections::HashMap, path::PathBuf};16use std::{collections::HashMap, path::PathBuf};
17
18#[derive(Clone, Debug)]
19struct SecretWrapper(Vec<u8>);
20impl Display for SecretWrapper {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 let encoded = z85::encode(&self.0);
23 write!(f, "{encoded}")
24 }
25}
26impl FromStr for SecretWrapper {
27 type Err = z85::DecodeError;
28
29 fn from_str(s: &str) -> Result<Self, Self::Err> {
30 z85::decode(s).map(Self)
31 }
32}
33impl<'de> Deserialize<'de> for SecretWrapper {
34 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
35 where
36 D: Deserializer<'de>,
37 {
38 let v = String::deserialize(deserializer)?;
39 let de = z85::decode(v).map_err(|err| serde::de::Error::custom(err.to_string()))?;
40 Ok(Self(de))
41 }
42}
1443
15#[derive(Parser)]44#[derive(Parser)]
16#[clap(author)]45#[clap(author)]
17struct Opts {46enum Opts {
47 /// Install secrets from json specification
18 data: PathBuf,48 Install { data: PathBuf },
19}49 /// Reencrypt secret using host key, outputting in z85 encoded string
50 Reencrypt {
51 #[clap(long)]
52 secret: SecretWrapper,
53 #[clap(long)]
54 targets: Vec<String>,
55 },
56 /// Decrypt secret using host key, outputting in z85 encoded string
57 Decrypt {
58 #[clap(long)]
59 secret: SecretWrapper,
60 /// Shoult decoded output be printed as plaintext, instead of z85?
61 #[clap(long)]
62 plaintext: bool,
63 },
64}
2065
21#[derive(Deserialize)]66#[derive(Deserialize)]
25 mode: String,70 mode: String,
26 owner: String,71 owner: String,
2772
28 #[serde(deserialize_with = "from_z85")]
29 secret: Option<Vec<u8>>,73 secret: Option<SecretWrapper>,
30 public: Option<String>,74 public: Option<String>,
3175
32 public_path: PathBuf,76 public_path: PathBuf,
36 stable_secret_path: PathBuf,80 stable_secret_path: PathBuf,
37}81}
38
39fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
40where
41 D: Deserializer<'de>,
42{
43 use serde::de::Error;
44 if let Some(v) = <Option<String>>::deserialize(deserializer)? {
45 Ok(Some(
46 z85::decode(v).map_err(|err| Error::custom(err.to_string()))?,
47 ))
48 } else {
49 Ok(None)
50 }
51}
5282
53type Data = HashMap<String, DataItem>;83type Data = HashMap<String, DataItem>;
84
85fn decrypt(input: &SecretWrapper, identity: &dyn Identity) -> Result<Vec<u8>> {
86 let mut input = Cursor::new(&input.0);
87 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;
88 let decryptor = match decryptor {
89 Decryptor::Recipients(r) => r,
90 Decryptor::Passphrase(_) => bail!("should be recipients"),
91 };
92 let mut decryptor = decryptor
93 .decrypt(iter::once(identity as &dyn age::Identity))
94 .context("failed to decrypt, wrong key?")?;
95
96 let mut decrypted = Vec::new();
97 decryptor
98 .read_to_end(&mut decrypted)
99 .context("failed to decrypt")?;
100 Ok(decrypted)
101}
102fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretWrapper> {
103 let recipients = targets
104 .into_iter()
105 .map(|t| {
106 SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))
107 })
108 .collect::<Result<Vec<SshRecipient>>>()?;
109 let recipients = recipients
110 .into_iter()
111 .map(|v| Box::new(v) as Box<dyn Recipient + Send>)
112 .collect::<Vec<_>>();
113 let mut encrypted = vec![];
114 let mut encryptor = Encryptor::with_recipients(recipients)
115 .expect("recipients provided")
116 .wrap_output(&mut encrypted)
117 .expect("constructor should not fail");
118 io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");
119 encryptor.finish().context("failed to finish encryption")?;
120 Ok(SecretWrapper(encrypted))
121}
54122
55fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {123fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {
56 if let Some(public) = &value.public {124 if let Some(public) = &value.public {
93 let mut hashed = File::create(&value.secret_path)?;161 let mut hashed = File::create(&value.secret_path)?;
94162
95 // File is owned by root, and only root can modify it163 // File is owned by root, and only root can modify it
96 let decrypted = {164 let decrypted = decrypt(&secret, identity)?;
97 let mut input = Cursor::new(&secret);
98 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;
99 let decryptor = match decryptor {
100 Decryptor::Recipients(r) => r,
101 Decryptor::Passphrase(_) => bail!("should be recipients"),
102 };
103 let mut decryptor = decryptor
104 .decrypt(iter::once(identity as &dyn age::Identity))
105 .context("failed to decrypt, wrong key?")?;
106
107 let mut decrypted = Vec::new();
108 decryptor
109 .read_to_end(&mut decrypted)
110 .context("failed to decrypt")?;
111 decrypted
112 };
113 if decrypted.is_empty() {165 if decrypted.is_empty() {
114 warn!("secret is decoded as empty, something is broken?");166 warn!("secret is decoded as empty, something is broken?");
115 }167 }
132 Ok(())184 Ok(())
133}185}
186
187fn host_identity() -> anyhow::Result<SshIdentity> {
188 let identity = SshIdentity::from_buffer(
189 &mut Cursor::new(
190 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,
191 ),
192 None,
193 )
194 .context("failed to parse identity")?;
195 Ok(identity)
196}
134197
135fn main() -> anyhow::Result<()> {198fn install(data: &Path) -> anyhow::Result<()> {
136 env_logger::Builder::new()
137 .filter_level(log::LevelFilter::Info)
138 .init();
139
140 let opts = Opts::parse();
141 let data = fs::read(&opts.data).context("failed to read secrets data")?;199 let data = fs::read(data).context("failed to read secrets data")?;
142 let data_str = from_utf8(&data).context("failed to read data to string")?;200 let data_str = from_utf8(&data).context("failed to read data to string")?;
143 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;201 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;
144202
149 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;207 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;
150 }208 }
151209
152 let identity = age::ssh::Identity::from_buffer(210 let identity = host_identity()?;
153 &mut Cursor::new(
154 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,
155 ),
156 None,
157 )
158 .context("failed to parse identity")?;
159211
160 let mut failed = false;212 let mut failed = false;
161 for (name, value) in data {213 for (name, value) in data {
175 Ok(())227 Ok(())
176}228}
229
230fn main() -> anyhow::Result<()> {
231 env_logger::Builder::new()
232 .filter_level(log::LevelFilter::Info)
233 .init();
234
235 let opts = Opts::parse();
236
237 match opts {
238 Opts::Install { data } => install(&data),
239 Opts::Reencrypt { secret, targets } => {
240 let identity = host_identity()?;
241 let decrypted = decrypt(&secret, &identity).context("during decryption")?;
242 let encrypted = encrypt(&decrypted, targets).context("during re-encryption")?;
243
244 println!("{encrypted}");
245 Ok(())
246 }
247 Opts::Decrypt { secret, plaintext } => {
248 let identity = host_identity()?;
249 let decrypted = decrypt(&secret, &identity).context("during decryption")?;
250
251 if plaintext {
252 let s = String::from_utf8(decrypted).context("output is not utf8")?;
253 print!("{}", s);
254 } else {
255 println!("{}", SecretWrapper(decrypted));
256 }
257 Ok(())
258 }
259 }
260}
177261
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -20,9 +20,18 @@
         '';
         default = [ ];
       };
+      ownerDependent = mkOption {
+        type = bool;
+        description = "Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted";
+      };
       generator = mkOption {
-        type = package;
-        description = "Derivation to execute for secret generation";
+        type = nullOr package;
+        description = ''
+          Derivation to execute for secret generation
+
+          If null - may only be created manually
+        '';
+        default = null;
       };
       expireIn = mkOption {
         type = nullOr int;
modifiednixos/secrets.nixdiffbeforeafterboth
--- a/nixos/secrets.nix
+++ b/nixos/secrets.nix
@@ -89,9 +89,10 @@
     };
   };
   config = {
+    environment.systemPackages = with pkgs; [pkgs.fleet-install-secrets];
     system.activationScripts.decryptSecrets = stringAfter [ "users" "groups" "specialfs" ] ''
       1>&2 echo "setting up secrets"
-      ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets ${secretsFile}
+      ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}
     '';
   };
 }