difftreelog
refactor minor rewrites
in: trunk
7 files changed
cmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth247 Ok(())247 Ok(())248 }248 }249 async fn send_command(&mut self, cmd: impl AsRef<[u8]>) -> Result<()> {249 async fn send_command(&mut self, cmd: impl AsRef<[u8]>) -> Result<()> {250 if tracing::enabled!(Level::DEBUG) {250 if tracing::enabled!(Level::DEBUG) && cmd.as_ref() != REPL_DELIMITER.as_bytes() {251 let cmd_str = String::from_utf8_lossy(cmd.as_ref());251 let cmd_str = String::from_utf8_lossy(cmd.as_ref());252 tracing::debug!("{cmd_str}");252 tracing::debug!("{cmd_str}");253 };253 };628 pub async fn field(session: NixSession, field: &str) -> Result<Self> {628 pub async fn field(session: NixSession, field: &str) -> Result<Self> {629 Self::root(session).select([Index::var(field)]).await629 Self::root(session).select([Index::var(field)]).await630 }630 }631 pub async fn get_json_deep<'a, V: DeserializeOwned>(632 &self,633 name: impl IntoIterator<Item = Index>,634 ) -> Result<V> {635 let field = self.select(name).await?;636 field.as_json().await637 }638 pub async fn select<'a>(&self, name: impl IntoIterator<Item = Index>) -> Result<Self> {631 pub async fn select<'a>(&self, name: impl IntoIterator<Item = Index>) -> Result<Self> {639 let mut used_fields = Vec::new();632 let mut used_fields = Vec::new();640 let mut name = name.into_iter();633 let mut name = name.into_iter();719 .await712 .await720 .with_context(|| context(self.0.full_path.as_deref(), &query))713 .with_context(|| context(self.0.full_path.as_deref(), &query))721 }714 }715 pub async fn has_field(&self, name: &str) -> Result<bool> {716 let id = self.0.value.expect("can't list root fields");717 let key = nixlike::escape_string(name);718 let query = format!("sess_field_{id} ? {key}");719 self.0720 .session721 .0722 .lock()723 .await724 .execute_expression_to_json(&query)725 .await726 .with_context(|| context(self.0.full_path.as_deref(), &query))727 }722 pub async fn list_fields(&self) -> Result<Vec<String>> {728 pub async fn list_fields(&self) -> Result<Vec<String>> {723 let id = self.0.value.expect("can't list root fields");729 let id = self.0.value.expect("can't list root fields");724 let query = format!("builtins.attrNames sess_field_{id}");730 let query = format!("builtins.attrNames sess_field_{id}");731 .await737 .await732 .with_context(|| context(self.0.full_path.as_deref(), &query))738 .with_context(|| context(self.0.full_path.as_deref(), &query))733 }739 }740 pub async fn type_of(&self) -> Result<String> {741 let id = self.0.value.expect("can't list root fields");742 let query = format!("builtins.typeOf sess_field_{id}");743 self.0744 .session745 .0746 .lock()747 .await748 .execute_expression_to_json(&query)749 .await750 .with_context(|| context(self.0.full_path.as_deref(), &query))751 }734 pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {752 pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {735 let id = self.0.value.expect("can't use build on not-value");753 let id = self.0.value.expect("can't use build on not-value");736 let query = format!(":b sess_field_{id}");754 let query = format!(":b sess_field_{id}");cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth1use crate::{1use crate::{2 command::MyCommand,2 better_nix_eval::Field,3 fleetdata::{FleetSecret, FleetSharedSecret},3 fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4 host::Config,4 host::Config,5 nix_go, nix_go_json,5 nix_go, nix_go_json,6};6};7use anyhow::{anyhow, bail, ensure, Context, Result};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};8use chrono::{DateTime, Utc};9use clap::Parser;9use clap::Parser;10use futures::{StreamExt, TryStreamExt};10use futures::{StreamExt, TryStreamExt};11use itertools::Itertools;11use owo_colors::OwoColorize;12use owo_colors::OwoColorize;12use std::{13use std::{13 collections::HashSet,14 collections::HashSet,14 io::{self, Cursor, Read},15 io::{self, Cursor, Read},15 path::PathBuf,16 path::PathBuf,16 sync::Arc,17};17};18use tabled::{Table, Tabled};18use tabled::{Table, Tabled};19use tokio::fs::read_to_string;19use tokio::fs::read_to_string;20use tracing::{error, info, info_span, warn};20use tracing::{info, info_span, warn};212122#[derive(Parser)]22#[derive(Parser)]23pub enum Secret {23pub enum Secret {90 prefer_identities: Vec<String>,90 prefer_identities: Vec<String>,91 },91 },92 List {},92 List {},93 InvokeGenerator,94}93}959496impl Secret {97 pub async fn run(self, config: &Config) -> Result<()> {95async fn generate_shared(96 config: &Config,97 display_name: &str,98 secret: Field,99) -> Result<FleetSharedSecret> {98 match self {100 Ok(if secret.has_field("generateImpure").await? {99 Secret::InvokeGenerator => {101 let config_field = &config.config_unchecked_field;100 let config_field = &config.config_unchecked_field;102 let generate = nix_go!(secret.generateImpure);103 let owners: Vec<String> = nix_go_json!(secret.expectedOwners);101104102 let secret =105 let on: String = nix_go_json!(generate.on);103 nix_go!(config_field.configUnchecked.sharedSecrets["kube-apiserver.pem"]);104 let generate_impure = nix_go!(secret.generateImpure);105 let on = nix_go!(generate_impure.on);106 let call_package = nix_go!(106 let call_package = nix_go!(107 config_field.buildableSystems(Obj {107 config_field.buildableSystems(Obj {108 localSystem: { config.local_system.clone() }108 localSystem: { config.local_system.clone() }109 })[on]109 })[{ on }]110 .config110 .config111 .nixpkgs111 .nixpkgs112 .resolvedPkgs112 .resolvedPkgs113 .callPackage113 .callPackage114 );114 );115 let generator = nix_go!(call_package(generate_impure.generator)(Obj {}));116 let built = &generator.build().await?["out"];117 let mut nix = MyCommand::new("nix");118 let on: String = on.as_json().await?;119 nix.arg("copy")120 .arg("--substitute-on-destination")121 .comparg("--to", format!("ssh-ng://{on}"))122 .arg(built);123 nix.run_nix().await?;124115125 let session = config.host(&on).await?;116 let host = config.host(&on).await?;126117127 let owners: Vec<String> = nix_go_json!(secret.expectedOwners);118 let generator = nix_go!(call_package(generate.generator)(Obj {}));128 dbg!(&owners);119 let generator = generator.build().await?;120 let generator = generator121 .get("out")122 .ok_or_else(|| anyhow!("missing generateImpure out"))?;123 let generator = host.remote_derivation(generator).await?;129124130 let mut recipients = String::new();125 let mut recipients = String::new();131 for owner in owners {126 for owner in &owners {132 let key = config.key(&owner).await?;127 let key = config.key(owner).await?;133 recipients.push_str(&format!("-r \"{key}\" "));128 recipients.push_str(&format!("-r \"{key}\" "));134 }129 }135 recipients.push_str("-e");130 recipients.push_str("-e");136131137 // FIXME: security: created directory might be accessible to other users132 let out = host.mktemp_dir().await?;138 // This shouldn't be much of a concern, as data is encrypted right after creation, yet139 // still better to have.140 let tempdir = session.mktemp_dir().await?;141133142 let mut gen = session.cmd(built).await?;134 let mut gen = host.cmd(generator).await?;143 gen.env("rageArgs", recipients).env("out", &tempdir);135 gen.env("rageArgs", recipients).env("out", &out);144 gen.run().await?;136 gen.run().await?;145137146 {138 {147 let marker = session.read_file_text(format!("{tempdir}/marker")).await?;139 let marker = host.read_file_text(format!("{out}/marker")).await?;148 ensure!(marker == "SUCCESS", "generation not succeeded");140 ensure!(marker == "SUCCESS", "generation not succeeded");149 }141 }150142151 let public = session143 let public = host.read_file_text(format!("{out}/public")).await.ok();152 .read_file_bin(format!("{tempdir}/public"))144 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();153 .await145 if let Some(secret) = &secret {154 .ok();146 ensure!(155 let secret = session147 age::Decryptor::new(Cursor::new(&secret)).is_ok(),156 .read_file_bin(format!("{tempdir}/secret"))148 "builder produced non-encrypted value as secret, this is highly insecure"157 .await149 );158 .ok();150 }159 if let Some(secret) = &secret {151160 ensure!(152 let created_at = host.read_file_value(format!("{out}/created_at")).await?;161 age::Decryptor::new(Cursor::new(&secret)).is_ok(),153 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();162 "builder produced non-encrypted value as secret, this is highly insecure"154163 );155 FleetSharedSecret {164 }156 owners,165 dbg!(&secret);157 secret: FleetSecret {166 // // .as_json().await?;158 created_at,167 // dbg!(&built);159 expires_at,168 }160 public,161 secret: secret.map(SecretData),162 },163 }164 } else {165 bail!("no generator defined for {display_name}")166 })167}168169async fn parse_public(170 public: Option<String>,171 public_file: Option<PathBuf>,172) -> Result<Option<String>> {173 Ok(match (public, public_file) {174 (Some(v), None) => Some(v),175 (None, Some(v)) => Some(read_to_string(v).await?),176 (Some(_), Some(_)) => {177 bail!("only public or public_file should be set")178 }179 (None, None) => None,180 })181}182183fn parse_machines(184 initial: Vec<String>,185 machines: Option<Vec<String>>,186 mut add_machines: Vec<String>,187 mut remove_machines: Vec<String>,188) -> Result<Vec<String>> {189 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {190 bail!("no operation");191 }192193 let initial_machines = initial.clone();194 let mut target_machines = initial;195 info!("Currently encrypted for {initial_machines:?}");196197 // ensure!(machines.is_some() || !add_machines.is_empty() || )198 if let Some(machines) = machines {199 ensure!(200 add_machines.is_empty() && remove_machines.is_empty(),201 "can't combine --machines and --add-machines/--remove-machines"202 );203 let target = initial_machines.iter().collect::<HashSet<_>>();204 let source = machines.iter().collect::<HashSet<_>>();205 for removed in target.difference(&source) {206 remove_machines.push((*removed).clone());207 }208 for added in source.difference(&target) {209 add_machines.push((*added).clone());210 }211 }212213 for machine in &remove_machines {214 let mut removed = false;215 while let Some(pos) = target_machines.iter().position(|m| m == machine) {216 target_machines.swap_remove(pos);217 removed = true;218 }219 if !removed {220 warn!("secret is not enabled for {machine}");221 }222 }223 for machine in &add_machines {224 if target_machines.iter().any(|m| m == machine) {225 warn!("secret is already added to {machine}");226 } else {227 target_machines.push(machine.to_owned());228 }229 }230 if !remove_machines.is_empty() {231 // TODO: maybe force secret regeneration?232 // Not that useful without revokation.233 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");234 }235 Ok(target_machines)236}237impl Secret {238 pub async fn run(self, config: &Config) -> Result<()> {239 match self {169 Secret::ForceKeys => {240 Secret::ForceKeys => {170 for host in config.list_hosts().await? {241 for host in config.list_hosts().await? {171 if config.should_skip(&host.name) {242 if config.should_skip(&host.name) {199 machines = shared.owners;270 machines = shared.owners;200 }271 }201272202 let recipients = futures::stream::iter(machines.iter())273 let recipients = config203 .then(|m| config.recipient(m))274 .recipients(&machines.iter().map(String::as_str).collect_vec())204 .try_collect::<Vec<_>>()205 .await?;275 .await?;206276207 let secret = {277 let secret = {208 let mut input = vec![];278 let mut input = vec![];209 io::stdin().read_to_end(&mut input)?;279 io::stdin().read_to_end(&mut input)?;210280211 if input.is_empty() {281 if input.is_empty() {212 input282 None213 } else {283 } else {214 let mut encrypted = vec![];284 Some(215 let recipients = recipients216 .iter()217 .cloned()285 SecretData::encrypt(recipients, input)218 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)219 .collect();286 .ok_or_else(|| anyhow!("no recipients provided"))?,220 let mut encryptor = age::Encryptor::with_recipients(recipients)221 .ok_or_else(|| anyhow!("no recipients provided"))?222 .wrap_output(&mut encrypted)?;223 io::copy(&mut Cursor::new(input), &mut encryptor)?;224 encryptor.finish()?;287 )225 encrypted226 }288 }227 };289 };290 let public = parse_public(public, public_file).await?;228 config.replace_shared(291 config.replace_shared(229 name,292 name,230 FleetSharedSecret {293 FleetSharedSecret {233 created_at: Utc::now(),296 created_at: Utc::now(),234 expires_at,297 expires_at,235 secret,298 secret,236 public: match (public, public_file) {299 public,237 (Some(v), None) => Some(v),238 (None, Some(v)) => Some(read_to_string(v).await?),239 (Some(_), Some(_)) => {240 bail!("only public or public_file should be set")241 }242 (None, None) => None,243 },244 },300 },245 },301 },246 );302 );261 bail!("no data provided")317 bail!("no data provided")262 }318 }263319264 let mut encrypted = vec![];320 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))265 let recipient = Box::new(recipient) as Box<dyn age::Recipient + Send>;266 let mut encryptor = age::Encryptor::with_recipients(vec![recipient])267 .expect("recipients provided")268 .wrap_output(&mut encrypted)?;269 io::copy(&mut Cursor::new(input), &mut encryptor)?;270 encryptor.finish()?;271 encrypted272 };321 };273322274 if config.has_secret(&machine, &name) && !force {323 if config.has_secret(&machine, &name) && !force {275 bail!("secret already defined");324 bail!("secret already defined");276 }325 }326 let public = parse_public(public, public_file).await?;327277 config.insert_secret(328 config.insert_secret(278 &machine,329 &machine,279 name,330 name,280 FleetSecret {331 FleetSecret {281 created_at: Utc::now(),332 created_at: Utc::now(),282 expires_at: None,333 expires_at: None,283 secret,334 secret,284 public: match (public, public_file) {335 public,285 (Some(v), None) => Some(v),286 (None, Some(v)) => Some(std::fs::read_to_string(v)?),287 (Some(_), Some(_)) => bail!("only public or public_file should be set"),288 (None, None) => None,289 },290 },336 },291 );337 );292 }338 }293 // TODO: Instead of using sudo, decode secret on remote machine294 #[allow(clippy::await_holding_refcell_ref)]339 #[allow(clippy::await_holding_refcell_ref)]295 Secret::Read {340 Secret::Read {296 name,341 name,297 machine,342 machine,298 plaintext,343 plaintext,299 } => {344 } => {300 let secret = config.host_secret(&machine, &name)?;345 let secret = config.host_secret(&machine, &name)?;301 if secret.secret.is_empty() {346 let Some(secret) = secret.secret else {302 bail!("no secret {name}");347 bail!("no secret {name}");303 }348 };304 let host = config.host(&machine).await?;349 let host = config.host(&machine).await?;305 let data = host.decrypt(secret.secret).await?;350 let data = host.decrypt(secret).await?;306 if plaintext {351 if plaintext {307 let s = String::from_utf8(data).context("output is not utf8")?;352 let s = String::from_utf8(data).context("output is not utf8")?;308 print!("{s}");353 print!("{s}");313 Secret::UpdateShared {358 Secret::UpdateShared {314 name,359 name,315 machines,360 machines,316 mut add_machines,361 add_machines,317 mut remove_machines,362 remove_machines,318 prefer_identities,363 prefer_identities,319 } => {364 } => {320 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {321 bail!("no operation");322 }323324 let mut secret = config.shared_secret(&name)?;365 let mut secret = config.shared_secret(&name)?;325 if secret.secret.secret.is_empty() {366 if secret.secret.secret.is_none() {326 bail!("no secret");367 bail!("no secret");327 }368 }328369329 let initial_machines = secret.owners.clone();370 let initial_machines = secret.owners.clone();330 let mut target_machines = secret.owners.clone();371 let target_machines = parse_machines(372 initial_machines.clone(),331 info!("Currently encrypted for {initial_machines:?}");373 machines,374 add_machines,375 remove_machines,376 )?;332377333 // ensure!(machines.is_some() || !add_machines.is_empty() || )334 if let Some(machines) = machines {335 ensure!(336 add_machines.is_empty() && remove_machines.is_empty(),337 "can't combine --machines and --add-machines/--remove-machines"338 );339 let target = initial_machines.iter().collect::<HashSet<_>>();340 let source = machines.iter().collect::<HashSet<_>>();341 for removed in target.difference(&source) {342 remove_machines.push((*removed).clone());343 }344 for added in source.difference(&target) {345 add_machines.push((*added).clone());346 }347 }348349 for machine in &remove_machines {350 let mut removed = false;351 while let Some(pos) = target_machines.iter().position(|m| m == machine) {352 target_machines.swap_remove(pos);353 removed = true;354 }355 if !removed {356 warn!("secret is not enabled for {machine}");357 }358 }359 for machine in &add_machines {360 if target_machines.iter().any(|m| m == machine) {361 warn!("secret is already added to {machine}");362 } else {363 target_machines.push(machine.to_owned());364 }365 }366 if !remove_machines.is_empty() {367 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");368 }369370 if target_machines.is_empty() {378 if target_machines.is_empty() {371 info!("no machines left for secret, removing it");379 info!("no machines left for secret, removing it");372 config.remove_shared(&name);380 config.remove_shared(&name);395 let target_recipients =403 let target_recipients =396 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;404 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;397405398 let encrypted = config406 if let Some(data) = secret.secret.secret {407 let encrypted = config399 .reencrypt_on_host(identity_holder, secret.secret.secret, target_recipients)408 .reencrypt_on_host(identity_holder, data, target_recipients)400 .await?;409 .await?;410 secret.secret.secret = Some(encrypted);411 }401412402 secret.owners = target_machines;413 secret.owners = target_machines;403 secret.secret.secret = encrypted;404 config.replace_shared(name, secret);414 config.replace_shared(name, secret);405 }415 }406 Secret::Regenerate { prefer_identities } => {416 Secret::Regenerate { prefer_identities } => {412 .collect::<HashSet<_>>();422 .collect::<HashSet<_>>();413 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();423 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();414 for removed in expected_shared_set.difference(&shared_set) {424 for removed in expected_shared_set.difference(&shared_set) {415 error!("secret needs to be generated: {removed}")425 info!("generating secret: {removed}");426 let config_field = &config.config_unchecked_field;427 let config_field = nix_go!(config_field.configUnchecked);428 let secret = nix_go!(config_field.sharedSecrets[{ removed }]);429 let shared = generate_shared(config, removed, secret).await?;430 config.replace_shared(removed.to_string(), shared)416 }431 }417 }432 }418 let mut to_remove = Vec::new();433 let mut to_remove = Vec::new();419 for name in &config.list_shared() {434 for name in &config.list_shared() {420 info!("updating secret: {name}");435 info!("updating secret: {name}");421 let mut data = config.shared_secret(name)?;436 let mut data = config.shared_secret(name)?;422 let config_field = &config.config_field;437 let config_field = &config.config_unchecked_field;438 let config_field = nix_go!(config_field.configUnchecked);423 let expected_owners: Vec<String> =439 let expected_owners: Vec<String> =424 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);440 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);425 if expected_owners.is_empty() {441 if expected_owners.is_empty() {430 let set = data.owners.iter().collect::<HashSet<_>>();446 let set = data.owners.iter().collect::<HashSet<_>>();431 let expected_set = expected_owners.iter().collect::<HashSet<_>>();447 let expected_set = expected_owners.iter().collect::<HashSet<_>>();432 let should_remove = set.difference(&expected_set).next().is_some();448 let should_remove = set.difference(&expected_set).next().is_some();433 if set != expected_set {449 if set == expected_set {434 let owner_dependent: bool =450 info!("secret data is ok");435 nix_go_json!(config_field.sharedSecrets[{ name }].ownerDependent);436 if !owner_dependent {437 warn!("reencrypting secret '{name}' for new owner set");438 // TODO: force regeneration451 continue;439 if should_remove {440 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");441 }452 }442453443 let identity_holder = if !prefer_identities.is_empty() {454 let secret = nix_go!(config_field.sharedSecrets[{ name }]);444 prefer_identities455 let owner_dependent: bool = nix_go_json!(secret.ownerDependent);445 .iter()456 let regenerate_on_remove: bool = nix_go_json!(secret.regenerateOnOwnerRemoved);457 #[allow(clippy::nonminimal_bool)]446 .find(|i| data.owners.iter().any(|s| s == *i))458 if !owner_dependent && !(should_remove && regenerate_on_remove) {447 } else {459 warn!("reencrypting secret '{name}' for new owner set");448 data.owners.first()449 };460 // TODO: force regeneration450 let Some(identity_holder) = identity_holder else {461 if should_remove {451 bail!("no available holder found");462 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");452 };463 }453464454 let target_recipients = futures::stream::iter(&expected_owners)465 let identity_holder = if !prefer_identities.is_empty() {466 prefer_identities467 .iter()455 .then(|m| async { config.key(m).await })468 .find(|i| data.owners.iter().any(|s| s == *i))456 .collect::<Vec<_>>()469 } else {470 data.owners.first()457 .await;471 };458 let target_recipients =472 let Some(identity_holder) = identity_holder else {459 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;473 bail!("no available holder found");474 };460475476 let target_recipients = futures::stream::iter(&expected_owners)477 .then(|m| async { config.key(m).await })478 .collect::<Vec<_>>()479 .await;480 let target_recipients =481 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;482483 if let Some(secret) = data.secret.secret {461 let encrypted = config484 let encrypted = config462 .reencrypt_on_host(485 .reencrypt_on_host(identity_holder, secret, target_recipients)463 identity_holder,464 data.secret.secret,465 target_recipients,466 )467 .await?;486 .await?;468487469 data.secret.secret = encrypted;488 data.secret.secret = Some(encrypted);470 data.owners = expected_owners;471 config.replace_shared(name.to_owned(), data);472 } else {473 error!("secret '{name}' should be regenerated manually");474 }489 }490 data.owners = expected_owners;491 config.replace_shared(name.to_owned(), data);475 } else {492 } else {476 info!("secret data is ok")493 let shared = generate_shared(config, name, secret).await?;494 config.replace_shared(name.to_owned(), shared)477 }495 }478 }496 }479 for k in to_remove {497 for k in to_remove {cmds/fleet/src/fleetdata.rsdiffbeforeafterboth1use age::Recipient;1use anyhow::Result;2use anyhow::Result;2use chrono::{DateTime, Utc};3use chrono::{DateTime, Utc};4use itertools::Itertools;3use nixlike::format_nix;5use nixlike::format_nix;4use serde::{Deserialize, Deserializer, Serialize, Serializer};6use serde::{Deserialize, Deserializer, Serialize, Serializer};5use std::collections::BTreeMap;7use std::{8 collections::BTreeMap,9 io::{self, Cursor},10};6use tempfile::TempDir;11use tempfile::TempDir;7use tokio::{12use tokio::{8 fs::{self, File},13 fs::{self, File},40 pub secret: FleetSecret,45 pub secret: FleetSecret,41}46}4748#[derive(Serialize, Deserialize, Clone)]49pub struct SecretData(50 #[serde(51 default,52 skip_serializing_if = "Vec::is_empty",53 serialize_with = "as_z85",54 deserialize_with = "from_z85"55 )]56 pub Vec<u8>,57);58impl SecretData {59 /// Returns None if recipients.is_empty()60 pub fn encrypt(61 recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,62 data: Vec<u8>,63 ) -> Option<Self> {64 let mut encrypted = vec![];65 let recipients = recipients66 .into_iter()67 .map(|v| Box::new(v) as Box<dyn Recipient + Send>)68 .collect_vec();69 let mut encryptor = age::Encryptor::with_recipients(recipients)?70 .wrap_output(&mut encrypted)71 .expect("in memory write");72 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");73 encryptor.finish().expect("in memory flush");74 Some(Self(encrypted))75 }76 pub fn encode_z85(&self) -> String {77 z85::encode(&self.0)78 }79 pub fn decode_z85(v: &str) -> Result<Self> {80 let v = z85::decode(v)?;81 Ok(Self(v))82 }83}428443#[derive(Serialize, Deserialize, Clone)]85#[derive(Serialize, Deserialize, Clone)]44#[serde(rename_all = "camelCase")]86#[serde(rename_all = "camelCase")]51 pub expires_at: Option<DateTime<Utc>>,93 pub expires_at: Option<DateTime<Utc>>,52 #[serde(skip_serializing_if = "Option::is_none")]94 #[serde(skip_serializing_if = "Option::is_none")]53 pub public: Option<String>,95 pub public: Option<String>,54 #[serde(96 #[serde(skip_serializing_if = "Option::is_none")]55 default,56 skip_serializing_if = "Vec::is_empty",57 serialize_with = "as_z85",58 deserialize_with = "from_z85"59 )]60 pub secret: Vec<u8>,97 pub secret: Option<SecretData>,61}98}629963fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>100fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>cmds/fleet/src/host.rsdiffbeforeafterboth1use std::{1use std::{2 env::current_dir,2 env::current_dir,3 ffi::{OsStr, OsString},3 ffi::{OsStr, OsString},4 fmt::Display,4 io::Write,5 io::Write,5 ops::Deref,6 ops::Deref,6 path::PathBuf,7 path::PathBuf,8 str::FromStr,7 sync::{Arc, Mutex, MutexGuard, OnceLock},9 sync::{Arc, Mutex, MutexGuard, OnceLock},8};10};91112use age::Recipient;10use anyhow::{anyhow, bail, Context, Result};13use anyhow::{anyhow, bail, Context, Result};11use clap::{ArgGroup, Parser};14use clap::{ArgGroup, Parser};12use openssh::SessionBuilder;15use openssh::SessionBuilder;16use serde::de::DeserializeOwned;13use tempfile::NamedTempFile;17use tempfile::NamedTempFile;141815use crate::{19use crate::{16 better_nix_eval::{Field, NixSessionPool},20 better_nix_eval::{Field, NixSessionPool},17 command::MyCommand,21 command::MyCommand,18 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},22 fleetdata::{FleetData, FleetSecret, FleetSharedSecret, SecretData},19 nix_go, nix_go_json,23 nix_go, nix_go_json,20};24};212580 cmd.arg(path);84 cmd.arg(path);81 cmd.run_string().await85 cmd.run_string().await82 }86 }87 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {88 let text = self.read_file_text(path).await?;89 Ok(serde_json::from_str(&text)?)90 }91 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>92 where93 <D as FromStr>::Err: Display,94 {95 let text = self.read_file_text(path).await?;96 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))97 }83 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {98 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {84 let session = self.open_session().await?;99 let session = self.open_session().await?;85 Ok(MyCommand::new_on(cmd, session))100 Ok(MyCommand::new_on(cmd, session))86 }101 }8710288 pub async fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>> {103 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {89 let mut cmd = self.cmd("fleet-install-secrets").await?;104 let mut cmd = self.cmd("fleet-install-secrets").await?;90 cmd.arg("decrypt").eqarg("--secret", z85::encode(&data));105 cmd.arg("decrypt").eqarg("--secret", data.encode_z85());91 let encoded = cmd106 let encoded = cmd92 .sudo()107 .sudo()93 .run_string()108 .run_string()94 .await109 .await95 .context("failed to call remote host for decrypt")?;110 .context("failed to call remote host for decrypt")?;96 z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")111 z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")97 }112 }113 /// Returns path for futureproofing, as path might change i.e on conversion to CA114 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {115 let mut nix = MyCommand::new("nix");116 nix.arg("copy")117 .arg("--substitute-on-destination")118 .comparg("--to", format!("ssh-ng://{}", self.name))119 .arg(path);120 nix.run_nix().await?;121 Ok(path.to_owned())122 }98}123}99124100impl Config {125impl Config {166 }191 }167 /// Shared secrets configured in fleet.nix or in flake192 /// Shared secrets configured in fleet.nix or in flake168 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {193 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {169 let config_field = &self.config_field;194 let config_field = &self.config_unchecked_field;170 nix_go!(config_field.sharedSecrets).list_fields().await195 nix_go!(config_field.configUnchecked.sharedSecrets)196 .list_fields()197 .await171 }198 }203 pub async fn reencrypt_on_host(230 pub async fn reencrypt_on_host(204 &self,231 &self,205 host: &str,232 host: &str,206 data: Vec<u8>,233 data: SecretData,207 targets: Vec<String>,234 targets: Vec<String>,208 ) -> Result<Vec<u8>> {235 ) -> Result<SecretData> {209 let data = z85::encode(&data);210 let mut recmd = MyCommand::new("fleet-install-secrets");236 let mut recmd = MyCommand::new("fleet-install-secrets");211 recmd.arg("reencrypt").eqarg("--secret", data);237 recmd.arg("reencrypt").eqarg("--secret", data.encode_z85());212 for target in targets {238 for target in targets {213 recmd.eqarg("--targets", target);239 recmd.eqarg("--targets", target);214 }240 }219 .context("failed to call remote host for decrypt")?245 .context("failed to call remote host for decrypt")?220 .trim()246 .trim()221 .to_owned();247 .to_owned();222 z85::decode(encoded).context("bad encoded data? outdated host?")248 SecretData::decode_z85(&encoded)223 }249 }224250225 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {251 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {240 Ok(secret.clone())266 Ok(secret.clone())241 }267 }242 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {268 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {243 let config_field = &self.config_field;269 let config_field = &self.config_unchecked_field;244 Ok(nix_go_json!(270 Ok(nix_go_json!(245 config_field.sharedSecrets[{ secret }].expectedOwners271 config_field.configUnchecked.sharedSecrets[{ secret }].expectedOwners246 ))272 ))247 }273 }248274cmds/fleet/src/keys.rsdiffbeforeafterboth223use crate::command::MyCommand;3use crate::command::MyCommand;4use crate::host::Config;4use crate::host::Config;5use age::Recipient;5use anyhow::{anyhow, Result};6use anyhow::{anyhow, Result};7use futures::{StreamExt, TryStreamExt};6use itertools::Itertools;8use itertools::Itertools;7use tracing::warn;9use tracing::warn;81036 }38 }37 }39 }38 /// Insecure, requires root40 /// Insecure, requires root39 pub async fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {41 pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient> {40 let key = self.key(host).await?;42 let key = self.key(host).await?;41 age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))43 age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))42 }44 }4546 pub async fn recipients(&self, hosts: &[&str]) -> Result<Vec<impl Recipient>> {47 futures::stream::iter(hosts.iter())48 .then(|m| self.recipient(m))49 .try_collect::<Vec<_>>()50 .await51 }435244 #[allow(dead_code)]53 #[allow(dead_code)]45 pub async fn orphaned_data(&self) -> Result<Vec<String>> {54 pub async fn orphaned_data(&self) -> Result<Vec<String>> {crates/nixlike/src/lib.rsdiffbeforeafterboth10mod se_impl;10mod se_impl;11mod to_string;11mod to_string;1213pub use to_string::escape_string;121413#[derive(thiserror::Error, Debug)]15#[derive(thiserror::Error, Debug)]14pub enum Error {16pub enum Error {crates/nixlike/src/to_string.rsdiffbeforeafterboth25 }25 }26}26}272728fn write_nix_str(str: &str, out: &mut String) {28pub fn escape_string(str: &str) -> String {29 out.push_str(&format!(29 format!(30 "\"{}\"",30 "\"{}\"",31 str.replace('\\', "\\\\")31 str.replace('\\', "\\\\")32 .replace('"', "\\\"")32 .replace('"', "\\\"")33 .replace('\n', "\\n")33 .replace('\n', "\\n")34 .replace('\t', "\\t")34 .replace('\t', "\\t")35 .replace('\r', "\\r")35 .replace('\r', "\\r")36 .replace('$', "\\$")36 .replace('$', "\\$")37 ))37 )38}38}3940pub fn write_nix_str(str: &str, out: &mut String) {41 out.push_str(&escape_string(str))42}394340fn write_nix_buf(value: &Value, out: &mut String) {44fn write_nix_buf(value: &Value, out: &mut String) {41 match value {45 match value {