difftreelog
feat reencrypt secret on remote server
in: trunk
8 files changed
cmds/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 {
cmds/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);
}
}
}
cmds/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?;
cmds/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)?;
cmds/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))
cmds/install-secrets/src/main.rsdiffbeforeafterboth1use 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};1718#[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;2829 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 where36 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}144315#[derive(Parser)]44#[derive(Parser)]16#[clap(author)]45#[clap(author)]17struct Opts {46enum Opts {47 /// Install secrets from json specification18 data: PathBuf,48 Install { data: PathBuf },19}49 /// Reencrypt secret using host key, outputting in z85 encoded string50 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 string57 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}206521#[derive(Deserialize)]66#[derive(Deserialize)]25 mode: String,70 mode: String,26 owner: String,71 owner: String,277228 #[serde(deserialize_with = "from_z85")]29 secret: Option<Vec<u8>>,73 secret: Option<SecretWrapper>,30 public: Option<String>,74 public: Option<String>,317532 public_path: PathBuf,76 public_path: PathBuf,36 stable_secret_path: PathBuf,80 stable_secret_path: PathBuf,37}81}3839fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>40where41 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}528253type Data = HashMap<String, DataItem>;83type Data = HashMap<String, DataItem>;8485fn 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 = decryptor93 .decrypt(iter::once(identity as &dyn age::Identity))94 .context("failed to decrypt, wrong key?")?;9596 let mut decrypted = Vec::new();97 decryptor98 .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 = targets104 .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 = recipients110 .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}5412255fn 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)?;9416295 // File is owned by root, and only root can modify it163 // File is owned by root, and only root can modify it96 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 = decryptor104 .decrypt(iter::once(identity as &dyn age::Identity))105 .context("failed to decrypt, wrong key?")?;106107 let mut decrypted = Vec::new();108 decryptor109 .read_to_end(&mut decrypted)110 .context("failed to decrypt")?;111 decrypted112 };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}186187fn 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}134197135fn main() -> anyhow::Result<()> {198fn install(data: &Path) -> anyhow::Result<()> {136 env_logger::Builder::new()137 .filter_level(log::LevelFilter::Info)138 .init();139140 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")?;144202149 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 }151209152 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")?;159211160 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}229230fn main() -> anyhow::Result<()> {231 env_logger::Builder::new()232 .filter_level(log::LevelFilter::Info)233 .init();234235 let opts = Opts::parse();236237 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")?;243244 println!("{encrypted}");245 Ok(())246 }247 Opts::Decrypt { secret, plaintext } => {248 let identity = host_identity()?;249 let decrypted = decrypt(&secret, &identity).context("during decryption")?;250251 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}177261modules/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;
nixos/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}
'';
};
}