1use std::{2 collections::{BTreeMap, BTreeSet, HashSet},3 io::{self, stdin, stdout, Read, Write},4 path::PathBuf,5};67use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use fleet_base::{11 fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},12 host::Config,13 opts::FleetOpts,14};15use fleet_shared::SecretData;16use nix_eval::{nix_go, nix_go_json, Value};17use owo_colors::OwoColorize;18use serde::Deserialize;19use tabled::{Table, Tabled};20use tokio::fs::read;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 #[clap(long, short)]33 machines: Vec<String>,34 35 #[clap(long)]36 force: bool,37 38 #[clap(long)]39 public: Option<String>,40 41 #[clap(long)]42 public_file: Option<PathBuf>,4344 45 #[clap(long)]46 expires_at: Option<DateTime<Utc>>,4748 49 #[clap(long)]50 re_add: bool,5152 53 #[clap(long, short = 'p', default_value = "public")]54 public_part: String,55 56 #[clap(short = 's', long, default_value = "secret")]57 part: String,58 },59 60 Add {61 62 name: String,63 64 #[clap(short = 'm', long)]65 machine: String,66 67 #[clap(long)]68 replace: bool,69 70 #[clap(long)]71 merge: bool,72 73 #[clap(long)]74 public: Option<String>,75 76 #[clap(long)]77 public_file: Option<PathBuf>,7879 80 #[clap(short = 'p', long, default_value = "public")]81 public_part: String,82 83 #[clap(short = 's', long, default_value = "secret")]84 part: String,85 },86 87 Read {88 name: String,89 #[clap(short = 'm', long)]90 machine: String,9192 93 #[clap(short = 'p', long, default_value = "secret")]94 part: String,95 },96 UpdateShared {97 name: String,9899 #[clap(short = 'm', long)]100 machine: Option<Vec<String>>,101102 #[clap(long)]103 add_machine: Vec<String>,104 #[clap(long)]105 remove_machine: Vec<String>,106107 108 #[clap(long)]109 prefer_identities: Vec<String>,110 },111 Regenerate {112 113 114 #[clap(long)]115 prefer_identities: Vec<String>,116 },117 List {},118 Edit {119 name: String,120 #[clap(short = 'm', long)]121 machine: String,122123 #[clap(long)]124 add: bool,125126 127 #[clap(short = 'p', long, default_value = "secret")]128 part: String,129 },130}131132#[tracing::instrument(skip(config, secret, field, prefer_identities))]133async fn update_owner_set(134 secret_name: &str,135 config: &Config,136 mut secret: FleetSharedSecret,137 field: Value,138 updated_set: &[String],139 prefer_identities: &[String],140) -> Result<FleetSharedSecret> {141 let original_set = secret.owners.clone();142143 let set = original_set.iter().collect::<BTreeSet<_>>();144 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();145146 if set == expected_set {147 info!("no need to update owner list, it is already correct");148 return Ok(secret);149 }150151 let should_regenerate = if set.difference(&expected_set).next().is_some() {152 153 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");154 nix_go_json!(field.regenerateOnOwnerRemoved)155 } else if expected_set.difference(&set).next().is_some() {156 nix_go_json!(field.regenerateOnOwnerAdded)157 } else {158 false159 };160161 if should_regenerate {162 info!("secret is owner-dependent, will regenerate");163 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;164 Ok(generated)165 } else {166 let identity_holder = if !prefer_identities.is_empty() {167 prefer_identities168 .iter()169 .find(|i| original_set.iter().any(|s| s == *i))170 } else {171 secret.owners.first()172 };173 let Some(identity_holder) = identity_holder else {174 bail!("no available holder found");175 };176177 for (part_name, part) in secret.secret.parts.iter_mut() {178 let _span = info_span!("part reencryption", part_name);179 if !part.raw.encrypted {180 continue;181 }182 let host = config.host(identity_holder).await?;183 let encrypted = host184 .reencrypt(part.raw.clone(), updated_set.to_vec())185 .await?;186 part.raw = encrypted;187 }188189 secret.owners = updated_set.to_vec();190 Ok(secret)191 }192}193194#[derive(Deserialize)]195#[serde(rename_all = "camelCase")]196enum GeneratorKind {197 Impure,198 Pure,199}200201async fn generate_pure(202 _config: &Config,203 _display_name: &str,204 _secret: Value,205 _default_generator: Value,206 _owners: &[String],207) -> Result<FleetSecret> {208 bail!("pure generators are broken for now")209}210async fn generate_impure(211 config: &Config,212 _display_name: &str,213 secret: Value,214 default_generator: Value,215 owners: &[String],216) -> Result<FleetSecret> {217 let generator = nix_go!(secret.generator);218 let on: Option<String> = nix_go_json!(default_generator.impureOn);219220 let host = if let Some(on) = &on {221 config.host(on).await?222 } else {223 config.local_host()224 };225 let on_pkgs = host.pkgs().await?;226 let call_package = nix_go!(on_pkgs.callPackage);227 let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);228229 let mut recipients = Vec::new();230 for owner in owners {231 let key = config.key(owner).await?;232 recipients.push(key);233 }234 let generators = nix_go!(mk_secret_generators(Obj { recipients }));235236 let generator = nix_go!(call_package(generator)(generators));237238 let generator = generator.build().await?;239 let generator = generator240 .get("out")241 .ok_or_else(|| anyhow!("missing generateImpure out"))?;242 let generator = host.remote_derivation(generator).await?;243244 let out_parent = host.mktemp_dir().await?;245 let out = format!("{out_parent}/out");246247 let mut gen = host.cmd(generator).await?;248 gen.env("out", &out);249 if on.is_none() {250 251 let project_path: String = config252 .directory253 .clone()254 .into_os_string()255 .into_string()256 .map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;257 gen.env("FLEET_PROJECT", project_path);258 }259 gen.run().await.context("impure generator")?;260261 {262 let marker = host.read_file_text(format!("{out}/marker")).await?;263 ensure!(marker == "SUCCESS", "generation not succeeded");264 }265266 let mut parts = BTreeMap::new();267 for part in host.read_dir(&out).await? {268 if part == "created_at" || part == "expired_at" || part == "marker" {269 continue;270 }271 let contents: SecretData = host272 .read_file_text(format!("{out}/{part}"))273 .await?274 .parse()275 .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;276 parts.insert(part.to_owned(), FleetSecretPart { raw: contents });277 }278279 let created_at = host.read_file_value(format!("{out}/created_at")).await?;280 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();281282 Ok(FleetSecret {283 created_at,284 expires_at,285 parts,286 })287}288async fn generate(289 config: &Config,290 display_name: &str,291 secret: Value,292 owners: &[String],293) -> Result<FleetSecret> {294 let generator = nix_go!(secret.generator);295 296 {297 let gen_ty = generator.type_of().await?;298 if gen_ty == "null" {299 bail!("secret has no generator defined, can't automatically generate it.");300 }301 if gen_ty != "lambda" {302 bail!("generator should be lambda, got {gen_ty}");303 }304 }305 let default_pkgs = &config.default_pkgs;306 let default_call_package = nix_go!(default_pkgs.callPackage);307 let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);308 309 310 311 312 313 314 315 316 let generators = nix_go!(default_mk_secret_generators(Obj {317 recipients: <Vec<String>>::new(),318 }));319 let default_generator = nix_go!(default_call_package(generator)(generators));320321 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);322323 match kind {324 GeneratorKind::Impure => {325 generate_impure(config, display_name, secret, default_generator, owners).await326 }327 GeneratorKind::Pure => {328 generate_pure(config, display_name, secret, default_generator, owners).await329 }330 }331}332async fn generate_shared(333 config: &Config,334 display_name: &str,335 secret: Value,336 expected_owners: Vec<String>,337) -> Result<FleetSharedSecret> {338 339 Ok(FleetSharedSecret {340 secret: generate(config, display_name, secret, &expected_owners).await?,341 owners: expected_owners,342 })343}344345async fn parse_public(346 public: Option<String>,347 public_file: Option<PathBuf>,348) -> Result<Option<SecretData>> {349 Ok(match (public, public_file) {350 (Some(v), None) => Some(SecretData {351 data: v.into(),352 encrypted: false,353 }),354 (None, Some(v)) => Some(SecretData {355 data: read(v).await?,356 encrypted: false,357 }),358 (Some(_), Some(_)) => {359 bail!("only public or public_file should be set")360 }361 (None, None) => None,362 })363}364365async fn parse_secret() -> Result<Option<Vec<u8>>> {366 let mut input = vec![];367 stdin().read_to_end(&mut input)?;368 if input.is_empty() {369 Ok(None)370 } else {371 Ok(Some(input))372 }373}374375fn parse_machines(376 initial: Vec<String>,377 machines: Option<Vec<String>>,378 mut add_machines: Vec<String>,379 mut remove_machines: Vec<String>,380) -> Result<Vec<String>> {381 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {382 bail!("no operation");383 }384385 let initial_machines = initial.clone();386 let mut target_machines = initial;387 info!("Currently encrypted for {initial_machines:?}");388389 390 if let Some(machines) = machines {391 ensure!(392 add_machines.is_empty() && remove_machines.is_empty(),393 "can't combine --machines and --add-machines/--remove-machines"394 );395 let target = initial_machines.iter().collect::<HashSet<_>>();396 let source = machines.iter().collect::<HashSet<_>>();397 for removed in target.difference(&source) {398 remove_machines.push((*removed).clone());399 }400 for added in source.difference(&target) {401 add_machines.push((*added).clone());402 }403 }404405 for machine in &remove_machines {406 let mut removed = false;407 while let Some(pos) = target_machines.iter().position(|m| m == machine) {408 target_machines.swap_remove(pos);409 removed = true;410 }411 if !removed {412 warn!("secret is not enabled for {machine}");413 }414 }415 for machine in &add_machines {416 if target_machines.iter().any(|m| m == machine) {417 warn!("secret is already added to {machine}");418 } else {419 target_machines.push(machine.to_owned());420 }421 }422 if !remove_machines.is_empty() {423 424 425 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");426 }427 Ok(target_machines)428}429impl Secret {430 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {431 match self {432 Secret::ForceKeys => {433 for host in config.list_hosts().await? {434 if opts.should_skip(&host).await? {435 continue;436 }437 config.key(&host.name).await?;438 }439 }440 Secret::AddShared {441 mut machines,442 name,443 force,444 public,445 public_part: public_name,446 public_file,447 expires_at,448 re_add,449 part: part_name,450 } => {451 452453 let exists = config.has_shared(&name);454 if exists && !force && !re_add {455 bail!("secret already defined");456 }457 if re_add {458 459 ensure!(!force, "--force and --readd are not compatible");460 ensure!(exists, "secret doesn't exists");461 ensure!(462 machines.is_empty(),463 "you can't use machines argument for --readd"464 );465 let shared = config.shared_secret(&name)?;466 machines = shared.owners;467 }468469 let recipients = config.recipients(machines.clone()).await?;470471 let mut parts = BTreeMap::new();472473 let mut input = vec![];474 io::stdin().read_to_end(&mut input)?;475476 if !input.is_empty() {477 let encrypted = encrypt_secret_data(recipients, input)478 .ok_or_else(|| anyhow!("no recipients provided"))?;479 parts.insert(part_name, FleetSecretPart { raw: encrypted });480 }481482 if let Some(public) = parse_public(public, public_file).await? {483 parts.insert(public_name, FleetSecretPart { raw: public });484 }485486 config.replace_shared(487 name,488 FleetSharedSecret {489 owners: machines,490 secret: FleetSecret {491 created_at: Utc::now(),492 expires_at,493 parts,494 },495 },496 );497 }498 Secret::Add {499 machine,500 name,501 replace,502 merge,503 public,504 public_part: public_name,505 public_file,506 part: part_name,507 } => {508 if config.has_secret(&machine, &name) && !replace && !merge {509 bail!("secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret");510 }511512 let mut out = if merge && !replace {513 config514 .host_secret(&machine, &name)515 .context("failed to read existing secret for --merge")?516 } else {517 FleetSecret {518 created_at: Utc::now(),519 expires_at: None,520 parts: BTreeMap::new(),521 }522 };523524 if let Some(secret) = parse_secret().await? {525 let recipient = config.recipient(&machine).await?;526 let encrypted =527 encrypt_secret_data(vec![recipient], secret).expect("recipient provided");528 if out529 .parts530 .insert(part_name.clone(), FleetSecretPart { raw: encrypted })531 .is_some() && !replace532 {533 bail!("part {part_name:?} is already defined");534 }535 }536537 if let Some(public) = parse_public(public, public_file).await? {538 if out539 .parts540 .insert(public_name.clone(), FleetSecretPart { raw: public })541 .is_some() && !replace542 {543 bail!("part {public_name:?} is already defined");544 }545 };546547 config.insert_secret(&machine, name, out);548 }549 #[allow(clippy::await_holding_refcell_ref)]550 Secret::Read {551 name,552 machine,553 part: part_name,554 } => {555 let secret = config.host_secret(&machine, &name)?;556 let Some(secret) = secret.parts.get(&part_name) else {557 bail!("no part {part_name} in secret {name}");558 };559 let data = if secret.raw.encrypted {560 let host = config.host(&machine).await?;561 host.decrypt(secret.raw.clone()).await?562 } else {563 secret.raw.data.clone()564 };565566 stdout().write_all(&data)?;567 }568 Secret::UpdateShared {569 name,570 machine,571 add_machine,572 remove_machine,573 prefer_identities,574 } => {575 576577 let secret = config.shared_secret(&name)?;578 if secret.secret.parts.values().all(|v| !v.raw.encrypted) {579 bail!("no secret");580 }581582 let initial_machines = secret.owners.clone();583 let target_machines = parse_machines(584 initial_machines.clone(),585 machine,586 add_machine,587 remove_machine,588 )?;589590 if target_machines.is_empty() {591 info!("no machines left for secret, removing it");592 config.remove_shared(&name);593 return Ok(());594 }595596 let config_field = &config.config_field;597 let field = nix_go!(config_field.sharedSecrets[{ name }]);598599 let updated = update_owner_set(600 &name,601 config,602 secret,603 field,604 &target_machines,605 &prefer_identities,606 )607 .await?;608 config.replace_shared(name, updated);609 }610 Secret::Regenerate { prefer_identities } => {611 info!("checking for secrets to regenerate");612 {613 let _span = info_span!("shared").entered();614 let expected_shared_set = config615 .list_configured_shared()616 .await?617 .into_iter()618 .collect::<HashSet<_>>();619 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();620 for missing in expected_shared_set.difference(&shared_set) {621 let config_field = &config.config_field;622 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);623 let expected_owners: Option<Vec<String>> =624 nix_go_json!(secret.expectedOwners);625 let Some(expected_owners) = expected_owners else {626 627 continue;628 };629 info!("generating secret: {missing}");630 let shared = generate_shared(config, missing, secret, expected_owners)631 .in_current_span()632 .await?;633 config.replace_shared(missing.to_string(), shared)634 }635 }636 for host in config.list_hosts().await? {637 if opts.should_skip(&host).await? {638 continue;639 }640641 let _span = info_span!("host", host = host.name).entered();642 let expected_set = host643 .list_configured_secrets()644 .in_current_span()645 .await?646 .into_iter()647 .collect::<HashSet<_>>();648 let stored_set = config649 .list_secrets(&host.name)650 .into_iter()651 .collect::<HashSet<_>>();652 for missing in expected_set.difference(&stored_set) {653 info!("generating secret: {missing}");654 let secret = host.secret_field(missing).in_current_span().await?;655 let generated =656 match generate(config, missing, secret, &[host.name.clone()])657 .in_current_span()658 .await659 {660 Ok(v) => v,661 Err(e) => {662 error!("{e:?}");663 continue;664 }665 };666 config.insert_secret(&host.name, missing.to_string(), generated)667 }668 }669 let mut to_remove = Vec::new();670 for name in &config.list_shared() {671 info!("updating secret: {name}");672 let data = config.shared_secret(name)?;673 let config_field = &config.config_field;674 let expected_owners: Vec<String> =675 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);676 if expected_owners.is_empty() {677 warn!("secret was removed from fleet config: {name}, removing from data");678 to_remove.push(name.to_string());679 continue;680 }681682 let secret = nix_go!(config_field.sharedSecrets[{ name }]);683 config.replace_shared(684 name.to_owned(),685 update_owner_set(686 name,687 config,688 data,689 secret,690 &expected_owners,691 &prefer_identities,692 )693 .await?,694 );695 }696 for k in to_remove {697 config.remove_shared(&k);698 }699 }700 Secret::List {} => {701 let _span = info_span!("loading secrets").entered();702 let configured = config.list_configured_shared().await?;703 #[derive(Tabled)]704 struct SecretDisplay {705 #[tabled(rename = "Name")]706 name: String,707 #[tabled(rename = "Owners")]708 owners: String,709 }710 let mut table = vec![];711 for name in configured.iter().cloned() {712 let config = config.clone();713 let expected_owners = config.shared_secret_expected_owners(&name).await?;714 let data = config.shared_secret(&name)?;715 let owners = data716 .owners717 .iter()718 .map(|o| {719 if expected_owners.contains(o) {720 o.green().to_string()721 } else {722 o.red().to_string()723 }724 })725 .collect::<Vec<_>>();726 table.push(SecretDisplay {727 owners: owners.join(", "),728 name,729 })730 }731 info!("loaded\n{}", Table::new(table).to_string())732 }733 Secret::Edit {734 name,735 machine,736 part,737 add,738 } => {739 let secret = config.host_secret(&machine, &name)?;740 if let Some(data) = secret.parts.get(&part) {741 let host = config.host(&machine).await?;742 let secret = host.decrypt(data.raw.clone()).await?;743 String::from_utf8(secret).context("secret is not utf8")?744 } else if add {745 String::new()746 } else {747 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");748 };749 }750 }751 Ok(())752 }753}754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834
1use std::{2 collections::{BTreeMap, BTreeSet, HashSet},3 io::{self, stdin, stdout, Read, Write},4 path::PathBuf,5};67use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use fleet_base::{11 fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},12 host::Config,13 opts::FleetOpts,14};15use fleet_shared::SecretData;16use nix_eval::{nix_go, nix_go_json, NixBuildBatch, Value};17use owo_colors::OwoColorize;18use serde::Deserialize;19use tabled::{Table, Tabled};20use tokio::fs::read;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 #[clap(long, short)]33 machines: Vec<String>,34 35 #[clap(long)]36 force: bool,37 38 #[clap(long)]39 public: Option<String>,40 41 #[clap(long)]42 public_file: Option<PathBuf>,4344 45 #[clap(long)]46 expires_at: Option<DateTime<Utc>>,4748 49 #[clap(long)]50 re_add: bool,5152 53 #[clap(long, short = 'p', default_value = "public")]54 public_part: String,55 56 #[clap(short = 's', long, default_value = "secret")]57 part: String,58 },59 60 Add {61 62 name: String,63 64 #[clap(short = 'm', long)]65 machine: String,66 67 #[clap(long)]68 replace: bool,69 70 #[clap(long)]71 merge: bool,72 73 #[clap(long)]74 public: Option<String>,75 76 #[clap(long)]77 public_file: Option<PathBuf>,7879 80 #[clap(short = 'p', long, default_value = "public")]81 public_part: String,82 83 #[clap(short = 's', long, default_value = "secret")]84 part: String,85 },86 87 Read {88 name: String,89 #[clap(short = 'm', long)]90 machine: String,9192 93 #[clap(short = 'p', long, default_value = "secret")]94 part: String,95 },96 UpdateShared {97 name: String,9899 #[clap(short = 'm', long)]100 machine: Option<Vec<String>>,101102 #[clap(long)]103 add_machine: Vec<String>,104 #[clap(long)]105 remove_machine: Vec<String>,106107 108 #[clap(long)]109 prefer_identities: Vec<String>,110 },111 Regenerate {112 113 114 #[clap(long)]115 prefer_identities: Vec<String>,116 },117 List {},118 Edit {119 name: String,120 #[clap(short = 'm', long)]121 machine: String,122123 #[clap(long)]124 add: bool,125126 127 #[clap(short = 'p', long, default_value = "secret")]128 part: String,129 },130}131132#[tracing::instrument(skip(config, secret, field, prefer_identities, batch))]133async fn update_owner_set(134 secret_name: &str,135 config: &Config,136 mut secret: FleetSharedSecret,137 field: Value,138 updated_set: &[String],139 prefer_identities: &[String],140 batch: Option<NixBuildBatch>,141) -> Result<FleetSharedSecret> {142 let original_set = secret.owners.clone();143144 let set = original_set.iter().collect::<BTreeSet<_>>();145 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();146147 if set == expected_set {148 info!("no need to update owner list, it is already correct");149 return Ok(secret);150 }151152 let should_regenerate = if set.difference(&expected_set).next().is_some() {153 154 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");155 nix_go_json!(field.regenerateOnOwnerRemoved)156 } else if expected_set.difference(&set).next().is_some() {157 nix_go_json!(field.regenerateOnOwnerAdded)158 } else {159 false160 };161162 if should_regenerate {163 info!("secret is owner-dependent, will regenerate");164 let generated = generate_shared(config, secret_name, field, updated_set.to_vec(), batch).await?;165 Ok(generated)166 } else {167 drop(batch);168 let identity_holder = if !prefer_identities.is_empty() {169 prefer_identities170 .iter()171 .find(|i| original_set.iter().any(|s| s == *i))172 } else {173 secret.owners.first()174 };175 let Some(identity_holder) = identity_holder else {176 bail!("no available holder found");177 };178179 for (part_name, part) in secret.secret.parts.iter_mut() {180 let _span = info_span!("part reencryption", part_name);181 if !part.raw.encrypted {182 continue;183 }184 let host = config.host(identity_holder).await?;185 let encrypted = host186 .reencrypt(part.raw.clone(), updated_set.to_vec())187 .await?;188 part.raw = encrypted;189 }190191 secret.owners = updated_set.to_vec();192 Ok(secret)193 }194}195196#[derive(Deserialize)]197#[serde(rename_all = "camelCase")]198enum GeneratorKind {199 Impure,200 Pure,201}202203async fn generate_pure(204 _config: &Config,205 _display_name: &str,206 _secret: Value,207 _default_generator: Value,208 _owners: &[String],209) -> Result<FleetSecret> {210 bail!("pure generators are broken for now")211}212async fn generate_impure(213 config: &Config,214 _display_name: &str,215 secret: Value,216 default_generator: Value,217 owners: &[String],218 batch: Option<NixBuildBatch>,219) -> Result<FleetSecret> {220 let generator = nix_go!(secret.generator);221 let on: Option<String> = nix_go_json!(default_generator.impureOn);222223 let host = if let Some(on) = &on {224 config.host(on).await?225 } else {226 config.local_host()227 };228 let on_pkgs = host.pkgs().await?;229 let call_package = nix_go!(on_pkgs.callPackage);230 let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);231232 let mut recipients = Vec::new();233 for owner in owners {234 let key = config.key(owner).await?;235 recipients.push(key);236 }237 let generators = nix_go!(mk_secret_generators(Obj { recipients }));238239 let generator = nix_go!(call_package(generator)(generators));240241 let generator = generator.build_maybe_batch(batch).await?;242 let generator = generator243 .get("out")244 .ok_or_else(|| anyhow!("missing generateImpure out"))?;245 let generator = host.remote_derivation(generator).await?;246247 let out_parent = host.mktemp_dir().await?;248 let out = format!("{out_parent}/out");249250 let mut gen = host.cmd(generator).await?;251 gen.env("out", &out);252 if on.is_none() {253 254 let project_path: String = config255 .directory256 .clone()257 .into_os_string()258 .into_string()259 .map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;260 gen.env("FLEET_PROJECT", project_path);261 }262 gen.run().await.context("impure generator")?;263264 {265 let marker = host.read_file_text(format!("{out}/marker")).await?;266 ensure!(marker == "SUCCESS", "generation not succeeded");267 }268269 let mut parts = BTreeMap::new();270 for part in host.read_dir(&out).await? {271 if part == "created_at" || part == "expired_at" || part == "marker" {272 continue;273 }274 let contents: SecretData = host275 .read_file_text(format!("{out}/{part}"))276 .await?277 .parse()278 .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;279 parts.insert(part.to_owned(), FleetSecretPart { raw: contents });280 }281282 let created_at = host.read_file_value(format!("{out}/created_at")).await?;283 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();284285 Ok(FleetSecret {286 created_at,287 expires_at,288 parts,289 })290}291async fn generate(292 config: &Config,293 display_name: &str,294 secret: Value,295 owners: &[String],296 batch: Option<NixBuildBatch>,297) -> Result<FleetSecret> {298 let generator = nix_go!(secret.generator);299 300 {301 let gen_ty = generator.type_of().await?;302 if gen_ty == "null" {303 bail!("secret has no generator defined, can't automatically generate it.");304 }305 if gen_ty != "lambda" {306 bail!("generator should be lambda, got {gen_ty}");307 }308 }309 let default_pkgs = &config.default_pkgs;310 let default_call_package = nix_go!(default_pkgs.callPackage);311 let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);312 313 314 315 316 317 318 319 320 let generators = nix_go!(default_mk_secret_generators(Obj {321 recipients: <Vec<String>>::new(),322 }));323 let default_generator = nix_go!(default_call_package(generator)(generators));324325 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);326327 match kind {328 GeneratorKind::Impure => {329 generate_impure(330 config,331 display_name,332 secret,333 default_generator,334 owners,335 batch,336 )337 .await338 }339 GeneratorKind::Pure => {340 generate_pure(config, display_name, secret, default_generator, owners).await341 }342 }343}344async fn generate_shared(345 config: &Config,346 display_name: &str,347 secret: Value,348 expected_owners: Vec<String>,349 batch: Option<NixBuildBatch>,350) -> Result<FleetSharedSecret> {351 352 Ok(FleetSharedSecret {353 secret: generate(config, display_name, secret, &expected_owners, batch).await?,354 owners: expected_owners,355 })356}357358async fn parse_public(359 public: Option<String>,360 public_file: Option<PathBuf>,361) -> Result<Option<SecretData>> {362 Ok(match (public, public_file) {363 (Some(v), None) => Some(SecretData {364 data: v.into(),365 encrypted: false,366 }),367 (None, Some(v)) => Some(SecretData {368 data: read(v).await?,369 encrypted: false,370 }),371 (Some(_), Some(_)) => {372 bail!("only public or public_file should be set")373 }374 (None, None) => None,375 })376}377378async fn parse_secret() -> Result<Option<Vec<u8>>> {379 let mut input = vec![];380 stdin().read_to_end(&mut input)?;381 if input.is_empty() {382 Ok(None)383 } else {384 Ok(Some(input))385 }386}387388fn parse_machines(389 initial: Vec<String>,390 machines: Option<Vec<String>>,391 mut add_machines: Vec<String>,392 mut remove_machines: Vec<String>,393) -> Result<Vec<String>> {394 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {395 bail!("no operation");396 }397398 let initial_machines = initial.clone();399 let mut target_machines = initial;400 info!("Currently encrypted for {initial_machines:?}");401402 403 if let Some(machines) = machines {404 ensure!(405 add_machines.is_empty() && remove_machines.is_empty(),406 "can't combine --machines and --add-machines/--remove-machines"407 );408 let target = initial_machines.iter().collect::<HashSet<_>>();409 let source = machines.iter().collect::<HashSet<_>>();410 for removed in target.difference(&source) {411 remove_machines.push((*removed).clone());412 }413 for added in source.difference(&target) {414 add_machines.push((*added).clone());415 }416 }417418 for machine in &remove_machines {419 let mut removed = false;420 while let Some(pos) = target_machines.iter().position(|m| m == machine) {421 target_machines.swap_remove(pos);422 removed = true;423 }424 if !removed {425 warn!("secret is not enabled for {machine}");426 }427 }428 for machine in &add_machines {429 if target_machines.iter().any(|m| m == machine) {430 warn!("secret is already added to {machine}");431 } else {432 target_machines.push(machine.to_owned());433 }434 }435 if !remove_machines.is_empty() {436 437 438 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");439 }440 Ok(target_machines)441}442impl Secret {443 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {444 match self {445 Secret::ForceKeys => {446 for host in config.list_hosts().await? {447 if opts.should_skip(&host).await? {448 continue;449 }450 config.key(&host.name).await?;451 }452 }453 Secret::AddShared {454 mut machines,455 name,456 force,457 public,458 public_part: public_name,459 public_file,460 expires_at,461 re_add,462 part: part_name,463 } => {464 465466 let exists = config.has_shared(&name);467 if exists && !force && !re_add {468 bail!("secret already defined");469 }470 if re_add {471 472 ensure!(!force, "--force and --readd are not compatible");473 ensure!(exists, "secret doesn't exists");474 ensure!(475 machines.is_empty(),476 "you can't use machines argument for --readd"477 );478 let shared = config.shared_secret(&name)?;479 machines = shared.owners;480 }481482 let recipients = config.recipients(machines.clone()).await?;483484 let mut parts = BTreeMap::new();485486 let mut input = vec![];487 io::stdin().read_to_end(&mut input)?;488489 if !input.is_empty() {490 let encrypted = encrypt_secret_data(recipients, input)491 .ok_or_else(|| anyhow!("no recipients provided"))?;492 parts.insert(part_name, FleetSecretPart { raw: encrypted });493 }494495 if let Some(public) = parse_public(public, public_file).await? {496 parts.insert(public_name, FleetSecretPart { raw: public });497 }498499 config.replace_shared(500 name,501 FleetSharedSecret {502 owners: machines,503 secret: FleetSecret {504 created_at: Utc::now(),505 expires_at,506 parts,507 },508 },509 );510 }511 Secret::Add {512 machine,513 name,514 replace,515 merge,516 public,517 public_part: public_name,518 public_file,519 part: part_name,520 } => {521 if config.has_secret(&machine, &name) && !replace && !merge {522 bail!("secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret");523 }524525 let mut out = if merge && !replace {526 config527 .host_secret(&machine, &name)528 .context("failed to read existing secret for --merge")?529 } else {530 FleetSecret {531 created_at: Utc::now(),532 expires_at: None,533 parts: BTreeMap::new(),534 }535 };536537 if let Some(secret) = parse_secret().await? {538 let recipient = config.recipient(&machine).await?;539 let encrypted =540 encrypt_secret_data(vec![recipient], secret).expect("recipient provided");541 if out542 .parts543 .insert(part_name.clone(), FleetSecretPart { raw: encrypted })544 .is_some() && !replace545 {546 bail!("part {part_name:?} is already defined");547 }548 }549550 if let Some(public) = parse_public(public, public_file).await? {551 if out552 .parts553 .insert(public_name.clone(), FleetSecretPart { raw: public })554 .is_some() && !replace555 {556 bail!("part {public_name:?} is already defined");557 }558 };559560 config.insert_secret(&machine, name, out);561 }562 #[allow(clippy::await_holding_refcell_ref)]563 Secret::Read {564 name,565 machine,566 part: part_name,567 } => {568 let secret = config.host_secret(&machine, &name)?;569 let Some(secret) = secret.parts.get(&part_name) else {570 bail!("no part {part_name} in secret {name}");571 };572 let data = if secret.raw.encrypted {573 let host = config.host(&machine).await?;574 host.decrypt(secret.raw.clone()).await?575 } else {576 secret.raw.data.clone()577 };578579 stdout().write_all(&data)?;580 }581 Secret::UpdateShared {582 name,583 machine,584 add_machine,585 remove_machine,586 prefer_identities,587 } => {588 589590 let secret = config.shared_secret(&name)?;591 if secret.secret.parts.values().all(|v| !v.raw.encrypted) {592 bail!("no secret");593 }594595 let initial_machines = secret.owners.clone();596 let target_machines = parse_machines(597 initial_machines.clone(),598 machine,599 add_machine,600 remove_machine,601 )?;602603 if target_machines.is_empty() {604 info!("no machines left for secret, removing it");605 config.remove_shared(&name);606 return Ok(());607 }608609 let config_field = &config.config_field;610 let field = nix_go!(config_field.sharedSecrets[{ name }]);611612 let updated = update_owner_set(613 &name,614 config,615 secret,616 field,617 &target_machines,618 &prefer_identities,619 None,620 )621 .await?;622 config.replace_shared(name, updated);623 }624 Secret::Regenerate { prefer_identities } => {625 info!("checking for secrets to regenerate");626 {627 let shared_batch = None;628 let _span = info_span!("shared").entered();629 let expected_shared_set = config630 .list_configured_shared()631 .await?632 .into_iter()633 .collect::<HashSet<_>>();634 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();635 for missing in expected_shared_set.difference(&shared_set) {636 let config_field = &config.config_field;637 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);638 let expected_owners: Option<Vec<String>> =639 nix_go_json!(secret.expectedOwners);640 let Some(expected_owners) = expected_owners else {641 642 continue;643 };644 info!("generating secret: {missing}");645 let shared = generate_shared(646 config,647 missing,648 secret,649 expected_owners,650 shared_batch.clone(),651 )652 .in_current_span()653 .await?;654 config.replace_shared(missing.to_string(), shared)655 }656 }657 let hosts_batch = None;658 for host in config.list_hosts().await? {659 if opts.should_skip(&host).await? {660 continue;661 }662663 let _span = info_span!("host", host = host.name).entered();664 let expected_set = host665 .list_configured_secrets()666 .in_current_span()667 .await?668 .into_iter()669 .collect::<HashSet<_>>();670 let stored_set = config671 .list_secrets(&host.name)672 .into_iter()673 .collect::<HashSet<_>>();674 for missing in expected_set.difference(&stored_set) {675 info!("generating secret: {missing}");676 let secret = host.secret_field(missing).in_current_span().await?;677 let generated = match generate(678 config,679 missing,680 secret,681 &[host.name.clone()],682 hosts_batch.clone(),683 )684 .in_current_span()685 .await686 {687 Ok(v) => v,688 Err(e) => {689 error!("{e:?}");690 continue;691 }692 };693 config.insert_secret(&host.name, missing.to_string(), generated)694 }695 }696 let mut to_remove = Vec::new();697 for name in &config.list_shared() {698 info!("updating secret: {name}");699 let data = config.shared_secret(name)?;700 let config_field = &config.config_field;701 let expected_owners: Vec<String> =702 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);703 if expected_owners.is_empty() {704 warn!("secret was removed from fleet config: {name}, removing from data");705 to_remove.push(name.to_string());706 continue;707 }708709 let secret = nix_go!(config_field.sharedSecrets[{ name }]);710 config.replace_shared(711 name.to_owned(),712 update_owner_set(713 name,714 config,715 data,716 secret,717 &expected_owners,718 &prefer_identities,719 None,720 )721 .await?,722 );723 }724 for k in to_remove {725 config.remove_shared(&k);726 }727 }728 Secret::List {} => {729 let _span = info_span!("loading secrets").entered();730 let configured = config.list_configured_shared().await?;731 #[derive(Tabled)]732 struct SecretDisplay {733 #[tabled(rename = "Name")]734 name: String,735 #[tabled(rename = "Owners")]736 owners: String,737 }738 let mut table = vec![];739 for name in configured.iter().cloned() {740 let config = config.clone();741 let expected_owners = config.shared_secret_expected_owners(&name).await?;742 let data = config.shared_secret(&name)?;743 let owners = data744 .owners745 .iter()746 .map(|o| {747 if expected_owners.contains(o) {748 o.green().to_string()749 } else {750 o.red().to_string()751 }752 })753 .collect::<Vec<_>>();754 table.push(SecretDisplay {755 owners: owners.join(", "),756 name,757 })758 }759 info!("loaded\n{}", Table::new(table).to_string())760 }761 Secret::Edit {762 name,763 machine,764 part,765 add,766 } => {767 let secret = config.host_secret(&machine, &name)?;768 if let Some(data) = secret.parts.get(&part) {769 let host = config.host(&machine).await?;770 let secret = host.decrypt(data.raw.clone()).await?;771 String::from_utf8(secret).context("secret is not utf8")?772 } else if add {773 String::new()774 } else {775 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");776 };777 }778 }779 Ok(())780 }781}782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862