difftreelog
refactor rework fleet.nix secret bookkeeping
in: trunk
17 files changed
Cargo.lockdiffbeforeafterboth70 "aes",70 "aes",71 "aes-gcm",71 "aes-gcm",72 "age-core",72 "age-core",73 "base64",73 "base64 0.21.7",74 "bcrypt-pbkdf",74 "bcrypt-pbkdf",75 "bech32",75 "bech32",76 "cbc",76 "cbc",102source = "registry+https://github.com/rust-lang/crates.io-index"102source = "registry+https://github.com/rust-lang/crates.io-index"103checksum = "a5f11899bc2bbddd135edbc30c36b1924fa59d0746bb45beb5933fafe3fe509b"103checksum = "a5f11899bc2bbddd135edbc30c36b1924fa59d0746bb45beb5933fafe3fe509b"104dependencies = [104dependencies = [105 "base64",105 "base64 0.21.7",106 "chacha20poly1305",106 "chacha20poly1305",107 "cookie-factory",107 "cookie-factory",108 "hkdf",108 "hkdf",252source = "registry+https://github.com/rust-lang/crates.io-index"252source = "registry+https://github.com/rust-lang/crates.io-index"253checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"253checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"254255[[package]]256name = "base64"257version = "0.22.1"258source = "registry+https://github.com/rust-lang/crates.io-index"259checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"254260255[[package]]261[[package]]256name = "base64ct"262name = "base64ct"534dependencies = [540dependencies = [535 "bitflags",541 "bitflags",536 "crossterm_winapi",542 "crossterm_winapi",543 "filedescriptor",537 "libc",544 "libc",538 "mio",545 "mio",539 "parking_lot",546 "parking_lot",694source = "registry+https://github.com/rust-lang/crates.io-index"701source = "registry+https://github.com/rust-lang/crates.io-index"695checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"702checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"703704[[package]]705name = "filedescriptor"706version = "0.8.2"707source = "registry+https://github.com/rust-lang/crates.io-index"708checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"709dependencies = [710 "libc",711 "thiserror",712 "winapi",713]696714697[[package]]715[[package]]698name = "find-crate"716name = "find-crate"712 "age-core",730 "age-core",713 "anyhow",731 "anyhow",714 "async-trait",732 "async-trait",715 "base64",733 "base64 0.22.1",716 "better-command",734 "better-command",717 "chrono",735 "chrono",718 "clap",736 "clap",719 "crossterm",737 "crossterm",738 "fleet-shared",720 "futures",739 "futures",721 "hostname",740 "hostname",722 "human-repr",741 "human-repr",741 "tracing-indicatif",760 "tracing-indicatif",742 "tracing-subscriber",761 "tracing-subscriber",743 "unindent",762 "unindent",744 "z85",745]763]746764747[[package]]765[[package]]751 "age",769 "age",752 "anyhow",770 "anyhow",753 "clap",771 "clap",772 "fleet-shared",754 "nix",773 "nix",755 "serde",774 "serde",756 "serde_json",775 "serde_json",757 "tempfile",776 "tempfile",758 "tracing",777 "tracing",759 "tracing-subscriber",778 "tracing-subscriber",760 "z85",761]779]780781[[package]]782name = "fleet-shared"783version = "0.1.0"784dependencies = [785 "base64 0.22.1",786 "serde",787 "unicode_categories",788 "z85",789]762790763[[package]]791[[package]]764name = "fluent"792name = "fluent"1824source = "registry+https://github.com/rust-lang/crates.io-index"1852source = "registry+https://github.com/rust-lang/crates.io-index"1825checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"1853checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"1826dependencies = [1854dependencies = [1827 "base64",1855 "base64 0.21.7",1828 "bitflags",1856 "bitflags",1829 "serde",1857 "serde",1830 "serde_derive",1858 "serde_derive",201320412014[[package]]2042[[package]]2015name = "serde"2043name = "serde"2016version = "1.0.201"2044version = "1.0.202"2017source = "registry+https://github.com/rust-lang/crates.io-index"2045source = "registry+https://github.com/rust-lang/crates.io-index"2018checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"2046checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"2019dependencies = [2047dependencies = [2020 "serde_derive",2048 "serde_derive",2021]2049]203120592032[[package]]2060[[package]]2033name = "serde_derive"2061name = "serde_derive"2034version = "1.0.201"2062version = "1.0.202"2035source = "registry+https://github.com/rust-lang/crates.io-index"2063source = "registry+https://github.com/rust-lang/crates.io-index"2036checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"2064checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"2037dependencies = [2065dependencies = [2038 "proc-macro2",2066 "proc-macro2",2039 "quote",2067 "quote",2570source = "registry+https://github.com/rust-lang/crates.io-index"2598source = "registry+https://github.com/rust-lang/crates.io-index"2571checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"2599checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"26002601[[package]]2602name = "unicode_categories"2603version = "0.1.1"2604source = "registry+https://github.com/rust-lang/crates.io-index"2605checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"257226062573[[package]]2607[[package]]2574name = "unindent"2608name = "unindent"Cargo.tomldiffbeforeafterboth9bifrostlink = "0.1.0"9bifrostlink = "0.1.0"10uuid = { version = "1.7.0", features = ["v4"] }10uuid = { version = "1.7.0", features = ["v4"] }11tokio = { version = "1.36.0", features = ["fs", "rt", "macros", "sync", "time", "rt-multi-thread"] }11tokio = { version = "1.36.0", features = ["fs", "rt", "macros", "sync", "time", "rt-multi-thread"] }12fleet-shared = { path = "./crates/fleet-shared" }121313cmds/fleet/Cargo.tomldiffbeforeafterboth19age-core = "0.10"19age-core = "0.10"20peg = "0.8"20peg = "0.8"21age = { version = "0.10", features = ["ssh", "armor"] }21age = { version = "0.10", features = ["ssh", "armor"] }22base64 = "0.21"22base64 = "0.22.1"23chrono = { version = "0.4", features = ["serde"] }23chrono = { version = "0.4", features = ["serde"] }24z85 = "3.0"24# Using fixed version for rust on stable nixos branches.25clap = { version = ">=4.4, <4.5", features = ["derive", "env", "wrap_help", "unicode"] }25clap = { version = ">=4.4, <4.5", features = [26 "derive",27 "env",28 "wrap_help",29 "unicode",30] }26tracing = "0.1"31tracing = "0.1"27tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }32tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }40unindent = "0.2"45unindent = "0.2"41regex = "1.10"46regex = "1.10"42openssh = "0.10"47openssh = "0.10"43crossterm = "0.27"48crossterm = { version = "0.27.0", features = ["use-dev-tty"] }49fleet-shared.workspace = true445045tracing-indicatif = { version = "0.3", optional = true }51tracing-indicatif = { version = "0.3", optional = true }46human-repr = { version = "1.1", optional = true }52human-repr = { version = "1.1", optional = true }47indicatif = { version = "0.17", optional = true }53indicatif = { version = "0.17", optional = true }485449[features]55[features]50# Not quite stable56# Not quite stable51indicatif = ["tracing-indicatif", "dep:indicatif", "human-repr", "better-command/indicatif"]57indicatif = [58 "tracing-indicatif",59 "dep:indicatif",60 "human-repr",61 "better-command/indicatif",62]5263cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth1use crate::{1use std::{2 better_nix_eval::Field,2 collections::{BTreeMap, BTreeSet, HashSet},3 fleetdata::{FleetSecret, FleetSharedSecret, SecretData},3 ffi::OsString,4 io::{self, stdin, stdout, Read, Write},4 host::Config,5 path::PathBuf,5 nix_go, nix_go_json,6};6};77use anyhow::{anyhow, bail, ensure, Context, Result};8use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use chrono::{DateTime, Utc};9use clap::{error::ErrorKind, Parser};10use clap::Parser;10use crossterm::{terminal, tty::IsTty};11use crossterm::{terminal, tty::IsTty};12use fleet_shared::SecretData;11use itertools::Itertools;13use itertools::Itertools;12use owo_colors::OwoColorize;14use owo_colors::OwoColorize;13use serde::Deserialize;15use serde::Deserialize;14use std::{15 collections::{BTreeSet, HashSet},16 ffi::OsString,17 io::{self, stdin, Cursor, Read, Write},18 path::PathBuf,19};20use tabled::{Table, Tabled};16use tabled::{Table, Tabled};21use tempfile::NamedTempFile;17use tokio::{fs::read, process::Command};22use tokio::{fs::read_to_string, process::Command};23use tracing::{error, info, info_span, warn, Instrument};18use tracing::{error, info, info_span, warn, Instrument};241920use crate::{21 better_nix_eval::Field,22 fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},23 host::Config,24 nix_go, nix_go_json,25};2625#[derive(Parser)]27#[derive(Parser)]26pub enum Secret {28pub enum Secret {27 /// Force load host keys for all defined hosts29 /// Force load host keys for all defined hosts38 /// Secret public part40 /// Secret public part39 #[clap(long)]41 #[clap(long)]40 public: Option<String>,42 public: Option<String>,43 /// How to name public secret part44 #[clap(long, default_value = "public")]45 public_name: String,41 /// Load public part from specified file46 /// Load public part from specified file42 #[clap(long)]47 #[clap(long)]43 public_file: Option<PathBuf>,48 public_file: Option<PathBuf>,49 /// Secret with this name already exists, override its value while keeping the same owners.54 /// Secret with this name already exists, override its value while keeping the same owners.50 #[clap(long)]55 #[clap(long)]51 re_add: bool,56 re_add: bool,5758 #[clap(default_value = "secret")]59 part_name: String,52 },60 },53 /// Add secret, data should be provided in stdin61 /// Add secret, data should be provided in stdin54 Add {62 Add {59 /// Override secret if already present67 /// Override secret if already present60 #[clap(long)]68 #[clap(long)]61 force: bool,69 force: bool,70 /// Secret public part62 #[clap(long)]71 #[clap(long)]63 public: Option<String>,72 public: Option<String>,73 /// How to name public secret part74 #[clap(long, default_value = "public")]75 public_name: String,76 /// Load public part from specified file64 #[clap(long)]77 #[clap(long)]65 public_file: Option<PathBuf>,78 public_file: Option<PathBuf>,7980 #[clap(default_value = "secret")]81 part_name: String,66 },82 },67 /// Read secret from remote host, requires sudo on said host83 /// Read secret from remote host, requires sudo on said host68 Read {84 Read {69 name: String,85 name: String,70 machine: String,86 machine: String,8771 #[clap(long)]88 #[clap(default_value = "secret")]72 plaintext: bool,89 part_name: String,73 },90 },74 ReadPublic {75 name: String,76 machine: String,77 },78 UpdateShared {91 UpdateShared {79 name: String,92 name: String,809389 /// Which host should we use to decrypt102 /// Which host should we use to decrypt90 #[clap(long)]103 #[clap(long)]91 prefer_identities: Vec<String>,104 prefer_identities: Vec<String>,105106 #[clap(default_value = "secret")]107 part_name: String,92 },108 },93 Regenerate {109 Regenerate {94 /// Which host should we use to decrypt, in case if reencryption is required, without110 /// Which host should we use to decrypt, in case if reencryption is required, without97 prefer_identities: Vec<String>,113 prefer_identities: Vec<String>,98 },114 },99 List {},115 List {},116 Edit {117 name: String,118 machine: String,119120 #[clap(default_value = "secret")]121 part: String,122123 #[clap(long)]124 add: bool,125 },100}126}101127102#[tracing::instrument(skip(config, secret, field, prefer_identities))]128#[tracing::instrument(skip(config, secret, field, prefer_identities))]144 bail!("no available holder found");170 bail!("no available holder found");145 };171 };146172147 if let Some(data) = secret.secret.secret {173 for (part_name, part) in secret.secret.parts.iter_mut() {174 let _span = info_span!("part reencryption", part_name);175 if !part.raw.encrypted {176 continue;177 }148 let host = config.host(identity_holder).await?;178 let host = config.host(identity_holder).await?;149 let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;179 let encrypted = host180 .reencrypt(part.raw.clone(), updated_set.to_vec())181 .await?;150 secret.secret.secret = Some(encrypted);182 part.raw = encrypted;151 }183 }152184153 secret.owners = updated_set.to_vec();185 secret.owners = updated_set.to_vec();232 ensure!(marker == "SUCCESS", "generation not succeeded");264 ensure!(marker == "SUCCESS", "generation not succeeded");233 }265 }234266235 let public = host.read_file_text(format!("{out}/public")).await.ok();267 let mut parts = BTreeMap::new();268 for part in host.read_dir(&out).await? {269 if part == "created_at" || part == "expired_at" || part == "marker" {270 continue;236 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();271 }272 let contents: SecretData = host273 .read_file_text(format!("{out}/{part}"))274 .await?275 .parse()237 if let Some(secret) = &secret {276 .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;238 ensure!(239 age::Decryptor::new(Cursor::new(&secret)).is_ok(),277 parts.insert(part.to_owned(), FleetSecretPart { raw: contents });240 "builder produced non-encrypted value as secret, this is highly insecure, and not allowed."241 );242 }278 }243279244 let created_at = host.read_file_value(format!("{out}/created_at")).await?;280 let created_at = host.read_file_value(format!("{out}/created_at")).await?;247 Ok(FleetSecret {283 Ok(FleetSecret {248 created_at,284 created_at,249 expires_at,285 expires_at,250 public,286 parts,251 secret: secret.map(SecretData),252 })287 })253}288}254async fn generate(289async fn generate(310async fn parse_public(345async fn parse_public(311 public: Option<String>,346 public: Option<String>,312 public_file: Option<PathBuf>,347 public_file: Option<PathBuf>,313) -> Result<Option<String>> {348) -> Result<Option<SecretData>> {314 Ok(match (public, public_file) {349 Ok(match (public, public_file) {315 (Some(v), None) => Some(v),350 (Some(v), None) => Some(SecretData {351 data: v.into(),316 (None, Some(v)) => Some(read_to_string(v).await?),352 encrypted: false,353 }),354 (None, Some(v)) => Some(SecretData {355 data: read(v).await?,356 encrypted: false,357 }),317 (Some(_), Some(_)) => {358 (Some(_), Some(_)) => {318 bail!("only public or public_file should be set")359 bail!("only public or public_file should be set")319 }360 }320 (None, None) => None,361 (None, None) => None,321 })362 })322}363}323364365async fn parse_secret() -> Result<Option<Vec<u8>>> {366 let mut input = vec![];367 io::stdin().read_to_end(&mut input)?;368 if input.is_empty() {369 Ok(None)370 } else {371 Ok(Some(input))372 }373}374324fn parse_machines(375fn parse_machines(325 initial: Vec<String>,376 initial: Vec<String>,326 machines: Option<Vec<String>>,377 machines: Option<Vec<String>>,391 name,442 name,392 force,443 force,393 public,444 public,445 public_name,394 public_file,446 public_file,395 expires_at,447 expires_at,396 re_add,448 re_add,449 part_name,397 } => {450 } => {398 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).451 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).399452415468416 let recipients = config.recipients(machines.clone()).await?;469 let recipients = config.recipients(machines.clone()).await?;417470418 let secret = {471 let mut parts = BTreeMap::new();419 let mut input = vec![];420 io::stdin().read_to_end(&mut input)?;421472422 if input.is_empty() {473 let mut input = vec![];423 None474 io::stdin().read_to_end(&mut input)?;475424 } else {476 if !input.is_empty() {425 Some(477 let encrypted = encrypt_secret_data(recipients, input)426 SecretData::encrypt(recipients, input)427 .ok_or_else(|| anyhow!("no recipients provided"))?,478 .ok_or_else(|| anyhow!("no recipients provided"))?;428 )479 parts.insert(part_name, FleetSecretPart { raw: encrypted });429 }480 }430 };481431 let public = parse_public(public, public_file).await?;482 if let Some(public) = parse_public(public, public_file).await? {483 parts.insert(public_name, FleetSecretPart { raw: public });484 }485432 config.replace_shared(486 config.replace_shared(433 name,487 name,436 secret: FleetSecret {490 secret: FleetSecret {437 created_at: Utc::now(),491 created_at: Utc::now(),438 expires_at,492 expires_at,439 secret,493 parts,440 public,441 },494 },442 },495 },443 );496 );447 name,500 name,448 force,501 force,449 public,502 public,503 public_name,450 public_file,504 public_file,505 part_name,451 } => {506 } => {452 let recipient = config.recipient(&machine).await?;507 if config.has_secret(&machine, &name) && !force {508 bail!("secret already defined");509 }453510454 let secret = {511 let mut parts = BTreeMap::new();455 let mut input = vec![];456 io::stdin().read_to_end(&mut input)?;457 if input.is_empty() {458 bail!("no data provided")459 }460512461 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))513 if let Some(secret) = parse_secret().await? {514 let recipient = config.recipient(&machine).await?;515 let encrypted =516 encrypt_secret_data(vec![recipient], secret).expect("recipient provided");517 parts.insert(part_name, FleetSecretPart { raw: encrypted });518 }519520 if let Some(public) = parse_public(public, public_file).await? {521 parts.insert(public_name, FleetSecretPart { raw: public });462 };522 };463523464 if config.has_secret(&machine, &name) && !force {465 bail!("secret already defined");466 }467 let public = parse_public(public, public_file).await?;468469 config.insert_secret(524 config.insert_secret(470 &machine,525 &machine,471 name,526 name,472 FleetSecret {527 FleetSecret {473 created_at: Utc::now(),528 created_at: Utc::now(),474 expires_at: None,529 expires_at: None,475 secret,530 parts,476 public,477 },531 },478 );532 );479 }533 }480 #[allow(clippy::await_holding_refcell_ref)]534 #[allow(clippy::await_holding_refcell_ref)]481 Secret::Read {535 Secret::Read {482 name,536 name,483 machine,537 machine,484 plaintext,538 part_name,485 } => {539 } => {486 let secret = config.host_secret(&machine, &name)?;540 let secret = config.host_secret(&machine, &name)?;487 let Some(secret) = secret.secret else {541 let Some(secret) = secret.parts.get(&part_name) else {488 bail!("no secret {name}");542 bail!("no part {part_name} in secret {name}");489 };543 };490 let host = config.host(&machine).await?;544 let data = if secret.raw.encrypted {491 let data = host.decrypt(secret).await?;545 let host = config.host(&machine).await?;492 if plaintext {546 host.decrypt(secret.raw.clone()).await?493 let s = String::from_utf8(data).context("output is not utf8")?;494 print!("{s}");495 } else {547 } else {496 println!("{}", z85::encode(&data));548 secret.raw.data.clone()497 }498 }499 Secret::ReadPublic { name, machine } => {500 let secret = config.host_secret(&machine, &name)?;501 let Some(public) = secret.public else {502 bail!("no secret {name}");503 };549 };550504 print!("{public}");551 stdout().write_all(&data)?;505 }552 }506 Secret::UpdateShared {553 Secret::UpdateShared {507 name,554 name,508 machines,555 machines,509 add_machines,556 add_machines,510 remove_machines,557 remove_machines,511 prefer_identities,558 prefer_identities,559 part_name,512 } => {560 } => {513 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).561 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).514562515 let secret = config.shared_secret(&name)?;563 let secret = config.shared_secret(&name)?;516 if secret.secret.secret.is_none() {564 if secret.secret.parts.get(&part_name).is_none() {517 bail!("no secret");565 bail!("no secret");518 }566 }519567572 }620 }573 }621 }574 for host in config.list_hosts().await? {622 for host in config.list_hosts().await? {623 if config.should_skip(&host.name) {624 continue;625 }626575 let _span = info_span!("host", host = host.name).entered();627 let _span = info_span!("host", host = host.name).entered();576 let expected_set = host628 let expected_set = host577 .list_configured_secrets()629 .list_configured_secrets()664 }716 }665 info!("loaded\n{}", Table::new(table).to_string())717 info!("loaded\n{}", Table::new(table).to_string())666 }718 }719 Secret::Edit {720 name,721 machine,722 part,723 add,724 } => {725 let secret = config.host_secret(&machine, &name)?;726 if let Some(data) = secret.parts.get(&part) {727 let host = config.host(&machine).await?;728 let secret = host.decrypt(data.raw.clone()).await?;729 String::from_utf8(secret).context("secret is not utf8")?730 } else if add {731 String::new()732 } else {733 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");734 };735 }667 }736 }668 Ok(())737 Ok(())669 }738 }739}740741async fn edit_temp_file(742 builder: tempfile::Builder<'_, '_>,743 r: Vec<u8>,744 header: &str,745 comment: &str,746) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {747 if !stdin().is_tty() {748 // TODO: Also try to open /dev/tty directly?749 bail!("stdin is not tty, can't open editor");750 }751752 use std::fmt::Write;753 let mut file = builder.tempfile()?;754755 let mut full_header = String::new();756 let mut had = false;757 for line in header.trim_end().lines() {758 had = true;759 writeln!(&mut full_header, "{comment}{line}")?;760 }761 if had {762 writeln!(&mut full_header, "{}", comment.trim_end())?;763 }764 writeln!(765 &mut full_header,766 "{comment}Do not touch this header! It will be removed automatically"767 )?;768769 file.write_all(full_header.as_bytes())?;770 file.write_all(&r)?;771772 let abs_path = file.into_temp_path();773 let editor = std::env::var_os("VISUAL")774 .or_else(|| std::env::var_os("EDITOR"))775 .unwrap_or_else(|| "vi".into());776 let editor_args = shlex::bytes::split(editor.as_encoded_bytes())777 .ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;778 let editor_args = editor_args779 .into_iter()780 .map(|v| {781 // Only ASCII subsequences are replaced782 unsafe { OsString::from_encoded_bytes_unchecked(v) }783 })784 .collect_vec();785 let Some((editor, args)) = editor_args.split_first() else {786 bail!("EDITOR env var has no command");787 };788 let mut command = Command::new(editor);789 command.args(args);790791 let path_arg = abs_path.canonicalize()?;792793 // TODO: Save full state, using tcget/_getmode/_setmode794 let was_raw = terminal::is_raw_mode_enabled()?;795 terminal::enable_raw_mode()?;796797 let status = command.arg(path_arg).status().await;798799 if !was_raw {800 terminal::disable_raw_mode()?;801 }802803 let success = match status {804 Ok(s) => s.success(),805 Err(e) if e.kind() == io::ErrorKind::NotFound => {806 bail!("editor not found")807 }808 Err(e) => bail!("editor spawn error: {e}"),809 };810811 let mut file = std::fs::read(&abs_path).context("read editor output")?;812 let Some(v) = file.strip_prefix(full_header.as_bytes()) else {813 todo!();814 };815 todo!();816817 // Ok((success, abs_path))670}818}671819cmds/fleet/src/fleetdata.rsdiffbeforeafterboth1use age::Recipient;1use std::{2 collections::BTreeMap,3 io::{self, Cursor},4};52use anyhow::Result;6use age::Recipient;3use chrono::{DateTime, Utc};7use chrono::{DateTime, Utc};4use itertools::Itertools;8use fleet_shared::SecretData;5use nixlike::format_nix;9use itertools::Itertools;6use serde::{Deserialize, Deserializer, Serialize, Serializer};10use serde::{de::Error, Deserialize, Serialize};7use std::{8 collections::BTreeMap,9 io::{self, Cursor},10};11use tempfile::TempDir;12use tokio::{13 fs::{self, File},14 io::AsyncWriteExt,15 process::Command,16};171118#[derive(Serialize, Deserialize, Default)]12#[derive(Serialize, Deserialize, Default)]19#[serde(rename_all = "camelCase")]13#[serde(rename_all = "camelCase")]23 pub encryption_key: String,17 pub encryption_key: String,24}18}1920const VERSION: &str = "0.1.0";21pub struct FleetDataVersion;22impl Serialize for FleetDataVersion {23 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>24 where25 S: serde::Serializer,26 {27 VERSION.serialize(serializer)28 }29}30impl<'de> Deserialize<'de> for FleetDataVersion {31 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>32 where33 D: serde::Deserializer<'de>,34 {35 let version = String::deserialize(deserializer)?;36 if version != VERSION {37 return Err(D::Error::custom(format!(38 "fleet.nix data version mismatch, expected {VERSION}, got {version}.\nFollow the docs for migration instruction"39 )));40 }41 Ok(Self)42 }43}254426#[derive(Serialize, Deserialize)]45#[derive(Serialize, Deserialize)]27#[serde(rename_all = "camelCase")]46#[serde(rename_all = "camelCase")]28pub struct FleetData {47pub struct FleetData {48 pub version: FleetDataVersion,4929 #[serde(default)]50 #[serde(default)]30 pub hosts: BTreeMap<String, HostData>,51 pub hosts: BTreeMap<String, HostData>,45 pub secret: FleetSecret,66 pub secret: FleetSecret,46}67}476848#[derive(Serialize, Deserialize, Clone)]49pub struct SecretData(50 #[serde(51 default,52 skip_serializing_if = "Vec::is_empty",53 serialize_with = "as_z85",54 deserialize_with = "from_z85"55 )]56 pub Vec<u8>,57);58impl SecretData {59 /// Returns None if recipients.is_empty()69/// Returns None if recipients.is_empty()60 pub fn encrypt(70pub fn encrypt_secret_data(61 recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,71 recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,62 data: Vec<u8>,72 data: Vec<u8>,63 ) -> Option<Self> {73) -> Option<SecretData> {64 let mut encrypted = vec![];74 let mut encrypted = vec![];65 let recipients = recipients75 let recipients = recipients66 .into_iter()76 .into_iter()71 .expect("in memory write");81 .expect("in memory write");72 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");82 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");73 encryptor.finish().expect("in memory flush");83 encryptor.finish().expect("in memory flush");74 Some(Self(encrypted))84 Some(SecretData {85 data: encrypted,86 encrypted: true,87 })75 }88}76 pub fn encode_z85(&self) -> String {8977 z85::encode(&self.0)90#[derive(Serialize, Deserialize, Clone)]78 }91pub struct FleetSecretPart {79 pub fn decode_z85(v: &str) -> Result<Self> {92 pub raw: SecretData,80 let v = z85::decode(v)?;93}81 Ok(Self(v))82 }83}849485#[derive(Serialize, Deserialize, Clone)]95#[derive(Serialize, Deserialize, Clone)]86#[serde(rename_all = "camelCase")]96#[serde(rename_all = "camelCase")]92 #[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]102 #[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]93 pub expires_at: Option<DateTime<Utc>>,103 pub expires_at: Option<DateTime<Utc>>,10494 #[serde(skip_serializing_if = "Option::is_none")]105 #[serde(flatten)]95 pub public: Option<String>,106 pub parts: BTreeMap<String, FleetSecretPart>,96 #[serde(skip_serializing_if = "Option::is_none")]97 pub secret: Option<SecretData>,98}107}99100fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>101where102 S: Serializer,103{104 serializer.serialize_str(&z85::encode(key))105}106107fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>108where109 D: Deserializer<'de>,110{111 use serde::de::Error;112 String::deserialize(deserializer)113 .and_then(|string| z85::decode(string).map_err(|err| Error::custom(err.to_string())))114}115116/// Isn't used yet117#[allow(dead_code)]118pub async fn dummy_flake() -> Result<TempDir> {119 let data_str = fs::read_to_string("fleet.nix").await?;120121 let mut cmd = Command::new("nix");122 cmd.arg("flake").arg("metadata").arg("--json");123124 let flake_dir = tempfile::tempdir()?;125 let mut flake_nix = flake_dir.path().to_path_buf();126 flake_nix.push("flake.nix");127 // flake_dir128129 File::create(&flake_nix)130 .await?131 .write_all(132 format_nix(&format!(133 "134 {{135 outputs = {{self, ...}}: {{136 data = {data_str};137 }};138 }}139 "140 ))141 .as_bytes(),142 )143 .await?;144145 // std::thread::sleep(Duration::MAX);146 // flake_dir.close()147 // FIXME148 dbg!(&flake_nix);149 Ok(flake_dir)150}151108cmds/fleet/src/host.rsdiffbeforeafterboth9 sync::{Arc, Mutex, MutexGuard, OnceLock},9 sync::{Arc, Mutex, MutexGuard, OnceLock},10};10};111112use anyhow::{anyhow, bail, Context, Result};12use anyhow::{anyhow, bail, ensure, Context, Result};13use clap::{ArgGroup, Parser};13use clap::{ArgGroup, Parser};14use fleet_shared::SecretData;14use openssh::SessionBuilder;15use openssh::SessionBuilder;15use serde::de::DeserializeOwned;16use serde::de::DeserializeOwned;16use tempfile::NamedTempFile;17use tempfile::NamedTempFile;171818use crate::{19use crate::{19 better_nix_eval::{Field, NixSessionPool},20 better_nix_eval::{Field, NixSessionPool},20 command::MyCommand,21 command::MyCommand,21 fleetdata::{FleetData, FleetSecret, FleetSharedSecret, SecretData},22 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},22 nix_go, nix_go_json,23 nix_go, nix_go_json,23};24};242589 cmd.arg(path);90 cmd.arg(path);90 cmd.run_string().await91 cmd.run_string().await91 }92 }93 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {94 let mut cmd = self.cmd("ls").await?;95 cmd.arg(path);96 let out = cmd.run_string().await?;97 let mut lines = out.split('\n');98 if let Some(last) = lines.next_back() {99 ensure!(last == "", "output of ls should end with newline");100 }101 Ok(lines.map(ToOwned::to_owned).collect())102 }92 #[allow(dead_code)]103 #[allow(dead_code)]93 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {104 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {94 let text = self.read_file_text(path).await?;105 let text = self.read_file_text(path).await?;111 }122 }112123113 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {124 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {125 ensure!(data.encrypted, "secret is not encrypted");114 let mut cmd = self.cmd("fleet-install-secrets").await?;126 let mut cmd = self.cmd("fleet-install-secrets").await?;115 cmd.arg("decrypt").eqarg("--secret", data.encode_z85());127 cmd.arg("decrypt").eqarg("--secret", data.to_string());116 let encoded = cmd128 let encoded = cmd117 .sudo()129 .sudo()118 .run_string()130 .run_string()119 .await131 .await120 .context("failed to call remote host for decrypt")?;132 .context("failed to call remote host for decrypt")?;121 z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")133 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;134 ensure!(!data.encrypted, "didn't decrypted secret");135 Ok(data.data)122 }136 }123 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {137 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {138 ensure!(data.encrypted, "secret is not encrypted");124 let mut cmd = self.cmd("fleet-install-secrets").await?;139 let mut cmd = self.cmd("fleet-install-secrets").await?;125 cmd.arg("reencrypt").eqarg("--secret", data.encode_z85());140 cmd.arg("reencrypt").eqarg("--secret", data.to_string());126 for target in targets {141 for target in targets {127 let key = self.config.key(&target).await?;142 let key = self.config.key(&target).await?;128 cmd.eqarg("--targets", key);143 cmd.eqarg("--targets", key);132 .run_string()147 .run_string()133 .await148 .await134 .context("failed to call remote host for decrypt")?;149 .context("failed to call remote host for decrypt")?;135 SecretData::decode_z85(encoded.trim_end()).context("bad encoded data? outdated host?")150 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;151 ensure!(!data.encrypted, "didn't decrypted secret");152 Ok(data)136 }153 }137 /// Returns path for futureproofing, as path might change i.e on conversion to CA154 /// Returns path for futureproofing, as path might change i.e on conversion to CA138 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {155 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {324 }341 }325342326 pub fn save(&self) -> Result<()> {343 pub fn save(&self) -> Result<()> {327 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;344 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;328 let data = nixlike::serialize(&self.data() as &FleetData)?;345 let data = nixlike::serialize(&self.data() as &FleetData)?;329 tempfile.write_all(346 tempfile.write_all(330 format!(347 format!(cmds/install-secrets/Cargo.tomldiffbeforeafterboth20 "unicode",20 "unicode",21] }21] }22tempfile = "3.10.0"22tempfile = "3.10.0"23z85 = "3.0.5"23fleet-shared.workspace = true2424cmds/install-secrets/src/main.rsdiffbeforeafterboth1use age::{ssh::Identity as SshIdentity, ssh::Recipient as SshRecipient, Decryptor};1use std::{2 collections::{BTreeMap, HashMap},3 fs::{self, File},4 io::{self, Cursor, Read, Write},5 iter,6 os::unix::prelude::PermissionsExt,7 path::{Path, PathBuf},8 str::{from_utf8, FromStr},9};102use age::{Encryptor, Identity, Recipient};11use age::{12 ssh::{Identity as SshIdentity, Recipient as SshRecipient},13 Decryptor, Encryptor, Identity, Recipient,14};3use anyhow::{anyhow, bail, Context, Result};15use anyhow::{anyhow, bail, ensure, Context, Result};4use clap::Parser;16use clap::Parser;5use nix::sys::stat::Mode;17use fleet_shared::SecretData;6use nix::unistd::{chown, Group, User};18use nix::unistd::{chown, Group, User};7use serde::{Deserialize, Deserializer};19use serde::Deserialize;8use std::fmt::{self, Display};9use std::fs::{self, File};10use std::io::{self, Cursor, Read, Write};11use std::iter;12use std::os::unix::prelude::PermissionsExt;13use std::path::Path;14use std::str::{from_utf8, FromStr};15use std::{collections::HashMap, path::PathBuf};16use tracing::{error, info, info_span, warn};20use tracing::{error, info_span};17use tracing_subscriber::filter::LevelFilter;21use tracing_subscriber::{filter::LevelFilter, EnvFilter};18use tracing_subscriber::EnvFilter;1920#[derive(Clone, Debug)]21struct SecretWrapper(Vec<u8>);22impl Display for SecretWrapper {23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {24 let encoded = z85::encode(&self.0);25 write!(f, "{encoded}")26 }27}28impl FromStr for SecretWrapper {29 type Err = z85::DecodeError;3031 fn from_str(s: &str) -> Result<Self, Self::Err> {32 z85::decode(s).map(Self)33 }34}35impl<'de> Deserialize<'de> for SecretWrapper {36 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>37 where38 D: Deserializer<'de>,39 {40 let v = String::deserialize(deserializer)?;41 let de = z85::decode(v).map_err(|err| serde::de::Error::custom(err.to_string()))?;42 Ok(Self(de))43 }44}452246#[derive(Parser)]23#[derive(Parser)]47#[clap(author)]24#[clap(author)]48enum Opts {25enum Opts {49 /// Install secrets from json specification26 /// Install secrets from json specification50 Install { data: PathBuf },27 Install { data: PathBuf },51 /// Reencrypt secret using host key, outputting in z85 encoded string28 /// Reencrypt secret using host key, outputting in fleet encoded string52 Reencrypt {29 Reencrypt {53 #[clap(long)]30 #[clap(long)]54 secret: SecretWrapper,31 secret: SecretData,55 #[clap(long)]32 #[clap(long)]56 targets: Vec<String>,33 targets: Vec<String>,57 },34 },58 /// Decrypt secret using host key, outputting in z85 encoded string35 /// Decrypt secret using host key, outputting in fleet encoded string59 Decrypt {36 Decrypt {60 #[clap(long)]37 #[clap(long)]61 secret: SecretWrapper,38 secret: SecretData,62 /// Shoult decoded output be printed as plaintext, instead of z85?39 /// Shoult decoded output be printed as plaintext, instead of z85?63 #[clap(long)]40 #[clap(long)]64 plaintext: bool,41 plaintext: bool,65 },42 },66}43}4445#[derive(Deserialize)]46#[serde(rename_all = "camelCase")]47struct Part {48 raw: SecretData,49 path: PathBuf,50 stable_path: PathBuf,51}675268#[derive(Deserialize)]53#[derive(Deserialize)]69#[serde(rename_all = "camelCase")]54#[serde(rename_all = "camelCase")]72 mode: String,57 mode: String,73 owner: String,58 owner: String,7475 secret: Option<SecretWrapper>,59 root_path: Option<PathBuf>,6061 #[serde(flatten)]76 public: Option<String>,62 parts: BTreeMap<String, Part>,7778 public_path: PathBuf,79 stable_public_path: PathBuf,8081 secret_path: PathBuf,82 stable_secret_path: PathBuf,83}63}846485type Data = HashMap<String, DataItem>;65type Data = HashMap<String, DataItem>;866687fn decrypt(input: &SecretWrapper, identity: &dyn Identity) -> Result<Vec<u8>> {67fn decrypt(input: &SecretData, identity: &dyn Identity) -> Result<Vec<u8>> {68 ensure!(input.encrypted, "passed data is not encrypted!");88 let mut input = Cursor::new(&input.0);69 let mut input = Cursor::new(&input.data);89 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;70 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;90 let decryptor = match decryptor {71 let decryptor = match decryptor {91 Decryptor::Recipients(r) => r,72 Decryptor::Recipients(r) => r,101 .context("failed to decrypt")?;82 .context("failed to decrypt")?;102 Ok(decrypted)83 Ok(decrypted)103}84}104fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretWrapper> {85fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretData> {105 let recipients = targets86 let recipients = targets106 .into_iter()87 .into_iter()107 .map(|t| {88 .map(|t| {119 .expect("constructor should not fail");100 .expect("constructor should not fail");120 io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");101 io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");121 encryptor.finish().context("failed to finish encryption")?;102 encryptor.finish().context("failed to finish encryption")?;122 Ok(SecretWrapper(encrypted))103 Ok(SecretData {104 data: encrypted,105 encrypted: true,106 })123}107}124108125fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {109fn init_part(identity: &dyn Identity, item: &DataItem, value: &Part) -> Result<()> {126 if let Some(public) = &value.public {127 let mut hashed = File::create(&value.public_path)?;128 let stable_dir = value.stable_public_path.parent().expect("not root");110 let stable_dir = value.stable_path.parent().expect("not root");111112 // Right now stable & non-stable data are both located in this dir.113 std::fs::create_dir_all(stable_dir)?;114129 let mut stable_temp =115 let mut stable_temp =130 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;116 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;117 let mut hashed = File::create(&value.path)?;118119 let private = value.raw.encrypted;120 let data = if private {121 decrypt(&value.raw, identity)?122 } else {123 value.raw.data.to_owned()124 };125131 hashed.write_all(public.as_bytes())?;126 hashed.write_all(&data)?;127 hashed.flush()?;132 stable_temp.write_all(public.as_bytes())?;128 stable_temp.write_all(&data)?;133 stable_temp.flush()?;129 stable_temp.flush()?;130131 let mode = if private {134 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(0o444))132 fs::Permissions::from_mode(135 .context("perm")?;133 u32::from_str_radix(&item.mode, 8).context("failed to parse mode as octal")?,134 )135 } else {136 fs::Permissions::from_mode(0o444)137 };136 fs::set_permissions(&value.public_path, fs::Permissions::from_mode(0o444))138 fs::set_permissions(stable_temp.path(), mode.clone()).context("stable temp mode")?;137 .context("perm")?;138139 stable_temp140 .persist(value.stable_public_path)141 .context("failed to persist")?;142 }143 if value.secret.is_none() {144 info!("no secret data found");145 return Ok(());146 }147 let secret = value.secret.as_ref().unwrap();148149 let mode = Mode::from_bits(139 fs::set_permissions(&value.path, mode).context("hashed mode")?;150 u32::from_str_radix(&value.mode, 8).context("failed to parse mode as octal")?,140151 )141 // Files are initially owned by root, thus making set mode first inaccessible to user, and then152 .context("failed to parse mode")?;142 // altering user/group.143 if private {153 let user = User::from_name(&value.owner)144 let user = User::from_name(&item.owner)154 .context("failed to get user")?145 .context("failed to get user")?155 .ok_or_else(|| anyhow!("user not found"))?;146 .ok_or_else(|| anyhow!("user not found"))?;156 let group = Group::from_name(&value.group)147 let group = Group::from_name(&item.group)157 .context("failed to get group")?148 .context("failed to get group")?158 .ok_or_else(|| anyhow!("group not found"))?;149 .ok_or_else(|| anyhow!("group not found"))?;159150160 let stable_dir = value.stable_secret_path.parent().expect("not root");161 let mut stable_temp =162 tempfile::NamedTempFile::new_in(stable_dir).context("failed to create tempfile")?;163 let mut hashed = File::create(&value.secret_path)?;164165 // File is owned by root, and only root can modify it166 let decrypted = decrypt(secret, identity)?;167 if decrypted.is_empty() {168 warn!("secret is decoded as empty, something is broken?");169 }170171 io::copy(&mut Cursor::new(&decrypted), &mut stable_temp)172 .context("failed to write decrypted file")?;173 io::copy(&mut Cursor::new(decrypted), &mut hashed).context("failed to write decrypted file")?;174175 // Make file owned by specified user and group, then change mode176 chown(stable_temp.path(), Some(user.uid), Some(group.gid))151 chown(stable_temp.path(), Some(user.uid), Some(group.gid))177 .context("failed to apply user/group")?;152 .context("failed to apply user/group")?;178 chown(&value.secret_path, Some(user.uid), Some(group.gid))153 chown(&value.path, Some(user.uid), Some(group.gid))179 .context("failed to apply user/group")?;154 .context("failed to apply user/group")?;180 fs::set_permissions(stable_temp.path(), fs::Permissions::from_mode(mode.bits())).unwrap();155 }181 fs::set_permissions(&value.secret_path, fs::Permissions::from_mode(mode.bits())).unwrap();156182 stable_temp157 stable_temp183 .persist(value.stable_secret_path)158 .persist(&value.stable_path)184 .context("failed to persist")?;159 .context("stable persist")?;185186 Ok(())160 Ok(())187}161}162163fn init_secret(identity: &age::ssh::Identity, value: &DataItem) -> Result<()> {164 if let Some(root_path) = &value.root_path {165 if !fs::metadata(root_path).map(|m| m.is_dir()).unwrap_or(false) {166 fs::create_dir(root_path).context("failed to create secret directory")?;167 }168 }169 for (part_id, part) in value.parts.iter() {170 let _span = info_span!("part", part_id = part_id);171 if let Err(e) = init_part(identity, value, part) {172 error!("failed to init part {part_id}: {e}");173 }174 }175176 Ok(())177}188178189fn host_identity() -> anyhow::Result<SshIdentity> {179fn host_identity() -> anyhow::Result<SshIdentity> {190 let identity = SshIdentity::from_buffer(180 let identity = SshIdentity::from_buffer(214 let mut failed = false;204 let mut failed = false;215 for (name, value) in data {205 for (name, value) in data {216 let _span = info_span!("init", name = name);206 let _span = info_span!("init", name = name);217 if let Err(e) = init_secret(&identity, value) {207 if let Err(e) = init_secret(&identity, &value) {218 error!("{e}");208 error!("secret failed to initialize: {e}");219 failed = true;209 failed = true;220 }210 }221 }211 }257 let s = String::from_utf8(decrypted).context("output is not utf8")?;247 let s = String::from_utf8(decrypted).context("output is not utf8")?;258 print!("{s}");248 print!("{s}");259 } else {249 } else {260 println!("{}", SecretWrapper(decrypted));250 println!(251 "{}",252 SecretData {253 data: decrypted,254 encrypted: false255 }256 );261 }257 }262 Ok(())258 Ok(())crates/better-command/src/handler.rsdiffbeforeafterboth1//! Collection of handlers, which transform program-specific stdout format to tracing1//! Collection of handlers, which transform program-specific stdout format to tracing223use std::collections::HashMap;4use std::sync::{Arc, Mutex};3use std::{4 collections::HashMap,5 sync::{Arc, Mutex},6};576use once_cell::sync::Lazy;8use once_cell::sync::Lazy;7use regex::Regex;9use regex::Regex;crates/fleet-shared/Cargo.tomldiffbeforeafterbothno changes
crates/fleet-shared/src/lib.rsdiffbeforeafterbothno changes
crates/nixlike/src/lib.rsdiffbeforeafterboth38 Null,38 Null,39}39}4041fn count_spaces(l: &str) -> usize {42 l.chars().take_while(|&c| c == ' ').count()43}44fn is_significant(l: &str) -> bool {45 count_spaces(l) != l.len()46}4748fn dedent(l: &str, by: usize) -> &str {49 assert!(50 l[0..by.min(l.len())].chars().all(|c| c == ' '),51 "dedent calculation is wrong"52 );53 &l[by.min(l.len())..]54}5556fn process_multiline(lines: Vec<&str>) -> String {57 // Even when parsing '''', there is single "line" between those '' delimiters.58 // unwrap_or is for case where there is no significant lines59 let dedent_by = lines60 .iter()61 .copied()62 .filter(|c| is_significant(c))63 .map(count_spaces)64 .min()65 .unwrap_or(0);6667 let mut out = String::new();6869 let mut had_first = false;70 for (i, line) in lines.into_iter().enumerate() {71 // Newline after '' is ignored, if there is no text.72 if i == 0 && !is_significant(line) {73 continue;74 }75 if had_first {76 out.push('\n');77 }78 had_first = true;79 // ''' is hard escape80 for (i, part) in dedent(line, dedent_by).split("'''").enumerate() {81 if i != 0 {82 out.push_str(r#"""""#);83 }84 // This is the only replacements done by nixlike writer, no need to support more.85 out.push_str(&part.replace("''${", "${").replace("''\\t", "\t"));86 }87 }8889 out90}409141peg::parser! {92peg::parser! {42pub grammar nixlike() for str {93pub grammar nixlike() for str {50 / "\\r" { "\r" }101 / "\\r" { "\r" }51 / "\\$" { "$" }102 / "\\$" { "$" }52 / c:$([_]) { c }103 / c:$([_]) { c }53 rule string() -> String104 rule string() -> String = singleline_string() / multiline_string();105 rule singleline_string() -> String54 = quiet! { "\"" v:(!"\"" c:string_char() {c})* "\"" { v.into_iter().collect() } } / expected!("<string>")106 = quiet! { "\"" v:(!"\"" c:string_char() {c})* "\"" { v.into_iter().collect() } } / expected!("<string>")107 pub rule multiline_string() -> String108 = "''"109 // First line may also contain text, and whitespace for it is counted, but if it is empty - then it is'nt counted as full line...110 // This logic is complicated, see `parse_multiline` test.111 lines:$(("'''" / !"''" [_])*) "''"112 {113 process_multiline(lines.split('\n').collect())114 }55 rule boolean() -> bool115 rule boolean() -> bool56 = quiet! { "true" {true}116 = quiet! { "true" {true}57 / "false" {false} } / expected!("<boolean>")117 / "false" {false} } / expected!("<boolean>")136 out196 out137}197}198199#[test]200fn parse_multiline() {201 assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), "");202 assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n");203 assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n");204 assert_eq!(nixlike::multiline_string("''''").expect("parse"), "");205 assert_eq!(nixlike::multiline_string("'' ''").expect("parse"), "");206}138207crates/nixlike/src/to_string.rsdiffbeforeafterboth38}38}393940pub fn write_nix_str(str: &str, out: &mut String) {40pub fn write_nix_str(str: &str, out: &mut String) {41 if str.ends_with('\n') {42 out.push_str("''");43 for ele in str.split('\n') {44 out.push('\n');45 out.push_str(46 &ele47 // '' is escaped with '48 .replace("''", "'''")49 // ${ is escaped wth ''50 .replace("${", "''${")51 // \t is not counted as whitespace for dedent52 // to avoid confusion, it is printed literally.53 //54 // ...Escaped \t literal should be prefixed with '' for... Idk, this logic is complicated.55 .replace('\t', "''\\t"),56 );57 }58 // Final newline is assumed due to str.ends_with condition59 out.push_str("''");60 } else {41 out.push_str(&escape_string(str))61 out.push_str(&escape_string(str))42}62 }63}436444fn write_nix_buf(value: &Value, out: &mut String) {65fn write_nix_buf(value: &Value, out: &mut String) {crates/remowt-fs/src/lib.rsdiffbeforeafterbothno syntactic changes
modules/fleet/secrets.nixdiffbeforeafterboth7with lib;7with lib;8with fleetLib; let8with fleetLib; let9 sharedSecret = with types; ({config, ...}: {9 sharedSecret = with types; ({config, ...}: {10 options = {11 managed = mkOption {10 freeformType = types.lazyAttrsOf unspecified;12 type = bool;11 options = {13 description = ''14 Is this secret managed by configuration (I.e will work with reencrypt/etc), or it is configured by user15 '';16 };1718 expectedOwners = mkOption {12 expectedOwners = mkOption {19 type = nullOr (listOf str);13 type = nullOr (listOf str);71 '';65 '';72 default = [];66 default = [];73 };67 };74 # TODO: Make secret generator generate arbitrary number of secret/public parts?75 # Make it generate a folder, where all files except suffixed by .enc are public, and the rest are secret?76 # How should modules refer to those files then?77 public = mkOption {78 type = nullOr str;79 description = "Secret public data. Imported from fleet.nix";80 default = null;81 };82 secret = mkOption {83 type = nullOr str;84 description = "Encrypted secret data. Imported from fleet.nix";85 default = null;86 internal = true;87 };88 };68 };89 });69 });90 hostSecret = with types; {70 hostSecret = with types; {71 freeformType = types.lazyAttrsOf unspecified;91 options = {72 options = {92 createdAt = mkOption {73 createdAt = mkOption {93 type = nullOr str;74 type = nullOr str;97 type = nullOr str;78 type = nullOr str;98 default = null;79 default = null;99 };80 };100 public = mkOption {101 type = nullOr str;102 description = "Secret public data. Imported from fleet.nix";103 default = null;104 };105 secret = mkOption {106 type = nullOr str;107 description = "Encrypted secret data. Imported from fleet.nix";108 default = null;109 internal = true;110 };111 };81 };112 };82 };113in {83in {114 options = with types; {84 options = with types; {85 version = mkOption {86 type = str;87 default = "";88 internal = true;89 };115 sharedSecrets = mkOption {90 sharedSecrets = mkOption {116 type = attrsOf (submodule sharedSecret);91 type = attrsOf (submodule sharedSecret);117 default = {};92 default = {};134 config.sharedSecrets;109 config.sharedSecrets;135 hosts = hostsToAttrs (host: {110 hosts = hostsToAttrs (host: {136 nixosModules = let111 nixosModules = let112 # processPart137 cleanupSecret = secretName: v: {113 processSecret = v:138 inherit (v) public secret;114 (removeAttrs v ["createdAt" "expiresAt" "expectedOwners" "owners" "regenerateOnOwnerAdded" "regenerateOnOwnerRemoved"])115 // {139 shared = true;116 shared = true;140 };117 };141 in [118 in [142 {119 {143 secrets =120 secrets =144 (121 (145 mapAttrs cleanupSecret122 mapAttrs (_: processSecret)146 (filterAttrs (_: v: builtins.elem host v.owners) config.sharedSecrets)123 (filterAttrs (_: v: builtins.elem host v.owners) config.sharedSecrets)147 )124 )148 // (mapAttrs cleanupSecret (config.hostSecrets.${host} or {}));125 // (mapAttrs (_: processSecret) (config.hostSecrets.${host} or {}));149 }126 }150 ];127 ];151 });128 });nixos/secrets.nixdiffbeforeafterboth5 ...6}:3with lib;7with lib; let48 inherit (lib.strings) hasPrefix stripPrefix;5let9 plaintextPrefix = "<PLAINTEXT>";10 plaintextNewlinePrefix = "<PLAINTEXT-NL>";116 sysConfig = config;12 sysConfig = config;13 secretPartType = secretName:14 types.submodule ({config, ...}: {15 options = with types; {16 raw = mkOption {17 description = "Secret in fleet-specific undocumented format, do not use. Import from fleet.nix";18 internal = true;19 };20 hash = mkOption {21 type = str;22 description = "Hash of secret in encoded format";23 };24 path = mkOption {25 type = str;26 description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)";27 };28 stablePath = mkOption {29 type = str;30 description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)";31 };32 data = mkOption {33 type = str;34 description = "Secret public data (only available for plaintext)";35 };36 };37 config = let38 partName = config._module.args.name;39 in {40 hash = mkOptionDefault (builtins.hashString "sha1" config.raw);41 data = mkOptionDefault (42 if hasPrefix plaintextPrefix config.raw43 then stripPrefix plaintextPrefix config.raw44 else if hasPrefix plaintextNewlinePrefix config.raw45 then stripPrefix plaintextNewlinePrefix config.raw46 else throw "secret.part.data attribute only works for public plaintext secret parts, got ${config.raw}"47 );48 path = mkOptionDefault "/run/secrets/${secretName}/${config.hash}-${partName}";49 stablePath = mkOptionDefault "/run/secrets/${secretName}/${partName}";50 };51 });7 secretType = types.submodule ({ config, ... }: {52 secretType = types.submodule ({config, ...}: let8 config = let secretName = config._module.args.name; in {53 secretName = config._module.args.name;9 stableSecretPath = mkOptionDefault "/run/secrets/secret-stable-${secretName}";54 in {10 secretPath = mkOptionDefault "/run/secrets/secret-${config.secretHash}-${secretName}";11 secretHash = mkOptionDefault (if config.secret != null then (builtins.hashString "sha1" config.secret) else throw "secret is not defined for secret ${secretName}");1213 stablePublicPath = mkOptionDefault "/run/secrets/public-stable-${secretName}";14 publicPath = mkOptionDefault "/run/secrets/public-${config.publicHash}-${secretName}";15 publicHash = mkOptionDefault (if config.public != null then (builtins.hashString "sha1" config.public) else throw "public is not defined for secret ${secretName}");16 };55 freeformType = types.lazyAttrsOf (secretPartType secretName);17 options = with types; {56 options = with types; {18 shared = mkOption {57 shared = mkOption {19 description = "Is this secret owned by this machine, or propagated from shared secrets";58 description = "Is this secret owned by this machine, or propagated from shared secrets";20 default = false;59 default = false;21 };60 };2223 generator = mkOption {61 expectedOwners = mkOption {24 type = nullOr unspecified;62 type = nullOr unspecified;25 description = "Derivation to evaluate for secret generation";26 default = null;63 default = null;64 internal = true;27 };65 };286629 public = mkOption {30 type = nullOr str;31 description = "Secret public data";32 default = null;33 };34 secret = mkOption {67 generator = mkOption {35 type = nullOr str;68 type = nullOr unspecified;36 description = "Encrypted secret data";69 description = "Derivation to evaluate for secret generation";37 default = null;70 default = null;38 };71 };39 mode = mkOption {72 mode = mkOption {52 default = sysConfig.users.users.${config.owner}.group;85 default = sysConfig.users.users.${config.owner}.group;53 };86 };5455 secretHash = mkOption {56 type = str;57 description = "Hash of .secret field";58 };59 publicHash = mkOption {60 type = str;61 description = "Hash of .public field";62 };6364 stableSecretPath = mkOption {65 type = str;66 description = ''67 Use this, if target process supports re-reading of secret from disk,68 and doesn't needs to be restarted when secret is updated in file69 '';70 };71 secretPath = mkOption {72 type = str;73 description = "Path to decrypted secret, suffixed with contents hash";74 };7576 stablePublicPath = mkOption {77 type = str;78 description = ''79 Use this, if target process supports re-reading of secret from disk,80 and doesn't needs to be restarted when secret is updated in file81 '';82 };83 publicPath = mkOption {84 type = str;85 description = "Path to the public part of secret";86 };87 };87 };88 });88 });89 processPart = part: {90 inherit (part) raw path stablePath;91 };92 processSecret = secret:93 {94 inherit (secret) group mode owner;95 }96 // (mapAttrs (_: processPart) (removeAttrs secret [97 "shared"98 "generator"99 "mode"100 "group"101 "owner"102103 # FIXME: Some of those removed attributes shouldn't be here, but there is some error in passing shared secrets from fleet to nixos.104 "expectedOwners"105 ]));89 secretsFile = pkgs.writeTextFile {106 secretsFile = pkgs.writeTextFile {90 name = "secrets.json";107 name = "secrets.json";91 text = builtins.toJSON (mapAttrs (_: value: rec {108 text =92 inherit (value) group mode owner secret public;93 publicPath = if public != null then value.publicPath else "/missingno";94 stablePublicPath = if public != null then value.stablePublicPath else "/missingno";95 secretPath = if secret != null then value.secretPath else "/missingno";96 stableSecretPath = if secret != null then value.stableSecretPath else "/missingno";97 }) config.secrets);109 builtins.toJSON (mapAttrs (_: processSecret)110 config.secrets);98 };111 };99in112in {106 };118 };107 };119 };108 config = {120 config = {109 environment.systemPackages = with pkgs; [pkgs.fleet-install-secrets];121 environment.systemPackages = [pkgs.fleet-install-secrets];110 system.activationScripts.decryptSecrets = stringAfter [ "users" "groups" "specialfs" ] ''122 system.activationScripts.decryptSecrets = stringAfter ["users" "groups" "specialfs"] ''111 1>&2 echo "setting up secrets"123 1>&2 echo "setting up secrets"112 ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}124 ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}rustfmt.tomldiffbeforeafterboth1hard_tabs = true1hard_tabs = true2imports_granularity = "Crate"3group_imports = "StdExternalCrate"24