1use crate::{2 better_nix_eval::Field,3 fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4 host::Config,5 nix_go, nix_go_json,6};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use futures::StreamExt;11use itertools::Itertools;12use owo_colors::OwoColorize;13use serde::Deserialize;14use std::{15 collections::{BTreeSet, HashSet},16 io::{self, Cursor, Read},17 path::PathBuf,18};19use tabled::{Table, Tabled};20use tokio::fs::read_to_string;21use tracing::{error, info, info_span, warn, Instrument};2223#[derive(Parser)]24pub enum Secret {25 26 ForceKeys,27 28 AddShared {29 30 name: String,31 32 machines: Vec<String>,33 34 #[clap(long)]35 force: bool,36 37 #[clap(long)]38 public: Option<String>,39 40 #[clap(long)]41 public_file: Option<PathBuf>,4243 44 #[clap(long)]45 expires_at: Option<DateTime<Utc>>,4647 48 #[clap(long)]49 re_add: bool,50 },51 52 Add {53 54 name: String,55 56 machine: String,57 58 #[clap(long)]59 force: bool,60 #[clap(long)]61 public: Option<String>,62 #[clap(long)]63 public_file: Option<PathBuf>,64 },65 66 Read {67 name: String,68 machine: String,69 #[clap(long)]70 plaintext: bool,71 },72 UpdateShared {73 name: String,7475 #[clap(long)]76 machines: Option<Vec<String>>,7778 #[clap(long)]79 add_machines: Vec<String>,80 #[clap(long)]81 remove_machines: Vec<String>,8283 84 #[clap(long)]85 prefer_identities: Vec<String>,86 },87 Regenerate {88 89 90 #[clap(long)]91 prefer_identities: Vec<String>,92 },93 List {},94}9596#[tracing::instrument(skip(config, secret, field, prefer_identities))]97async fn update_owner_set(98 secret_name: &str,99 config: &Config,100 mut secret: FleetSharedSecret,101 field: Field,102 updated_set: &[String],103 prefer_identities: &[String],104) -> Result<FleetSharedSecret> {105 let original_set = secret.owners.clone();106107 let set = original_set.iter().collect::<BTreeSet<_>>();108 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();109110 if set == expected_set {111 info!("no need to update owner list, it is already correct");112 return Ok(secret);113 }114115 let should_regenerate = if set.difference(&expected_set).next().is_some() {116 117 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");118 nix_go_json!(field.regenerateOnOwnerRemoved)119 } else if expected_set.difference(&set).next().is_some() {120 nix_go_json!(field.regenerateOnOwnerAdded)121 } else {122 false123 };124125 if should_regenerate {126 info!("secret is owner-dependent, will regenerate");127 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;128 Ok(generated)129 } else {130 let identity_holder = if !prefer_identities.is_empty() {131 prefer_identities132 .iter()133 .find(|i| original_set.iter().any(|s| s == *i))134 } else {135 secret.owners.first()136 };137 let Some(identity_holder) = identity_holder else {138 bail!("no available holder found");139 };140141 if let Some(data) = secret.secret.secret {142 let host = config.host(identity_holder).await?;143 let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;144 secret.secret.secret = Some(encrypted);145 }146147 secret.owners = updated_set.to_vec();148 Ok(secret)149 }150}151152#[derive(Deserialize)]153#[serde(rename_all = "camelCase")]154enum GeneratorKind {155 Impure,156}157158async fn generate_impure(159 config: &Config,160 _display_name: &str,161 secret: Field,162 default_generator: Field,163 owners: &[String],164) -> Result<FleetSecret> {165 let config_field = &config.config_unchecked_field;166 let generator = nix_go!(secret.generator);167168 let on: String = nix_go_json!(default_generator.impureOn);169 let call_package = nix_go!(170 config_field.hosts[{ on }]171 .nixosSystem172 .config173 .nixpkgs174 .resolvedPkgs175 .callPackage176 );177178 let host = config.host(&on).await?;179180 let generator = nix_go!(call_package(generator)(Obj {}));181 let generator = generator.build().await?;182 let generator = generator183 .get("out")184 .ok_or_else(|| anyhow!("missing generateImpure out"))?;185 let generator = host.remote_derivation(generator).await?;186187 let mut recipients = String::new();188 for owner in owners {189 let key = config.key(owner).await?;190 recipients.push_str(&format!("-r \"{key}\" "));191 }192 recipients.push_str("-e");193194 let out = host.mktemp_dir().await?;195196 let mut gen = host.cmd(generator).await?;197 gen.env("rageArgs", recipients).env("out", &out);198 gen.run().await.context("impure generator")?;199200 {201 let marker = host.read_file_text(format!("{out}/marker")).await?;202 ensure!(marker == "SUCCESS", "generation not succeeded");203 }204205 let public = host.read_file_text(format!("{out}/public")).await.ok();206 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();207 if let Some(secret) = &secret {208 ensure!(209 age::Decryptor::new(Cursor::new(&secret)).is_ok(),210 "builder produced non-encrypted value as secret, this is highly insecure, and not allowed."211 );212 }213214 let created_at = host.read_file_value(format!("{out}/created_at")).await?;215 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();216217 Ok(FleetSecret {218 created_at,219 expires_at,220 public,221 secret: secret.map(SecretData),222 })223}224async fn generate(225 config: &Config,226 display_name: &str,227 secret: Field,228 owners: &[String],229) -> Result<FleetSecret> {230 let generator = nix_go!(secret.generator);231 232 {233 let gen_ty = generator.type_of().await?;234 if gen_ty == "null" {235 bail!("secret has no generator defined, can't automatically generate it.");236 }237 if gen_ty != "lambda" {238 bail!("generator should be lambda, got {gen_ty}");239 }240 }241 let default_pkgs = &config.default_pkgs;242 let default_call_package = nix_go!(default_pkgs.callPackage);243 244 245 246 247 248 249 250 251 let default_generator = nix_go!(default_call_package(generator)(Obj {}));252253 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);254255 match kind {256 GeneratorKind::Impure => {257 generate_impure(config, display_name, secret, default_generator, owners).await258 }259 }260}261async fn generate_shared(262 config: &Config,263 display_name: &str,264 secret: Field,265 expected_owners: Vec<String>,266) -> Result<FleetSharedSecret> {267 268 Ok(FleetSharedSecret {269 secret: generate(config, display_name, secret, &expected_owners).await?,270 owners: expected_owners,271 })272}273274async fn parse_public(275 public: Option<String>,276 public_file: Option<PathBuf>,277) -> Result<Option<String>> {278 Ok(match (public, public_file) {279 (Some(v), None) => Some(v),280 (None, Some(v)) => Some(read_to_string(v).await?),281 (Some(_), Some(_)) => {282 bail!("only public or public_file should be set")283 }284 (None, None) => None,285 })286}287288fn parse_machines(289 initial: Vec<String>,290 machines: Option<Vec<String>>,291 mut add_machines: Vec<String>,292 mut remove_machines: Vec<String>,293) -> Result<Vec<String>> {294 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {295 bail!("no operation");296 }297298 let initial_machines = initial.clone();299 let mut target_machines = initial;300 info!("Currently encrypted for {initial_machines:?}");301302 303 if let Some(machines) = machines {304 ensure!(305 add_machines.is_empty() && remove_machines.is_empty(),306 "can't combine --machines and --add-machines/--remove-machines"307 );308 let target = initial_machines.iter().collect::<HashSet<_>>();309 let source = machines.iter().collect::<HashSet<_>>();310 for removed in target.difference(&source) {311 remove_machines.push((*removed).clone());312 }313 for added in source.difference(&target) {314 add_machines.push((*added).clone());315 }316 }317318 for machine in &remove_machines {319 let mut removed = false;320 while let Some(pos) = target_machines.iter().position(|m| m == machine) {321 target_machines.swap_remove(pos);322 removed = true;323 }324 if !removed {325 warn!("secret is not enabled for {machine}");326 }327 }328 for machine in &add_machines {329 if target_machines.iter().any(|m| m == machine) {330 warn!("secret is already added to {machine}");331 } else {332 target_machines.push(machine.to_owned());333 }334 }335 if !remove_machines.is_empty() {336 337 338 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");339 }340 Ok(target_machines)341}342impl Secret {343 pub async fn run(self, config: &Config) -> Result<()> {344 match self {345 Secret::ForceKeys => {346 for host in config.list_hosts().await? {347 if config.should_skip(&host.name) {348 continue;349 }350 config.key(&host.name).await?;351 }352 }353 Secret::AddShared {354 mut machines,355 name,356 force,357 public,358 public_file,359 expires_at,360 re_add,361 } => {362 let exists = config.has_shared(&name);363 if exists && !force && !re_add {364 bail!("secret already defined");365 }366 if re_add {367 368 ensure!(!force, "--force and --readd are not compatible");369 ensure!(exists, "secret doesn't exists");370 ensure!(371 machines.is_empty(),372 "you can't use machines argument for --readd"373 );374 let shared = config.shared_secret(&name)?;375 machines = shared.owners;376 }377378 let recipients = config.recipients(machines.clone()).await?;379380 let secret = {381 let mut input = vec![];382 io::stdin().read_to_end(&mut input)?;383384 if input.is_empty() {385 None386 } else {387 Some(388 SecretData::encrypt(recipients, input)389 .ok_or_else(|| anyhow!("no recipients provided"))?,390 )391 }392 };393 let public = parse_public(public, public_file).await?;394 config.replace_shared(395 name,396 FleetSharedSecret {397 owners: machines,398 secret: FleetSecret {399 created_at: Utc::now(),400 expires_at,401 secret,402 public,403 },404 },405 );406 }407 Secret::Add {408 machine,409 name,410 force,411 public,412 public_file,413 } => {414 let recipient = config.recipient(&machine).await?;415416 let secret = {417 let mut input = vec![];418 io::stdin().read_to_end(&mut input)?;419 if input.is_empty() {420 bail!("no data provided")421 }422423 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))424 };425426 if config.has_secret(&machine, &name) && !force {427 bail!("secret already defined");428 }429 let public = parse_public(public, public_file).await?;430431 config.insert_secret(432 &machine,433 name,434 FleetSecret {435 created_at: Utc::now(),436 expires_at: None,437 secret,438 public,439 },440 );441 }442 #[allow(clippy::await_holding_refcell_ref)]443 Secret::Read {444 name,445 machine,446 plaintext,447 } => {448 let secret = config.host_secret(&machine, &name)?;449 let Some(secret) = secret.secret else {450 bail!("no secret {name}");451 };452 let host = config.host(&machine).await?;453 let data = host.decrypt(secret).await?;454 if plaintext {455 let s = String::from_utf8(data).context("output is not utf8")?;456 print!("{s}");457 } else {458 println!("{}", z85::encode(&data));459 }460 }461 Secret::UpdateShared {462 name,463 machines,464 add_machines,465 remove_machines,466 prefer_identities,467 } => {468 let secret = config.shared_secret(&name)?;469 if secret.secret.secret.is_none() {470 bail!("no secret");471 }472473 let initial_machines = secret.owners.clone();474 let target_machines = parse_machines(475 initial_machines.clone(),476 machines,477 add_machines,478 remove_machines,479 )?;480481 if target_machines.is_empty() {482 info!("no machines left for secret, removing it");483 config.remove_shared(&name);484 return Ok(());485 }486487 let config_field = &config.config_unchecked_field;488 let field = nix_go!(config_field.sharedSecrets[{ name }]);489490 let updated = update_owner_set(491 &name,492 config,493 secret,494 field,495 &target_machines,496 &prefer_identities,497 )498 .await?;499 config.replace_shared(name, updated);500 }501 Secret::Regenerate { prefer_identities } => {502 info!("checking for secrets to regenerate");503 {504 let _span = info_span!("shared").entered();505 let expected_shared_set = config506 .list_configured_shared()507 .await?508 .into_iter()509 .collect::<HashSet<_>>();510 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();511 for missing in expected_shared_set.difference(&shared_set) {512 let config_field = &config.config_unchecked_field;513 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);514 let expected_owners: Option<Vec<String>> =515 nix_go_json!(secret.expectedOwners);516 let Some(expected_owners) = expected_owners else {517 518 continue;519 };520 info!("generating secret: {missing}");521 let shared = generate_shared(config, missing, secret, expected_owners)522 .in_current_span()523 .await?;524 config.replace_shared(missing.to_string(), shared)525 }526 }527 for host in config.list_hosts().await? {528 let _span = info_span!("host", host = host.name).entered();529 let expected_set = host530 .list_configured_secrets()531 .in_current_span()532 .await?533 .into_iter()534 .collect::<HashSet<_>>();535 let stored_set = config536 .list_secrets(&host.name)537 .into_iter()538 .collect::<HashSet<_>>();539 for missing in expected_set.difference(&stored_set) {540 info!("generating secret: {missing}");541 let secret = host.secret_field(missing).in_current_span().await?;542 let generated =543 match generate(config, missing, secret, &[host.name.clone()])544 .in_current_span()545 .await546 {547 Ok(v) => v,548 Err(e) => {549 error!("{e}");550 continue;551 }552 };553 config.insert_secret(&host.name, missing.to_string(), generated)554 }555 }556 let mut to_remove = Vec::new();557 for name in &config.list_shared() {558 info!("updating secret: {name}");559 let data = config.shared_secret(name)?;560 let config_field = &config.config_unchecked_field;561 let expected_owners: Vec<String> =562 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);563 if expected_owners.is_empty() {564 warn!("secret was removed from fleet config: {name}, removing from data");565 to_remove.push(name.to_string());566 continue;567 }568569 let secret = nix_go!(config_field.sharedSecrets[{ name }]);570 config.replace_shared(571 name.to_owned(),572 update_owner_set(573 &name,574 config,575 data,576 secret,577 &expected_owners,578 &prefer_identities,579 )580 .await?,581 );582 }583 for k in to_remove {584 config.remove_shared(&k);585 }586 }587 Secret::List {} => {588 let _span = info_span!("loading secrets").entered();589 let configured = config.list_configured_shared().await?;590 #[derive(Tabled)]591 struct SecretDisplay {592 #[tabled(rename = "Name")]593 name: String,594 #[tabled(rename = "Owners")]595 owners: String,596 }597 let mut table = vec![];598 for name in configured.iter().cloned() {599 let config = config.clone();600 let expected_owners = config.shared_secret_expected_owners(&name).await?;601 let data = config.shared_secret(&name)?;602 let owners = data603 .owners604 .iter()605 .map(|o| {606 if expected_owners.contains(o) {607 o.green().to_string()608 } else {609 o.red().to_string()610 }611 })612 .collect::<Vec<_>>();613 table.push(SecretDisplay {614 owners: owners.join(", "),615 name,616 })617 }618 info!("loaded\n{}", Table::new(table).to_string())619 }620 }621 Ok(())622 }623}
1use crate::{2 better_nix_eval::Field,3 fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4 host::Config,5 nix_go, nix_go_json,6};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use owo_colors::OwoColorize;11use serde::Deserialize;12use std::{13 collections::{BTreeSet, HashSet},14 io::{self, Cursor, Read},15 path::PathBuf,16};17use tabled::{Table, Tabled};18use tokio::fs::read_to_string;19use tracing::{error, info, info_span, warn, Instrument};2021#[derive(Parser)]22pub enum Secret {23 24 ForceKeys,25 26 AddShared {27 28 name: String,29 30 machines: Vec<String>,31 32 #[clap(long)]33 force: bool,34 35 #[clap(long)]36 public: Option<String>,37 38 #[clap(long)]39 public_file: Option<PathBuf>,4041 42 #[clap(long)]43 expires_at: Option<DateTime<Utc>>,4445 46 #[clap(long)]47 re_add: bool,48 },49 50 Add {51 52 name: String,53 54 machine: String,55 56 #[clap(long)]57 force: bool,58 #[clap(long)]59 public: Option<String>,60 #[clap(long)]61 public_file: Option<PathBuf>,62 },63 64 Read {65 name: String,66 machine: String,67 #[clap(long)]68 plaintext: bool,69 },70 UpdateShared {71 name: String,7273 #[clap(long)]74 machines: Option<Vec<String>>,7576 #[clap(long)]77 add_machines: Vec<String>,78 #[clap(long)]79 remove_machines: Vec<String>,8081 82 #[clap(long)]83 prefer_identities: Vec<String>,84 },85 Regenerate {86 87 88 #[clap(long)]89 prefer_identities: Vec<String>,90 },91 List {},92}9394#[tracing::instrument(skip(config, secret, field, prefer_identities))]95async fn update_owner_set(96 secret_name: &str,97 config: &Config,98 mut secret: FleetSharedSecret,99 field: Field,100 updated_set: &[String],101 prefer_identities: &[String],102) -> Result<FleetSharedSecret> {103 let original_set = secret.owners.clone();104105 let set = original_set.iter().collect::<BTreeSet<_>>();106 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();107108 if set == expected_set {109 info!("no need to update owner list, it is already correct");110 return Ok(secret);111 }112113 let should_regenerate = if set.difference(&expected_set).next().is_some() {114 115 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");116 nix_go_json!(field.regenerateOnOwnerRemoved)117 } else if expected_set.difference(&set).next().is_some() {118 nix_go_json!(field.regenerateOnOwnerAdded)119 } else {120 false121 };122123 if should_regenerate {124 info!("secret is owner-dependent, will regenerate");125 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;126 Ok(generated)127 } else {128 let identity_holder = if !prefer_identities.is_empty() {129 prefer_identities130 .iter()131 .find(|i| original_set.iter().any(|s| s == *i))132 } else {133 secret.owners.first()134 };135 let Some(identity_holder) = identity_holder else {136 bail!("no available holder found");137 };138139 if let Some(data) = secret.secret.secret {140 let host = config.host(identity_holder).await?;141 let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;142 secret.secret.secret = Some(encrypted);143 }144145 secret.owners = updated_set.to_vec();146 Ok(secret)147 }148}149150#[derive(Deserialize)]151#[serde(rename_all = "camelCase")]152enum GeneratorKind {153 Impure,154}155156async fn generate_impure(157 config: &Config,158 _display_name: &str,159 secret: Field,160 default_generator: Field,161 owners: &[String],162) -> Result<FleetSecret> {163 let config_field = &config.config_unchecked_field;164 let generator = nix_go!(secret.generator);165166 let on: String = nix_go_json!(default_generator.impureOn);167 let call_package = nix_go!(168 config_field.hosts[{ on }]169 .nixosSystem170 .config171 .nixpkgs172 .resolvedPkgs173 .callPackage174 );175176 let host = config.host(&on).await?;177178 let generator = nix_go!(call_package(generator)(Obj {}));179 let generator = generator.build().await?;180 let generator = generator181 .get("out")182 .ok_or_else(|| anyhow!("missing generateImpure out"))?;183 let generator = host.remote_derivation(generator).await?;184185 let mut recipients = String::new();186 for owner in owners {187 let key = config.key(owner).await?;188 recipients.push_str(&format!("-r \"{key}\" "));189 }190 recipients.push_str("-e");191192 let out = host.mktemp_dir().await?;193194 let mut gen = host.cmd(generator).await?;195 gen.env("rageArgs", recipients).env("out", &out);196 gen.run().await.context("impure generator")?;197198 {199 let marker = host.read_file_text(format!("{out}/marker")).await?;200 ensure!(marker == "SUCCESS", "generation not succeeded");201 }202203 let public = host.read_file_text(format!("{out}/public")).await.ok();204 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();205 if let Some(secret) = &secret {206 ensure!(207 age::Decryptor::new(Cursor::new(&secret)).is_ok(),208 "builder produced non-encrypted value as secret, this is highly insecure, and not allowed."209 );210 }211212 let created_at = host.read_file_value(format!("{out}/created_at")).await?;213 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();214215 Ok(FleetSecret {216 created_at,217 expires_at,218 public,219 secret: secret.map(SecretData),220 })221}222async fn generate(223 config: &Config,224 display_name: &str,225 secret: Field,226 owners: &[String],227) -> Result<FleetSecret> {228 let generator = nix_go!(secret.generator);229 230 {231 let gen_ty = generator.type_of().await?;232 if gen_ty == "null" {233 bail!("secret has no generator defined, can't automatically generate it.");234 }235 if gen_ty != "lambda" {236 bail!("generator should be lambda, got {gen_ty}");237 }238 }239 let default_pkgs = &config.default_pkgs;240 let default_call_package = nix_go!(default_pkgs.callPackage);241 242 243 244 245 246 247 248 249 let default_generator = nix_go!(default_call_package(generator)(Obj {}));250251 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);252253 match kind {254 GeneratorKind::Impure => {255 generate_impure(config, display_name, secret, default_generator, owners).await256 }257 }258}259async fn generate_shared(260 config: &Config,261 display_name: &str,262 secret: Field,263 expected_owners: Vec<String>,264) -> Result<FleetSharedSecret> {265 266 Ok(FleetSharedSecret {267 secret: generate(config, display_name, secret, &expected_owners).await?,268 owners: expected_owners,269 })270}271272async fn parse_public(273 public: Option<String>,274 public_file: Option<PathBuf>,275) -> Result<Option<String>> {276 Ok(match (public, public_file) {277 (Some(v), None) => Some(v),278 (None, Some(v)) => Some(read_to_string(v).await?),279 (Some(_), Some(_)) => {280 bail!("only public or public_file should be set")281 }282 (None, None) => None,283 })284}285286fn parse_machines(287 initial: Vec<String>,288 machines: Option<Vec<String>>,289 mut add_machines: Vec<String>,290 mut remove_machines: Vec<String>,291) -> Result<Vec<String>> {292 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {293 bail!("no operation");294 }295296 let initial_machines = initial.clone();297 let mut target_machines = initial;298 info!("Currently encrypted for {initial_machines:?}");299300 301 if let Some(machines) = machines {302 ensure!(303 add_machines.is_empty() && remove_machines.is_empty(),304 "can't combine --machines and --add-machines/--remove-machines"305 );306 let target = initial_machines.iter().collect::<HashSet<_>>();307 let source = machines.iter().collect::<HashSet<_>>();308 for removed in target.difference(&source) {309 remove_machines.push((*removed).clone());310 }311 for added in source.difference(&target) {312 add_machines.push((*added).clone());313 }314 }315316 for machine in &remove_machines {317 let mut removed = false;318 while let Some(pos) = target_machines.iter().position(|m| m == machine) {319 target_machines.swap_remove(pos);320 removed = true;321 }322 if !removed {323 warn!("secret is not enabled for {machine}");324 }325 }326 for machine in &add_machines {327 if target_machines.iter().any(|m| m == machine) {328 warn!("secret is already added to {machine}");329 } else {330 target_machines.push(machine.to_owned());331 }332 }333 if !remove_machines.is_empty() {334 335 336 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");337 }338 Ok(target_machines)339}340impl Secret {341 pub async fn run(self, config: &Config) -> Result<()> {342 match self {343 Secret::ForceKeys => {344 for host in config.list_hosts().await? {345 if config.should_skip(&host.name) {346 continue;347 }348 config.key(&host.name).await?;349 }350 }351 Secret::AddShared {352 mut machines,353 name,354 force,355 public,356 public_file,357 expires_at,358 re_add,359 } => {360 let exists = config.has_shared(&name);361 if exists && !force && !re_add {362 bail!("secret already defined");363 }364 if re_add {365 366 ensure!(!force, "--force and --readd are not compatible");367 ensure!(exists, "secret doesn't exists");368 ensure!(369 machines.is_empty(),370 "you can't use machines argument for --readd"371 );372 let shared = config.shared_secret(&name)?;373 machines = shared.owners;374 }375376 let recipients = config.recipients(machines.clone()).await?;377378 let secret = {379 let mut input = vec![];380 io::stdin().read_to_end(&mut input)?;381382 if input.is_empty() {383 None384 } else {385 Some(386 SecretData::encrypt(recipients, input)387 .ok_or_else(|| anyhow!("no recipients provided"))?,388 )389 }390 };391 let public = parse_public(public, public_file).await?;392 config.replace_shared(393 name,394 FleetSharedSecret {395 owners: machines,396 secret: FleetSecret {397 created_at: Utc::now(),398 expires_at,399 secret,400 public,401 },402 },403 );404 }405 Secret::Add {406 machine,407 name,408 force,409 public,410 public_file,411 } => {412 let recipient = config.recipient(&machine).await?;413414 let secret = {415 let mut input = vec![];416 io::stdin().read_to_end(&mut input)?;417 if input.is_empty() {418 bail!("no data provided")419 }420421 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))422 };423424 if config.has_secret(&machine, &name) && !force {425 bail!("secret already defined");426 }427 let public = parse_public(public, public_file).await?;428429 config.insert_secret(430 &machine,431 name,432 FleetSecret {433 created_at: Utc::now(),434 expires_at: None,435 secret,436 public,437 },438 );439 }440 #[allow(clippy::await_holding_refcell_ref)]441 Secret::Read {442 name,443 machine,444 plaintext,445 } => {446 let secret = config.host_secret(&machine, &name)?;447 let Some(secret) = secret.secret else {448 bail!("no secret {name}");449 };450 let host = config.host(&machine).await?;451 let data = host.decrypt(secret).await?;452 if plaintext {453 let s = String::from_utf8(data).context("output is not utf8")?;454 print!("{s}");455 } else {456 println!("{}", z85::encode(&data));457 }458 }459 Secret::UpdateShared {460 name,461 machines,462 add_machines,463 remove_machines,464 prefer_identities,465 } => {466 let secret = config.shared_secret(&name)?;467 if secret.secret.secret.is_none() {468 bail!("no secret");469 }470471 let initial_machines = secret.owners.clone();472 let target_machines = parse_machines(473 initial_machines.clone(),474 machines,475 add_machines,476 remove_machines,477 )?;478479 if target_machines.is_empty() {480 info!("no machines left for secret, removing it");481 config.remove_shared(&name);482 return Ok(());483 }484485 let config_field = &config.config_unchecked_field;486 let field = nix_go!(config_field.sharedSecrets[{ name }]);487488 let updated = update_owner_set(489 &name,490 config,491 secret,492 field,493 &target_machines,494 &prefer_identities,495 )496 .await?;497 config.replace_shared(name, updated);498 }499 Secret::Regenerate { prefer_identities } => {500 info!("checking for secrets to regenerate");501 {502 let _span = info_span!("shared").entered();503 let expected_shared_set = config504 .list_configured_shared()505 .await?506 .into_iter()507 .collect::<HashSet<_>>();508 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();509 for missing in expected_shared_set.difference(&shared_set) {510 let config_field = &config.config_unchecked_field;511 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);512 let expected_owners: Option<Vec<String>> =513 nix_go_json!(secret.expectedOwners);514 let Some(expected_owners) = expected_owners else {515 516 continue;517 };518 info!("generating secret: {missing}");519 let shared = generate_shared(config, missing, secret, expected_owners)520 .in_current_span()521 .await?;522 config.replace_shared(missing.to_string(), shared)523 }524 }525 for host in config.list_hosts().await? {526 let _span = info_span!("host", host = host.name).entered();527 let expected_set = host528 .list_configured_secrets()529 .in_current_span()530 .await?531 .into_iter()532 .collect::<HashSet<_>>();533 let stored_set = config534 .list_secrets(&host.name)535 .into_iter()536 .collect::<HashSet<_>>();537 for missing in expected_set.difference(&stored_set) {538 info!("generating secret: {missing}");539 let secret = host.secret_field(missing).in_current_span().await?;540 let generated =541 match generate(config, missing, secret, &[host.name.clone()])542 .in_current_span()543 .await544 {545 Ok(v) => v,546 Err(e) => {547 error!("{e}");548 continue;549 }550 };551 config.insert_secret(&host.name, missing.to_string(), generated)552 }553 }554 let mut to_remove = Vec::new();555 for name in &config.list_shared() {556 info!("updating secret: {name}");557 let data = config.shared_secret(name)?;558 let config_field = &config.config_unchecked_field;559 let expected_owners: Vec<String> =560 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);561 if expected_owners.is_empty() {562 warn!("secret was removed from fleet config: {name}, removing from data");563 to_remove.push(name.to_string());564 continue;565 }566567 let secret = nix_go!(config_field.sharedSecrets[{ name }]);568 config.replace_shared(569 name.to_owned(),570 update_owner_set(571 name,572 config,573 data,574 secret,575 &expected_owners,576 &prefer_identities,577 )578 .await?,579 );580 }581 for k in to_remove {582 config.remove_shared(&k);583 }584 }585 Secret::List {} => {586 let _span = info_span!("loading secrets").entered();587 let configured = config.list_configured_shared().await?;588 #[derive(Tabled)]589 struct SecretDisplay {590 #[tabled(rename = "Name")]591 name: String,592 #[tabled(rename = "Owners")]593 owners: String,594 }595 let mut table = vec![];596 for name in configured.iter().cloned() {597 let config = config.clone();598 let expected_owners = config.shared_secret_expected_owners(&name).await?;599 let data = config.shared_secret(&name)?;600 let owners = data601 .owners602 .iter()603 .map(|o| {604 if expected_owners.contains(o) {605 o.green().to_string()606 } else {607 o.red().to_string()608 }609 })610 .collect::<Vec<_>>();611 table.push(SecretDisplay {612 owners: owners.join(", "),613 name,614 })615 }616 info!("loaded\n{}", Table::new(table).to_string())617 }618 }619 Ok(())620 }621}