difftreelog
refactor minor rewrites
in: trunk
7 files changed
cmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth--- a/cmds/fleet/src/better_nix_eval.rs
+++ b/cmds/fleet/src/better_nix_eval.rs
@@ -247,7 +247,7 @@
Ok(())
}
async fn send_command(&mut self, cmd: impl AsRef<[u8]>) -> Result<()> {
- if tracing::enabled!(Level::DEBUG) {
+ if tracing::enabled!(Level::DEBUG) && cmd.as_ref() != REPL_DELIMITER.as_bytes() {
let cmd_str = String::from_utf8_lossy(cmd.as_ref());
tracing::debug!("{cmd_str}");
};
@@ -627,13 +627,6 @@
}
pub async fn field(session: NixSession, field: &str) -> Result<Self> {
Self::root(session).select([Index::var(field)]).await
- }
- pub async fn get_json_deep<'a, V: DeserializeOwned>(
- &self,
- name: impl IntoIterator<Item = Index>,
- ) -> Result<V> {
- let field = self.select(name).await?;
- field.as_json().await
}
pub async fn select<'a>(&self, name: impl IntoIterator<Item = Index>) -> Result<Self> {
let mut used_fields = Vec::new();
@@ -719,6 +712,19 @@
.await
.with_context(|| context(self.0.full_path.as_deref(), &query))
}
+ pub async fn has_field(&self, name: &str) -> Result<bool> {
+ let id = self.0.value.expect("can't list root fields");
+ let key = nixlike::escape_string(name);
+ let query = format!("sess_field_{id} ? {key}");
+ self.0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&query)
+ .await
+ .with_context(|| context(self.0.full_path.as_deref(), &query))
+ }
pub async fn list_fields(&self) -> Result<Vec<String>> {
let id = self.0.value.expect("can't list root fields");
let query = format!("builtins.attrNames sess_field_{id}");
@@ -731,6 +737,18 @@
.await
.with_context(|| context(self.0.full_path.as_deref(), &query))
}
+ pub async fn type_of(&self) -> Result<String> {
+ let id = self.0.value.expect("can't list root fields");
+ let query = format!("builtins.typeOf sess_field_{id}");
+ self.0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&query)
+ .await
+ .with_context(|| context(self.0.full_path.as_deref(), &query))
+ }
pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {
let id = self.0.value.expect("can't use build on not-value");
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.rsdiffbeforeafterboth--- a/cmds/fleet/src/fleetdata.rs
+++ b/cmds/fleet/src/fleetdata.rs
@@ -1,8 +1,13 @@
+use age::Recipient;
use anyhow::Result;
use chrono::{DateTime, Utc};
+use itertools::Itertools;
use nixlike::format_nix;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
-use std::collections::BTreeMap;
+use std::{
+ collections::BTreeMap,
+ io::{self, Cursor},
+};
use tempfile::TempDir;
use tokio::{
fs::{self, File},
@@ -41,6 +46,43 @@
}
#[derive(Serialize, Deserialize, Clone)]
+pub struct SecretData(
+ #[serde(
+ default,
+ skip_serializing_if = "Vec::is_empty",
+ serialize_with = "as_z85",
+ deserialize_with = "from_z85"
+ )]
+ pub Vec<u8>,
+);
+impl SecretData {
+ /// Returns None if recipients.is_empty()
+ pub fn encrypt(
+ recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,
+ data: Vec<u8>,
+ ) -> Option<Self> {
+ 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(Self(encrypted))
+ }
+ pub fn encode_z85(&self) -> String {
+ z85::encode(&self.0)
+ }
+ pub fn decode_z85(v: &str) -> Result<Self> {
+ let v = z85::decode(v)?;
+ Ok(Self(v))
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[must_use]
pub struct FleetSecret {
@@ -51,13 +93,8 @@
pub expires_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public: Option<String>,
- #[serde(
- default,
- skip_serializing_if = "Vec::is_empty",
- serialize_with = "as_z85",
- deserialize_with = "from_z85"
- )]
- pub secret: Vec<u8>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub secret: Option<SecretData>,
}
fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>
cmds/fleet/src/host.rsdiffbeforeafterboth--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -1,21 +1,25 @@
use std::{
env::current_dir,
ffi::{OsStr, OsString},
+ fmt::Display,
io::Write,
ops::Deref,
path::PathBuf,
+ str::FromStr,
sync::{Arc, Mutex, MutexGuard, OnceLock},
};
+use age::Recipient;
use anyhow::{anyhow, bail, Context, Result};
use clap::{ArgGroup, Parser};
use openssh::SessionBuilder;
+use serde::de::DeserializeOwned;
use tempfile::NamedTempFile;
use crate::{
better_nix_eval::{Field, NixSessionPool},
command::MyCommand,
- fleetdata::{FleetData, FleetSecret, FleetSharedSecret},
+ fleetdata::{FleetData, FleetSecret, FleetSharedSecret, SecretData},
nix_go, nix_go_json,
};
@@ -80,14 +84,25 @@
cmd.arg(path);
cmd.run_string().await
}
+ pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {
+ let text = self.read_file_text(path).await?;
+ Ok(serde_json::from_str(&text)?)
+ }
+ pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>
+ where
+ <D as FromStr>::Err: Display,
+ {
+ let text = self.read_file_text(path).await?;
+ D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))
+ }
pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {
let session = self.open_session().await?;
Ok(MyCommand::new_on(cmd, session))
}
- pub async fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>> {
+ pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {
let mut cmd = self.cmd("fleet-install-secrets").await?;
- cmd.arg("decrypt").eqarg("--secret", z85::encode(&data));
+ cmd.arg("decrypt").eqarg("--secret", data.encode_z85());
let encoded = cmd
.sudo()
.run_string()
@@ -95,6 +110,16 @@
.context("failed to call remote host for decrypt")?;
z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")
}
+ /// Returns path for futureproofing, as path might change i.e on conversion to CA
+ pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {
+ let mut nix = MyCommand::new("nix");
+ nix.arg("copy")
+ .arg("--substitute-on-destination")
+ .comparg("--to", format!("ssh-ng://{}", self.name))
+ .arg(path);
+ nix.run_nix().await?;
+ Ok(path.to_owned())
+ }
}
impl Config {
@@ -166,8 +191,10 @@
}
/// Shared secrets configured in fleet.nix or in flake
pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
- let config_field = &self.config_field;
- nix_go!(config_field.sharedSecrets).list_fields().await
+ let config_field = &self.config_unchecked_field;
+ nix_go!(config_field.configUnchecked.sharedSecrets)
+ .list_fields()
+ .await
}
/// Shared secrets configured in fleet.nix
pub fn list_shared(&self) -> Vec<String> {
@@ -203,12 +230,11 @@
pub async fn reencrypt_on_host(
&self,
host: &str,
- data: Vec<u8>,
+ data: SecretData,
targets: Vec<String>,
- ) -> Result<Vec<u8>> {
- let data = z85::encode(&data);
+ ) -> Result<SecretData> {
let mut recmd = MyCommand::new("fleet-install-secrets");
- recmd.arg("reencrypt").eqarg("--secret", data);
+ recmd.arg("reencrypt").eqarg("--secret", data.encode_z85());
for target in targets {
recmd.eqarg("--targets", target);
}
@@ -219,7 +245,7 @@
.context("failed to call remote host for decrypt")?
.trim()
.to_owned();
- z85::decode(encoded).context("bad encoded data? outdated host?")
+ SecretData::decode_z85(&encoded)
}
pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {
@@ -240,9 +266,9 @@
Ok(secret.clone())
}
pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {
- let config_field = &self.config_field;
+ let config_field = &self.config_unchecked_field;
Ok(nix_go_json!(
- config_field.sharedSecrets[{ secret }].expectedOwners
+ config_field.configUnchecked.sharedSecrets[{ secret }].expectedOwners
))
}
cmds/fleet/src/keys.rsdiffbeforeafterboth--- a/cmds/fleet/src/keys.rs
+++ b/cmds/fleet/src/keys.rs
@@ -2,7 +2,9 @@
use crate::command::MyCommand;
use crate::host::Config;
+use age::Recipient;
use anyhow::{anyhow, Result};
+use futures::{StreamExt, TryStreamExt};
use itertools::Itertools;
use tracing::warn;
@@ -36,11 +38,18 @@
}
}
/// Insecure, requires root
- pub async fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {
+ pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient> {
let key = self.key(host).await?;
age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
}
+ pub async fn recipients(&self, hosts: &[&str]) -> Result<Vec<impl Recipient>> {
+ futures::stream::iter(hosts.iter())
+ .then(|m| self.recipient(m))
+ .try_collect::<Vec<_>>()
+ .await
+ }
+
#[allow(dead_code)]
pub async fn orphaned_data(&self) -> Result<Vec<String>> {
let mut out = Vec::new();
crates/nixlike/src/lib.rsdiffbeforeafterboth--- a/crates/nixlike/src/lib.rs
+++ b/crates/nixlike/src/lib.rs
@@ -10,6 +10,8 @@
mod se_impl;
mod to_string;
+pub use to_string::escape_string;
+
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("bad number")]
crates/nixlike/src/to_string.rsdiffbeforeafterboth--- a/crates/nixlike/src/to_string.rs
+++ b/crates/nixlike/src/to_string.rs
@@ -25,8 +25,8 @@
}
}
-fn write_nix_str(str: &str, out: &mut String) {
- out.push_str(&format!(
+pub fn escape_string(str: &str) -> String {
+ format!(
"\"{}\"",
str.replace('\\', "\\\\")
.replace('"', "\\\"")
@@ -34,7 +34,11 @@
.replace('\t', "\\t")
.replace('\r', "\\r")
.replace('$', "\\$")
- ))
+ )
+}
+
+pub fn write_nix_str(str: &str, out: &mut String) {
+ out.push_str(&escape_string(str))
}
fn write_nix_buf(value: &Value, out: &mut String) {