git.delta.rocks / jrsonnet / refs/commits / 904d12180e52

difftreelog

refactor minor rewrites

Yaroslav Bolyukin2023-12-28parent: #a369041.patch.diff
in: trunk

7 files changed

modifiedcmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth
247 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)]).await
630 }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().await
637 }
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 .await
720 .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.0
720 .session
721 .0
722 .lock()
723 .await
724 .execute_expression_to_json(&query)
725 .await
726 .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 .await
732 .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.0
744 .session
745 .0
746 .lock()
747 .await
748 .execute_expression_to_json(&query)
749 .await
750 .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}");
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
1use 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};
2121
22#[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}
9594
96impl 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);
101104
102 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 .config
111 .nixpkgs111 .nixpkgs
112 .resolvedPkgs112 .resolvedPkgs
113 .callPackage113 .callPackage
114 );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?;
124115
125 let session = config.host(&on).await?;116 let host = config.host(&on).await?;
126117
127 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 = generator
121 .get("out")
122 .ok_or_else(|| anyhow!("missing generateImpure out"))?;
123 let generator = host.remote_derivation(generator).await?;
129124
130 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");
136131
137 // 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, yet
139 // still better to have.
140 let tempdir = session.mktemp_dir().await?;
141133
142 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?;
145137
146 {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 }
150142
151 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 {151
160 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"154
163 );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}
168
169async 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}
182
183fn 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 }
192
193 let initial_machines = initial.clone();
194 let mut target_machines = initial;
195 info!("Currently encrypted for {initial_machines:?}");
196
197 // 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 }
212
213 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 }
201272
202 let recipients = futures::stream::iter(machines.iter())273 let recipients = config
203 .then(|m| config.recipient(m))274 .recipients(&machines.iter().map(String::as_str).collect_vec())
204 .try_collect::<Vec<_>>()
205 .await?;275 .await?;
206276
207 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)?;
210280
211 if input.is_empty() {281 if input.is_empty() {
212 input282 None
213 } else {283 } else {
214 let mut encrypted = vec![];284 Some(
215 let recipients = recipients
216 .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 encrypted
226 }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 }
263319
264 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 encrypted
272 };321 };
273322
274 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?;
327
277 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 machine
294 #[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 }
323
324 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 }
328369
329 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 )?;
332377
333 // 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 }
348
349 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 }
369
370 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<_>>>()?;
397405
398 let encrypted = config406 if let Some(data) = secret.secret.secret {
407 let encrypted = config
399 .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 }
401412
402 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 }
442453
443 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 regeneration
450 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 }
453464
454 let target_recipients = futures::stream::iter(&expected_owners)465 let identity_holder = if !prefer_identities.is_empty() {
466 prefer_identities
467 .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 };
460475
476 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<_>>>()?;
482
483 if let Some(secret) = data.secret.secret {
461 let encrypted = config484 let encrypted = config
462 .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?;
468487
469 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 {
modifiedcmds/fleet/src/fleetdata.rsdiffbeforeafterboth
1use 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}
47
48#[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 = recipients
66 .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}
4284
43#[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}
6299
63fn 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>
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
1use 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};
911
12use 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;
1418
15use 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};
2125
80 cmd.arg(path);84 cmd.arg(path);
81 cmd.run_string().await85 cmd.run_string().await
82 }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 where
93 <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 }
87102
88 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 = cmd
92 .sudo()107 .sudo()
93 .run_string()108 .run_string()
94 .await109 .await
95 .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 CA
114 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}
99124
100impl Config {125impl Config {
166 }191 }
167 /// Shared secrets configured in fleet.nix or in flake192 /// Shared secrets configured in fleet.nix or in flake
168 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 .await
171 }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 }
224250
225 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 }].expectedOwners
246 ))272 ))
247 }273 }
248274
modifiedcmds/fleet/src/keys.rsdiffbeforeafterboth
22
3use 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;
810
36 }38 }
37 }39 }
38 /// Insecure, requires root40 /// Insecure, requires root
39 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 }
45
46 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 .await
51 }
4352
44 #[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>> {
modifiedcrates/nixlike/src/lib.rsdiffbeforeafterboth
10mod se_impl;10mod se_impl;
11mod to_string;11mod to_string;
12
13pub use to_string::escape_string;
1214
13#[derive(thiserror::Error, Debug)]15#[derive(thiserror::Error, Debug)]
14pub enum Error {16pub enum Error {
modifiedcrates/nixlike/src/to_string.rsdiffbeforeafterboth
25 }25 }
26}26}
2727
28fn 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}
39
40pub fn write_nix_str(str: &str, out: &mut String) {
41 out.push_str(&escape_string(str))
42}
3943
40fn write_nix_buf(value: &Value, out: &mut String) {44fn write_nix_buf(value: &Value, out: &mut String) {
41 match value {45 match value {