difftreelog
fix post-refactor
in: trunk
3 files changed
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth1use 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 /// Force load host keys for all defined hosts26 ForceKeys,27 /// Add secret, data should be provided in stdin28 AddShared {29 /// Secret name30 name: String,31 /// Secret owners32 machines: Vec<String>,33 /// Override secret if already present34 #[clap(long)]35 force: bool,36 /// Secret public part37 #[clap(long)]38 public: Option<String>,39 /// Load public part from specified file40 #[clap(long)]41 public_file: Option<PathBuf>,4243 /// Create a notification on secret expiration44 #[clap(long)]45 expires_at: Option<DateTime<Utc>>,4647 /// Secret with this name already exists, override its value while keeping the same owners.48 #[clap(long)]49 re_add: bool,50 },51 /// Add secret, data should be provided in stdin52 Add {53 /// Secret name54 name: String,55 /// Secret owners56 machine: String,57 /// Override secret if already present58 #[clap(long)]59 force: bool,60 #[clap(long)]61 public: Option<String>,62 #[clap(long)]63 public_file: Option<PathBuf>,64 },65 /// Read secret from remote host, requires sudo on said host66 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 /// Which host should we use to decrypt84 #[clap(long)]85 prefer_identities: Vec<String>,86 },87 Regenerate {88 /// Which host should we use to decrypt, in case if reencryption is required, without89 /// regeneration90 #[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 // TODO: Remove this warning for revokable secrets.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.buildableSystems(Obj {171 localSystem: { config.local_system.clone() },172 })[{ on }]173 .config174 .nixpkgs175 .resolvedPkgs176 .callPackage177 );178179 let host = config.host(&on).await?;180181 let generator = nix_go!(call_package(generator)(Obj {}));182 let generator = generator.build().await?;183 let generator = generator184 .get("out")185 .ok_or_else(|| anyhow!("missing generateImpure out"))?;186 let generator = host.remote_derivation(generator).await?;187188 let mut recipients = String::new();189 for owner in owners {190 let key = config.key(owner).await?;191 recipients.push_str(&format!("-r \"{key}\" "));192 }193 recipients.push_str("-e");194195 let out = host.mktemp_dir().await?;196197 let mut gen = host.cmd(generator).await?;198 gen.env("rageArgs", recipients).env("out", &out);199 gen.run().await.context("impure generator")?;200201 {202 let marker = host.read_file_text(format!("{out}/marker")).await?;203 ensure!(marker == "SUCCESS", "generation not succeeded");204 }205206 let public = host.read_file_text(format!("{out}/public")).await.ok();207 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();208 if let Some(secret) = &secret {209 ensure!(210 age::Decryptor::new(Cursor::new(&secret)).is_ok(),211 "builder produced non-encrypted value as secret, this is highly insecure, and not allowed."212 );213 }214215 let created_at = host.read_file_value(format!("{out}/created_at")).await?;216 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();217218 Ok(FleetSecret {219 created_at,220 expires_at,221 public,222 secret: secret.map(SecretData),223 })224}225async fn generate(226 config: &Config,227 display_name: &str,228 secret: Field,229 owners: &[String],230) -> Result<FleetSecret> {231 let generator = nix_go!(secret.generator);232 // Can't properly check on nix module system level233 {234 let gen_ty = generator.type_of().await?;235 if gen_ty == "null" {236 bail!("secret has no generator defined, can't automatically generate it.");237 }238 if gen_ty != "lambda" {239 bail!("generator should be lambda, got {gen_ty}");240 }241 }242 let default_pkgs = &config.default_pkgs;243 let default_call_package = nix_go!(default_pkgs.callPackage);244 // Generators provide additional information in passthru, to access245 // passthru we should call generator, but information about where this generator is supposed to build246 // is located in passthru... Thus evaluating generator on host.247 //248 // Maybe it is also possible to do some magic with __functor?249 //250 // I don't want to make modules always responsible for additional secret data anyway,251 // so it should be in derivation, and not in the secret data itself.252 let default_generator = nix_go!(default_call_package(generator)(Obj {}));253254 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);255256 match kind {257 GeneratorKind::Impure => {258 generate_impure(config, display_name, secret, default_generator, owners).await259 }260 }261}262async fn generate_shared(263 config: &Config,264 display_name: &str,265 secret: Field,266 expected_owners: Vec<String>,267) -> Result<FleetSharedSecret> {268 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);269 Ok(FleetSharedSecret {270 secret: generate(config, display_name, secret, &expected_owners).await?,271 owners: expected_owners,272 })273}274275async fn parse_public(276 public: Option<String>,277 public_file: Option<PathBuf>,278) -> Result<Option<String>> {279 Ok(match (public, public_file) {280 (Some(v), None) => Some(v),281 (None, Some(v)) => Some(read_to_string(v).await?),282 (Some(_), Some(_)) => {283 bail!("only public or public_file should be set")284 }285 (None, None) => None,286 })287}288289fn parse_machines(290 initial: Vec<String>,291 machines: Option<Vec<String>>,292 mut add_machines: Vec<String>,293 mut remove_machines: Vec<String>,294) -> Result<Vec<String>> {295 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {296 bail!("no operation");297 }298299 let initial_machines = initial.clone();300 let mut target_machines = initial;301 info!("Currently encrypted for {initial_machines:?}");302303 // ensure!(machines.is_some() || !add_machines.is_empty() || )304 if let Some(machines) = machines {305 ensure!(306 add_machines.is_empty() && remove_machines.is_empty(),307 "can't combine --machines and --add-machines/--remove-machines"308 );309 let target = initial_machines.iter().collect::<HashSet<_>>();310 let source = machines.iter().collect::<HashSet<_>>();311 for removed in target.difference(&source) {312 remove_machines.push((*removed).clone());313 }314 for added in source.difference(&target) {315 add_machines.push((*added).clone());316 }317 }318319 for machine in &remove_machines {320 let mut removed = false;321 while let Some(pos) = target_machines.iter().position(|m| m == machine) {322 target_machines.swap_remove(pos);323 removed = true;324 }325 if !removed {326 warn!("secret is not enabled for {machine}");327 }328 }329 for machine in &add_machines {330 if target_machines.iter().any(|m| m == machine) {331 warn!("secret is already added to {machine}");332 } else {333 target_machines.push(machine.to_owned());334 }335 }336 if !remove_machines.is_empty() {337 // TODO: maybe force secret regeneration?338 // Not that useful without revokation.339 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");340 }341 Ok(target_machines)342}343impl Secret {344 pub async fn run(self, config: &Config) -> Result<()> {345 match self {346 Secret::ForceKeys => {347 for host in config.list_hosts().await? {348 if config.should_skip(&host.name) {349 continue;350 }351 config.key(&host.name).await?;352 }353 }354 Secret::AddShared {355 mut machines,356 name,357 force,358 public,359 public_file,360 expires_at,361 re_add,362 } => {363 let exists = config.has_shared(&name);364 if exists && !force && !re_add {365 bail!("secret already defined");366 }367 if re_add {368 // Fixme: use clap to limit this usage369 ensure!(!force, "--force and --readd are not compatible");370 ensure!(exists, "secret doesn't exists");371 ensure!(372 machines.is_empty(),373 "you can't use machines argument for --readd"374 );375 let shared = config.shared_secret(&name)?;376 machines = shared.owners;377 }378379 let recipients = config.recipients(machines.clone()).await?;380381 let secret = {382 let mut input = vec![];383 io::stdin().read_to_end(&mut input)?;384385 if input.is_empty() {386 None387 } else {388 Some(389 SecretData::encrypt(recipients, input)390 .ok_or_else(|| anyhow!("no recipients provided"))?,391 )392 }393 };394 let public = parse_public(public, public_file).await?;395 config.replace_shared(396 name,397 FleetSharedSecret {398 owners: machines,399 secret: FleetSecret {400 created_at: Utc::now(),401 expires_at,402 secret,403 public,404 },405 },406 );407 }408 Secret::Add {409 machine,410 name,411 force,412 public,413 public_file,414 } => {415 let recipient = config.recipient(&machine).await?;416417 let secret = {418 let mut input = vec![];419 io::stdin().read_to_end(&mut input)?;420 if input.is_empty() {421 bail!("no data provided")422 }423424 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))425 };426427 if config.has_secret(&machine, &name) && !force {428 bail!("secret already defined");429 }430 let public = parse_public(public, public_file).await?;431432 config.insert_secret(433 &machine,434 name,435 FleetSecret {436 created_at: Utc::now(),437 expires_at: None,438 secret,439 public,440 },441 );442 }443 #[allow(clippy::await_holding_refcell_ref)]444 Secret::Read {445 name,446 machine,447 plaintext,448 } => {449 let secret = config.host_secret(&machine, &name)?;450 let Some(secret) = secret.secret else {451 bail!("no secret {name}");452 };453 let host = config.host(&machine).await?;454 let data = host.decrypt(secret).await?;455 if plaintext {456 let s = String::from_utf8(data).context("output is not utf8")?;457 print!("{s}");458 } else {459 println!("{}", z85::encode(&data));460 }461 }462 Secret::UpdateShared {463 name,464 machines,465 add_machines,466 remove_machines,467 prefer_identities,468 } => {469 let secret = config.shared_secret(&name)?;470 if secret.secret.secret.is_none() {471 bail!("no secret");472 }473474 let initial_machines = secret.owners.clone();475 let target_machines = parse_machines(476 initial_machines.clone(),477 machines,478 add_machines,479 remove_machines,480 )?;481482 if target_machines.is_empty() {483 info!("no machines left for secret, removing it");484 config.remove_shared(&name);485 return Ok(());486 }487488 let config_field = &config.config_unchecked_field;489 let config_field = nix_go!(config_field.configUnchecked);490 let field = nix_go!(config_field.sharedSecrets[{ name }]);491492 let updated = update_owner_set(493 &name,494 config,495 secret,496 field,497 &target_machines,498 &prefer_identities,499 )500 .await?;501 config.replace_shared(name, updated);502 }503 Secret::Regenerate { prefer_identities } => {504 info!("checking for secrets to regenerate");505 {506 let _span = info_span!("shared").entered();507 let expected_shared_set = config508 .list_configured_shared()509 .await?510 .into_iter()511 .collect::<HashSet<_>>();512 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();513 for missing in expected_shared_set.difference(&shared_set) {514 let config_field = &config.config_unchecked_field;515 let config_field = nix_go!(config_field.configUnchecked);516 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);517 let expected_owners: Option<Vec<String>> =518 nix_go_json!(secret.expectedOwners);519 let Some(expected_owners) = expected_owners else {520 // TODO: Might still need to regenerate521 continue;522 };523 info!("generating secret: {missing}");524 let shared = generate_shared(config, missing, secret, expected_owners)525 .in_current_span()526 .await?;527 config.replace_shared(missing.to_string(), shared)528 }529 }530 for host in config.list_hosts().await? {531 let _span = info_span!("host", host = host.name).entered();532 let expected_set = host533 .list_configured_secrets()534 .in_current_span()535 .await?536 .into_iter()537 .collect::<HashSet<_>>();538 let stored_set = config539 .list_secrets(&host.name)540 .into_iter()541 .collect::<HashSet<_>>();542 for missing in expected_set.difference(&stored_set) {543 info!("generating secret: {missing}");544 let secret = host.secret_field(missing).in_current_span().await?;545 let generated =546 match generate(config, missing, secret, &[host.name.clone()])547 .in_current_span()548 .await549 {550 Ok(v) => v,551 Err(e) => {552 error!("{e}");553 continue;554 }555 };556 config.insert_secret(&host.name, missing.to_string(), generated)557 }558 }559 let mut to_remove = Vec::new();560 for name in &config.list_shared() {561 info!("updating secret: {name}");562 let data = config.shared_secret(name)?;563 let config_field = &config.config_unchecked_field;564 let config_field = nix_go!(config_field.configUnchecked);565 let expected_owners: Vec<String> =566 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);567 if expected_owners.is_empty() {568 warn!("secret was removed from fleet config: {name}, removing from data");569 to_remove.push(name.to_string());570 continue;571 }572573 let secret = nix_go!(config_field.sharedSecrets[{ name }]);574 config.replace_shared(575 name.to_owned(),576 update_owner_set(577 &name,578 config,579 data,580 secret,581 &expected_owners,582 &prefer_identities,583 )584 .await?,585 );586 }587 for k in to_remove {588 config.remove_shared(&k);589 }590 }591 Secret::List {} => {592 let _span = info_span!("loading secrets").entered();593 let configured = config.list_configured_shared().await?;594 #[derive(Tabled)]595 struct SecretDisplay {596 #[tabled(rename = "Name")]597 name: String,598 #[tabled(rename = "Owners")]599 owners: String,600 }601 let mut table = vec![];602 for name in configured.iter().cloned() {603 let config = config.clone();604 let expected_owners = config.shared_secret_expected_owners(&name).await?;605 let data = config.shared_secret(&name)?;606 let owners = data607 .owners608 .iter()609 .map(|o| {610 if expected_owners.contains(o) {611 o.green().to_string()612 } else {613 o.red().to_string()614 }615 })616 .collect::<Vec<_>>();617 table.push(SecretDisplay {618 owners: owners.join(", "),619 name,620 })621 }622 info!("loaded\n{}", Table::new(table).to_string())623 }624 }625 Ok(())626 }627}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 /// Force load host keys for all defined hosts26 ForceKeys,27 /// Add secret, data should be provided in stdin28 AddShared {29 /// Secret name30 name: String,31 /// Secret owners32 machines: Vec<String>,33 /// Override secret if already present34 #[clap(long)]35 force: bool,36 /// Secret public part37 #[clap(long)]38 public: Option<String>,39 /// Load public part from specified file40 #[clap(long)]41 public_file: Option<PathBuf>,4243 /// Create a notification on secret expiration44 #[clap(long)]45 expires_at: Option<DateTime<Utc>>,4647 /// Secret with this name already exists, override its value while keeping the same owners.48 #[clap(long)]49 re_add: bool,50 },51 /// Add secret, data should be provided in stdin52 Add {53 /// Secret name54 name: String,55 /// Secret owners56 machine: String,57 /// Override secret if already present58 #[clap(long)]59 force: bool,60 #[clap(long)]61 public: Option<String>,62 #[clap(long)]63 public_file: Option<PathBuf>,64 },65 /// Read secret from remote host, requires sudo on said host66 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 /// Which host should we use to decrypt84 #[clap(long)]85 prefer_identities: Vec<String>,86 },87 Regenerate {88 /// Which host should we use to decrypt, in case if reencryption is required, without89 /// regeneration90 #[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 // TODO: Remove this warning for revokable secrets.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 // Can't properly check on nix module system level232 {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 // Generators provide additional information in passthru, to access244 // passthru we should call generator, but information about where this generator is supposed to build245 // is located in passthru... Thus evaluating generator on host.246 //247 // Maybe it is also possible to do some magic with __functor?248 //249 // I don't want to make modules always responsible for additional secret data anyway,250 // so it should be in derivation, and not in the secret data itself.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 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);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 // ensure!(machines.is_some() || !add_machines.is_empty() || )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 // TODO: maybe force secret regeneration?337 // Not that useful without revokation.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 // Fixme: use clap to limit this usage368 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 // TODO: Might still need to regenerate518 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}cmds/fleet/src/host.rsdiffbeforeafterboth--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -204,7 +204,7 @@
pub async fn host(&self, name: &str) -> Result<ConfigHost> {
let config = &self.config_unchecked_field;
- let nixos_config = nix_go!(config.configuredSystems[{ name }].config);
+ let nixos_config = nix_go!(config.hosts[{ name }].nixosSystem.config);
Ok(ConfigHost {
config: self.clone(),
name: name.to_owned(),
@@ -236,9 +236,7 @@
/// Shared secrets configured in fleet.nix or in flake
pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
let config_field = &self.config_unchecked_field;
- nix_go!(config_field.configUnchecked.sharedSecrets)
- .list_fields()
- .await
+ nix_go!(config_field.sharedSecrets).list_fields().await
}
/// Shared secrets configured in fleet.nix
pub fn list_shared(&self) -> Vec<String> {
@@ -299,7 +297,7 @@
pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {
let config_field = &self.config_unchecked_field;
Ok(nix_go_json!(
- config_field.configUnchecked.sharedSecrets[{ secret }].expectedOwners
+ config_field.sharedSecrets[{ secret }].expectedOwners
))
}
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -22,7 +22,7 @@
++ [
data
({...}: {
- inherit globalModules hosts;
+ inherit globalModules hosts overlays;
})
]
++ modules;
@@ -39,7 +39,6 @@
root,
data,
}: {
- configuredHosts = root.config.hosts;
config = root.config;
};
defaultData = withData {
@@ -49,9 +48,9 @@
uncheckedData = withData {inherit data root;};
in {
inherit nixpkgs overlays;
- inherit (defaultData) configuredHosts configuredSystems config buildableSystems;
+ inherit (defaultData) config;
unchecked = {
- inherit (uncheckedData) configuredHosts configuredSystems config buildableSystems;
+ inherit (uncheckedData) config;
};
};
}