difftreelog
refactor nix secret module
in: trunk
17 files changed
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
members = ["crates/*", "cmds/*"]
resolver = "2"
+package.version = "0.1.0"
[workspace.dependencies]
nixlike = { path = "./crates/nixlike" }
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 owo_colors::OwoColorize;11use serde::Deserialize;12use std::{13 collections::{BTreeSet, HashSet},14 io::{self, Cursor, Read},15 path::PathBuf,16};17use tabled::{Table, Tabled};18use tokio::fs::read_to_string;19use tracing::{error, info, info_span, warn, Instrument};2021#[derive(Parser)]22pub enum Secret {23 /// Force load host keys for all defined hosts24 ForceKeys,25 /// Add secret, data should be provided in stdin26 AddShared {27 /// Secret name28 name: String,29 /// Secret owners30 machines: Vec<String>,31 /// Override secret if already present32 #[clap(long)]33 force: bool,34 /// Secret public part35 #[clap(long)]36 public: Option<String>,37 /// Load public part from specified file38 #[clap(long)]39 public_file: Option<PathBuf>,4041 /// Create a notification on secret expiration42 #[clap(long)]43 expires_at: Option<DateTime<Utc>>,4445 /// Secret with this name already exists, override its value while keeping the same owners.46 #[clap(long)]47 re_add: bool,48 },49 /// Add secret, data should be provided in stdin50 Add {51 /// Secret name52 name: String,53 /// Secret owners54 machine: String,55 /// Override secret if already present56 #[clap(long)]57 force: bool,58 #[clap(long)]59 public: Option<String>,60 #[clap(long)]61 public_file: Option<PathBuf>,62 },63 /// Read secret from remote host, requires sudo on said host64 Read {65 name: String,66 machine: String,67 #[clap(long)]68 plaintext: bool,69 },70 ReadPublic {71 name: String,72 machine: String,73 },74 UpdateShared {75 name: String,7677 #[clap(long)]78 machines: Option<Vec<String>>,7980 #[clap(long)]81 add_machines: Vec<String>,82 #[clap(long)]83 remove_machines: Vec<String>,8485 /// Which host should we use to decrypt86 #[clap(long)]87 prefer_identities: Vec<String>,88 },89 Regenerate {90 /// Which host should we use to decrypt, in case if reencryption is required, without91 /// regeneration92 #[clap(long)]93 prefer_identities: Vec<String>,94 },95 List {},96}9798#[tracing::instrument(skip(config, secret, field, prefer_identities))]99async fn update_owner_set(100 secret_name: &str,101 config: &Config,102 mut secret: FleetSharedSecret,103 field: Field,104 updated_set: &[String],105 prefer_identities: &[String],106) -> Result<FleetSharedSecret> {107 let original_set = secret.owners.clone();108109 let set = original_set.iter().collect::<BTreeSet<_>>();110 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();111112 if set == expected_set {113 info!("no need to update owner list, it is already correct");114 return Ok(secret);115 }116117 let should_regenerate = if set.difference(&expected_set).next().is_some() {118 // TODO: Remove this warning for revokable secrets.119 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");120 nix_go_json!(field.regenerateOnOwnerRemoved)121 } else if expected_set.difference(&set).next().is_some() {122 nix_go_json!(field.regenerateOnOwnerAdded)123 } else {124 false125 };126127 if should_regenerate {128 info!("secret is owner-dependent, will regenerate");129 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;130 Ok(generated)131 } else {132 let identity_holder = if !prefer_identities.is_empty() {133 prefer_identities134 .iter()135 .find(|i| original_set.iter().any(|s| s == *i))136 } else {137 secret.owners.first()138 };139 let Some(identity_holder) = identity_holder else {140 bail!("no available holder found");141 };142143 if let Some(data) = secret.secret.secret {144 let host = config.host(identity_holder).await?;145 let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;146 secret.secret.secret = Some(encrypted);147 }148149 secret.owners = updated_set.to_vec();150 Ok(secret)151 }152}153154#[derive(Deserialize)]155#[serde(rename_all = "camelCase")]156enum GeneratorKind {157 Impure,158 Pure,159}160161async fn generate_pure(162 _config: &Config,163 _display_name: &str,164 _secret: Field,165 _default_generator: Field,166 _owners: &[String],167) -> Result<FleetSecret> {168 bail!("pure generators are broken for now")169}170async fn generate_impure(171 config: &Config,172 _display_name: &str,173 secret: Field,174 default_generator: Field,175 owners: &[String],176) -> Result<FleetSecret> {177 let generator = nix_go!(secret.generator);178 let on: Option<String> = nix_go_json!(default_generator.impureOn);179180 let host = if let Some(on) = &on {181 config.host(on).await?182 } else {183 config.local_host()184 };185 let on_pkgs = host.pkgs().await?;186 let call_package = nix_go!(on_pkgs.callPackage);187 let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);188189 let mut recipients = Vec::new();190 for owner in owners {191 let key = config.key(owner).await?;192 recipients.push(key);193 }194 let encrypt = nix_go!(mk_encrypt_secret(Obj {195 recipients: { recipients },196 }));197198 let generator = nix_go!(call_package(generator)(Obj {199 encrypt,200 rustfmt_please_newline: { true },201 }));202203 let generator = generator.build().await?;204 let generator = generator205 .get("out")206 .ok_or_else(|| anyhow!("missing generateImpure out"))?;207 let generator = host.remote_derivation(generator).await?;208209 let out_parent = host.mktemp_dir().await?;210 let out = format!("{out_parent}/out");211212 let mut gen = host.cmd(generator).await?;213 gen.env("out", &out);214 if on.is_none() {215 // This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.216 let project_path: String = config217 .directory218 .clone()219 .into_os_string()220 .into_string()221 .map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;222 gen.env("FLEET_PROJECT", project_path);223 }224 gen.run().await.context("impure generator")?;225226 {227 let marker = host.read_file_text(format!("{out}/marker")).await?;228 ensure!(marker == "SUCCESS", "generation not succeeded");229 }230231 let public = host.read_file_text(format!("{out}/public")).await.ok();232 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();233 if let Some(secret) = &secret {234 ensure!(235 age::Decryptor::new(Cursor::new(&secret)).is_ok(),236 "builder produced non-encrypted value as secret, this is highly insecure, and not allowed."237 );238 }239240 let created_at = host.read_file_value(format!("{out}/created_at")).await?;241 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();242243 Ok(FleetSecret {244 created_at,245 expires_at,246 public,247 secret: secret.map(SecretData),248 })249}250async fn generate(251 config: &Config,252 display_name: &str,253 secret: Field,254 owners: &[String],255) -> Result<FleetSecret> {256 let generator = nix_go!(secret.generator);257 // Can't properly check on nix module system level258 {259 let gen_ty = generator.type_of().await?;260 if gen_ty == "null" {261 bail!("secret has no generator defined, can't automatically generate it.");262 }263 if gen_ty != "lambda" {264 bail!("generator should be lambda, got {gen_ty}");265 }266 }267 let default_pkgs = &config.default_pkgs;268 let default_call_package = nix_go!(default_pkgs.callPackage);269 // Generators provide additional information in passthru, to access270 // passthru we should call generator, but information about where this generator is supposed to build271 // is located in passthru... Thus evaluating generator on host.272 //273 // Maybe it is also possible to do some magic with __functor?274 //275 // I don't want to make modules always responsible for additional secret data anyway,276 // so it should be in derivation, and not in the secret data itself.277 let default_generator = nix_go!(default_call_package(generator)(Obj {}));278279 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);280281 match kind {282 GeneratorKind::Impure => {283 generate_impure(config, display_name, secret, default_generator, owners).await284 }285 GeneratorKind::Pure => {286 generate_pure(config, display_name, secret, default_generator, owners).await287 }288 }289}290async fn generate_shared(291 config: &Config,292 display_name: &str,293 secret: Field,294 expected_owners: Vec<String>,295) -> Result<FleetSharedSecret> {296 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);297 Ok(FleetSharedSecret {298 secret: generate(config, display_name, secret, &expected_owners).await?,299 owners: expected_owners,300 })301}302303async fn parse_public(304 public: Option<String>,305 public_file: Option<PathBuf>,306) -> Result<Option<String>> {307 Ok(match (public, public_file) {308 (Some(v), None) => Some(v),309 (None, Some(v)) => Some(read_to_string(v).await?),310 (Some(_), Some(_)) => {311 bail!("only public or public_file should be set")312 }313 (None, None) => None,314 })315}316317fn parse_machines(318 initial: Vec<String>,319 machines: Option<Vec<String>>,320 mut add_machines: Vec<String>,321 mut remove_machines: Vec<String>,322) -> Result<Vec<String>> {323 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {324 bail!("no operation");325 }326327 let initial_machines = initial.clone();328 let mut target_machines = initial;329 info!("Currently encrypted for {initial_machines:?}");330331 // ensure!(machines.is_some() || !add_machines.is_empty() || )332 if let Some(machines) = machines {333 ensure!(334 add_machines.is_empty() && remove_machines.is_empty(),335 "can't combine --machines and --add-machines/--remove-machines"336 );337 let target = initial_machines.iter().collect::<HashSet<_>>();338 let source = machines.iter().collect::<HashSet<_>>();339 for removed in target.difference(&source) {340 remove_machines.push((*removed).clone());341 }342 for added in source.difference(&target) {343 add_machines.push((*added).clone());344 }345 }346347 for machine in &remove_machines {348 let mut removed = false;349 while let Some(pos) = target_machines.iter().position(|m| m == machine) {350 target_machines.swap_remove(pos);351 removed = true;352 }353 if !removed {354 warn!("secret is not enabled for {machine}");355 }356 }357 for machine in &add_machines {358 if target_machines.iter().any(|m| m == machine) {359 warn!("secret is already added to {machine}");360 } else {361 target_machines.push(machine.to_owned());362 }363 }364 if !remove_machines.is_empty() {365 // TODO: maybe force secret regeneration?366 // Not that useful without revokation.367 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");368 }369 Ok(target_machines)370}371impl Secret {372 pub async fn run(self, config: &Config) -> Result<()> {373 match self {374 Secret::ForceKeys => {375 for host in config.list_hosts().await? {376 if config.should_skip(&host.name) {377 continue;378 }379 config.key(&host.name).await?;380 }381 }382 Secret::AddShared {383 mut machines,384 name,385 force,386 public,387 public_file,388 expires_at,389 re_add,390 } => {391 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).392393 let exists = config.has_shared(&name);394 if exists && !force && !re_add {395 bail!("secret already defined");396 }397 if re_add {398 // Fixme: use clap to limit this usage399 ensure!(!force, "--force and --readd are not compatible");400 ensure!(exists, "secret doesn't exists");401 ensure!(402 machines.is_empty(),403 "you can't use machines argument for --readd"404 );405 let shared = config.shared_secret(&name)?;406 machines = shared.owners;407 }408409 let recipients = config.recipients(machines.clone()).await?;410411 let secret = {412 let mut input = vec![];413 io::stdin().read_to_end(&mut input)?;414415 if input.is_empty() {416 None417 } else {418 Some(419 SecretData::encrypt(recipients, input)420 .ok_or_else(|| anyhow!("no recipients provided"))?,421 )422 }423 };424 let public = parse_public(public, public_file).await?;425 config.replace_shared(426 name,427 FleetSharedSecret {428 owners: machines,429 secret: FleetSecret {430 created_at: Utc::now(),431 expires_at,432 secret,433 public,434 },435 },436 );437 }438 Secret::Add {439 machine,440 name,441 force,442 public,443 public_file,444 } => {445 let recipient = config.recipient(&machine).await?;446447 let secret = {448 let mut input = vec![];449 io::stdin().read_to_end(&mut input)?;450 if input.is_empty() {451 bail!("no data provided")452 }453454 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))455 };456457 if config.has_secret(&machine, &name) && !force {458 bail!("secret already defined");459 }460 let public = parse_public(public, public_file).await?;461462 config.insert_secret(463 &machine,464 name,465 FleetSecret {466 created_at: Utc::now(),467 expires_at: None,468 secret,469 public,470 },471 );472 }473 #[allow(clippy::await_holding_refcell_ref)]474 Secret::Read {475 name,476 machine,477 plaintext,478 } => {479 let secret = config.host_secret(&machine, &name)?;480 let Some(secret) = secret.secret else {481 bail!("no secret {name}");482 };483 let host = config.host(&machine).await?;484 let data = host.decrypt(secret).await?;485 if plaintext {486 let s = String::from_utf8(data).context("output is not utf8")?;487 print!("{s}");488 } else {489 println!("{}", z85::encode(&data));490 }491 }492 Secret::ReadPublic { name, machine } => {493 let secret = config.host_secret(&machine, &name)?;494 let Some(public) = secret.public else {495 bail!("no secret {name}");496 };497 print!("{public}");498 }499 Secret::UpdateShared {500 name,501 machines,502 add_machines,503 remove_machines,504 prefer_identities,505 } => {506 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).507508 let secret = config.shared_secret(&name)?;509 if secret.secret.secret.is_none() {510 bail!("no secret");511 }512513 let initial_machines = secret.owners.clone();514 let target_machines = parse_machines(515 initial_machines.clone(),516 machines,517 add_machines,518 remove_machines,519 )?;520521 if target_machines.is_empty() {522 info!("no machines left for secret, removing it");523 config.remove_shared(&name);524 return Ok(());525 }526527 let config_field = &config.config_unchecked_field;528 let field = nix_go!(config_field.sharedSecrets[{ name }]);529530 let updated = update_owner_set(531 &name,532 config,533 secret,534 field,535 &target_machines,536 &prefer_identities,537 )538 .await?;539 config.replace_shared(name, updated);540 }541 Secret::Regenerate { prefer_identities } => {542 info!("checking for secrets to regenerate");543 {544 let _span = info_span!("shared").entered();545 let expected_shared_set = config546 .list_configured_shared()547 .await?548 .into_iter()549 .collect::<HashSet<_>>();550 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();551 for missing in expected_shared_set.difference(&shared_set) {552 let config_field = &config.config_unchecked_field;553 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);554 let expected_owners: Option<Vec<String>> =555 nix_go_json!(secret.expectedOwners);556 let Some(expected_owners) = expected_owners else {557 // TODO: Might still need to regenerate558 continue;559 };560 info!("generating secret: {missing}");561 let shared = generate_shared(config, missing, secret, expected_owners)562 .in_current_span()563 .await?;564 config.replace_shared(missing.to_string(), shared)565 }566 }567 for host in config.list_hosts().await? {568 let _span = info_span!("host", host = host.name).entered();569 let expected_set = host570 .list_configured_secrets()571 .in_current_span()572 .await?573 .into_iter()574 .collect::<HashSet<_>>();575 let stored_set = config576 .list_secrets(&host.name)577 .into_iter()578 .collect::<HashSet<_>>();579 for missing in expected_set.difference(&stored_set) {580 info!("generating secret: {missing}");581 let secret = host.secret_field(missing).in_current_span().await?;582 let generated =583 match generate(config, missing, secret, &[host.name.clone()])584 .in_current_span()585 .await586 {587 Ok(v) => v,588 Err(e) => {589 error!("{e}");590 continue;591 }592 };593 config.insert_secret(&host.name, missing.to_string(), generated)594 }595 }596 let mut to_remove = Vec::new();597 for name in &config.list_shared() {598 info!("updating secret: {name}");599 let data = config.shared_secret(name)?;600 let config_field = &config.config_unchecked_field;601 let expected_owners: Vec<String> =602 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);603 if expected_owners.is_empty() {604 warn!("secret was removed from fleet config: {name}, removing from data");605 to_remove.push(name.to_string());606 continue;607 }608609 let secret = nix_go!(config_field.sharedSecrets[{ name }]);610 config.replace_shared(611 name.to_owned(),612 update_owner_set(613 name,614 config,615 data,616 secret,617 &expected_owners,618 &prefer_identities,619 )620 .await?,621 );622 }623 for k in to_remove {624 config.remove_shared(&k);625 }626 }627 Secret::List {} => {628 let _span = info_span!("loading secrets").entered();629 let configured = config.list_configured_shared().await?;630 #[derive(Tabled)]631 struct SecretDisplay {632 #[tabled(rename = "Name")]633 name: String,634 #[tabled(rename = "Owners")]635 owners: String,636 }637 let mut table = vec![];638 for name in configured.iter().cloned() {639 let config = config.clone();640 let expected_owners = config.shared_secret_expected_owners(&name).await?;641 let data = config.shared_secret(&name)?;642 let owners = data643 .owners644 .iter()645 .map(|o| {646 if expected_owners.contains(o) {647 o.green().to_string()648 } else {649 o.red().to_string()650 }651 })652 .collect::<Vec<_>>();653 table.push(SecretDisplay {654 owners: owners.join(", "),655 name,656 })657 }658 info!("loaded\n{}", Table::new(table).to_string())659 }660 }661 Ok(())662 }663}cmds/fleet/src/host.rsdiffbeforeafterboth--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -54,7 +54,7 @@
pub local: bool,
pub session: OnceLock<Arc<openssh::Session>>,
- pub nixos_config: Field,
+ pub nixos_config: Option<Field>,
}
impl ConfigHost {
async fn open_session(&self) -> Result<Arc<openssh::Session>> {
@@ -169,7 +169,9 @@
}
pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {
- let nixos = &self.nixos_config;
+ let Some(nixos) = &self.nixos_config else {
+ return Ok(vec![]);
+ };
let secrets = nix_go!(nixos.secrets);
let mut out = Vec::new();
for name in secrets.list_fields().await? {
@@ -183,9 +185,19 @@
Ok(out)
}
pub async fn secret_field(&self, name: &str) -> Result<Field> {
- let nixos = &self.nixos_config;
+ let Some(nixos) = &self.nixos_config else {
+ bail!("host is virtual and has no secrets");
+ };
Ok(nix_go!(nixos.secrets[{ name }]))
}
+
+ /// Packages for this host, resolved with nixpkgs overlays
+ pub async fn pkgs(&self) -> Result<Field> {
+ let Some(nixos) = &self.nixos_config else {
+ return Ok(self.config.default_pkgs.clone());
+ };
+ Ok(nix_go!(nixos.nixpkgs.resolvedPkgs))
+ }
}
impl Config {
@@ -202,6 +214,16 @@
self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)
}
+ pub fn local_host(&self) -> ConfigHost {
+ ConfigHost {
+ config: self.clone(),
+ name: "<virtual localhost>".to_owned(),
+ local: true,
+ session: OnceLock::new(),
+ nixos_config: None,
+ }
+ }
+
pub async fn host(&self, name: &str) -> Result<ConfigHost> {
let config = &self.config_unchecked_field;
let nixos_config = nix_go!(config.hosts[{ name }].nixosSystem.config);
@@ -210,7 +232,7 @@
name: name.to_owned(),
local: self.is_local(name),
session: OnceLock::new(),
- nixos_config,
+ nixos_config: Some(nixos_config),
})
}
pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -11,7 +11,6 @@
mod fleetdata;
-use std::time::Duration;
use std::{ffi::OsString, process::ExitCode};
use anyhow::{bail, Result};
@@ -158,7 +157,7 @@
let reg = tracing_subscriber::registry().with({
let sub = tracing_subscriber::fmt::layer()
.without_time()
- .with_target(true);
+ .with_target(false);
#[cfg(feature = "indicatif")]
let sub = sub.with_writer(indicatif_layer.get_stdout_writer());
sub.with_filter(filter) // .withou,
cmds/install-secrets/src/main.rsdiffbeforeafterboth--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -13,7 +13,7 @@
use std::path::Path;
use std::str::{from_utf8, FromStr};
use std::{collections::HashMap, path::PathBuf};
-use tracing::{error, info, warn};
+use tracing::{error, info, info_span, warn};
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::EnvFilter;
@@ -213,12 +213,9 @@
let mut failed = false;
for (name, value) in data {
- info!("initializing secret {name}");
+ let _span = info_span!("init", name = name);
if let Err(e) = init_secret(&identity, value) {
- error!(
- "{:?}",
- e.context(format!("failed to initialize secret {}", name))
- );
+ error!("{e}");
failed = true;
}
}
@@ -237,6 +234,7 @@
.from_env_lossy(),
)
.without_time()
+ .with_target(false)
.init();
let opts = Opts::parse();
crates/better-command/src/handler.rsdiffbeforeafterboth--- a/crates/better-command/src/handler.rs
+++ b/crates/better-command/src/handler.rs
@@ -274,7 +274,10 @@
#[cfg(feature = "indicatif")]
span.pb_set_message(&process_message(s.trim()));
#[cfg(not(feature = "indicatif"))]
- info!("{}", process_message(s));
+ {
+ let _span = span.enter();
+ info!("{}", process_message(s));
+ }
} else {
warn!("bad fields: {fields:?}");
}
crates/better-command/src/lib.rsdiffbeforeafterboth--- a/crates/better-command/src/lib.rs
+++ b/crates/better-command/src/lib.rs
@@ -1,5 +1,5 @@
mod handler;
-pub use handler::{Handler, PlainHandler, NoopHandler, NixHandler, ClonableHandler};
+pub use handler::{ClonableHandler, Handler, NixHandler, NoopHandler, PlainHandler};
pub fn add(left: usize, right: usize) -> usize {
left + right
flake.lockdiffbeforeafterboth--- a/flake.lock
+++ b/flake.lock
@@ -1,26 +1,28 @@
{
"nodes": {
- "flake-utils": {
+ "crane": {
"inputs": {
- "systems": "systems"
+ "nixpkgs": [
+ "nixpkgs"
+ ]
},
"locked": {
- "lastModified": 1705309234,
- "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
- "owner": "numtide",
- "repo": "flake-utils",
- "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+ "lastModified": 1712681629,
+ "narHash": "sha256-bMDXn4AkTXLCpoZbII6pDGoSeSe9gI87jxPsHRXgu/E=",
+ "owner": "ipetkov",
+ "repo": "crane",
+ "rev": "220387ac8e99cbee0ca4c95b621c4bc782b6a235",
"type": "github"
},
"original": {
- "owner": "numtide",
- "repo": "flake-utils",
+ "owner": "ipetkov",
+ "repo": "crane",
"type": "github"
}
},
- "flake-utils_2": {
+ "flake-utils": {
"inputs": {
- "systems": "systems_2"
+ "systems": "systems"
},
"locked": {
"lastModified": 1705309234,
@@ -54,6 +56,7 @@
},
"root": {
"inputs": {
+ "crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
@@ -61,7 +64,9 @@
},
"rust-overlay": {
"inputs": {
- "flake-utils": "flake-utils_2",
+ "flake-utils": [
+ "flake-utils"
+ ],
"nixpkgs": [
"nixpkgs"
]
@@ -81,21 +86,6 @@
}
},
"systems": {
- "locked": {
- "lastModified": 1681028828,
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
- "owner": "nix-systems",
- "repo": "default",
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
- "type": "github"
- },
- "original": {
- "owner": "nix-systems",
- "repo": "default",
- "type": "github"
- }
- },
- "systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
flake.nixdiffbeforeafterboth--- a/flake.nix
+++ b/flake.nix
@@ -5,15 +5,23 @@
nixpkgs.url = "github:nixos/nixpkgs/master";
rust-overlay = {
url = "github:oxalica/rust-overlay";
+ inputs = {
+ nixpkgs.follows = "nixpkgs";
+ flake-utils.follows = "flake-utils";
+ };
+ };
+ flake-utils.url = "github:numtide/flake-utils";
+ crane = {
+ url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
- flake-utils = {url = "github:numtide/flake-utils";};
};
outputs = {
self,
rust-overlay,
flake-utils,
nixpkgs,
+ crane,
}:
with nixpkgs.lib;
{
@@ -26,20 +34,16 @@
inherit system;
overlays = [(import rust-overlay)];
};
- llvmPkgs = pkgs.buildPackages.llvmPackages_11;
- rust =
- (pkgs.rustChannelOf {
- date = "2024-02-10";
- channel = "nightly";
- })
- .default
- .override {extensions = ["rust-src" "rust-analyzer"];};
+ rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
+ craneLib = (crane.mkLib pkgs).overrideToolchain rust;
in {
- packages = (import ./pkgs) pkgs pkgs;
- devShell = (pkgs.mkShell.override {stdenv = llvmPkgs.stdenv;}) {
+ packages = import ./pkgs {
+ inherit (pkgs) callPackage;
+ inherit craneLib;
+ };
+ devShell = craneLib.devShell {
nativeBuildInputs = with pkgs; [
alejandra
- rust
lld
cargo-edit
cargo-udeps
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -10,11 +10,14 @@
hosts,
modules,
globalModules ? [],
+ extraFleetLib ? {},
}: let
hostNames = nixpkgs.lib.attrNames hosts;
- fleetLib = import ./fleetLib.nix {
- inherit nixpkgs hostNames;
- };
+ fleetLib =
+ (import ./fleetLib.nix {
+ inherit nixpkgs hostNames;
+ })
+ // extraFleetLib;
in let
root = nixpkgs.lib.evalModules {
modules =
lib/fleetLib.nixdiffbeforeafterboth--- a/lib/fleetLib.nix
+++ b/lib/fleetLib.nix
@@ -39,4 +39,32 @@
mkFleetDefault = mkOverride 999;
# Some generators use mkDefault, but optionDefault is set by nixpkgs.
mkFleetGeneratorDefault = mkOverride 1001;
+
+ mkPassword = {size ? 32}: {
+ coreutils,
+ encrypt,
+ mkSecretGenerator,
+ }:
+ mkSecretGenerator {
+ script = ''
+ ${coreutils}/bin/tr -dc 'A-Za-z0-9!?%=' < /dev/random \
+ | ${coreutils}/bin/head -c ${toString size} \
+ | ${encrypt} > $out/secret
+ '';
+ };
+
+ mkRsa = {size ? 4096}: {
+ openssl,
+ encrypt,
+ mkSecretGenerator,
+ }:
+ mkSecretGenerator {
+ script = ''
+ ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
+ ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
+
+ sudo cat rsa_private.key | ${encrypt} > $out/secret
+ sudo cat rsa_public.key > $out/public
+ '';
+ };
}
modules/fleet/secrets.nixdiffbeforeafterboth--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -8,6 +8,13 @@
with fleetLib; let
sharedSecret = with types; ({config, ...}: {
options = {
+ managed = mkOption {
+ type = bool;
+ description = ''
+ Is this secret managed by configuration (I.e will work with reencrypt/etc), or it is configured by user
+ '';
+ };
+
expectedOwners = mkOption {
type = nullOr (listOf str);
description = ''
@@ -146,77 +153,81 @@
overlays = [
(final: prev: let
lib = final.lib;
+ inherit (lib) strings;
+ inherit (strings) escapeShellArgs;
in {
- mkPassword = {size ? 32}:
- final.mkSecretGenerator ''
- ${final.coreutils}/bin/tr -dc 'A-Za-z0-9!?%=' < /dev/random \
- | ${final.coreutils}/bin/head -c ${toString size} \
- | encrypt > $out/secret
- '';
- mkRsa = {size ? 4096}:
- final.mkSecretGenerator ''
- ${final.openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
- ${final.openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
-
- sudo cat rsa_private.key | encrypt > $out/secret
- sudo cat rsa_public.key > $out/public
+ mkEncryptSecret = {
+ rage ? prev.rage,
+ recipients,
+ }:
+ prev.writeShellScript "encryptor" ''
+ #!/bin/sh
+ exec ${rage}/bin/rage ${escapeShellArgs recipients} -e "$@"
'';
# TODO: Move to fleet
# TODO: Merge both generators to one with consistent options syntax?
# Impure generator is built on local machine, then built closure is copied to remote machine,
# and then it is ran in inpure context, so that this generator may access HSMs and other things.
- mkImpureSecretGenerator = generatorText: machine:
+ mkImpureSecretGenerator = {
+ script,
+ # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
+ # (Some secrets-encryption-in-git/managed PKI solution is expected)
+ impureOn ? null,
+ }:
(prev.writeShellScript "impureGenerator.sh" ''
#!/bin/sh
set -eu
-
- # TODO: Provide encryption function as script passed to `callPackage generator {encrypt = ...;}`
- function encrypt() {
- eval ${final.rage}/bin/rage $rageArgs
- }
+ cd /var/empty
created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
- echo -n $created_at > $out/created_at
- ${generatorText}
+ ${script}
+ if ! test -d $out; then
+ echo "impure generator script did not produce expected \$out output"
+ exit 1
+ fi
+
+ echo -n $created_at > $out/created_at
echo -n SUCCESS > $out/marker
'')
.overrideAttrs (old: {
passthru = {
+ inherit impureOn;
generatorKind = "impure";
- impureOn = machine;
};
});
+ # Pure generators are disabled for now
+ mkSecretGenerator = {script}: final.mkImpureSecretGenerator {inherit script;};
+
# TODO: Implement consistent naming
# Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...
# But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.
- mkSecretGenerator = generatorText:
- (prev.writeShellScript "generator.sh" ''
- #!/bin/sh
- set -eu
- # TODO: User should create output directory by themselves.
- cd $out
-
- # TODO: Provide encryption function as script passed to `callPackage generator {encrypt = ...;}`
- function encrypt() {
- eval ${final.rage}/bin/rage $rageArgs
- }
-
- created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
- echo -n $created_at > $out/created_at
-
- ${generatorText}
-
- echo -n SUCCESS > $out/marker
- '')
- .overrideAttrs (old: {
- passthru = {
- generatorKind = "pure";
- };
- # TODO: make nix daemon build secret, not just the script.
- # __impure = true;
- });
+ # mkSecretGenerator = {script}:
+ # (prev.writeShellScript "generator.sh" ''
+ # #!/bin/sh
+ # set -eu
+ # # TODO: make nix daemon build secret, not just the script.
+ # cd /var/empty
+ #
+ # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
+ #
+ # ${script}
+ # if ! test -d $out; then
+ # echo "impure generator script did not produce expected \$out output"
+ # exit 1
+ # fi
+ #
+ # echo -n $created_at > $out/created_at
+ # echo -n SUCCESS > $out/marker
+ # '')
+ # .overrideAttrs (old: {
+ # passthru = {
+ # generatorKind = "pure";
+ # };
+ # # TODO: make nix daemon build secret, not just the script.
+ # # __impure = true;
+ # });
})
];
};
nixos/fleetPkgs.nixdiffbeforeafterboth--- a/nixos/fleetPkgs.nix
+++ b/nixos/fleetPkgs.nix
@@ -1,3 +1,24 @@
-{ ... }: {
- nixpkgs.overlays = [ (import ../pkgs) ];
+{...}: {
+ nixpkgs.overlays = [
+ # Not using craneLib here, because we don't want to have two different rust versions for some platforms.
+ (final: prev: {
+ fleet-install-secrets = prev.callPackage ({rustPlatform}:
+ rustPlatform.buildRustPackage rec {
+ pname = "fleet-install-secrets";
+ name = "${pname}";
+
+ src = ../.;
+ strictDeps = true;
+
+ buildAndTestSubdir = "cmds/install-secrets";
+
+ cargoLock = {
+ lockFile = ../Cargo.lock;
+ outputHashes = {
+ "alejandra-3.0.0" = "sha256-lStDIPizbJipd1JpNKX1olBKzyIosyC2U/mVFwJPcZE=";
+ };
+ };
+ }) {};
+ })
+ ];
}
pkgs/default.nixdiffbeforeafterboth--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,6 +1,9 @@
-pkgs: super:
-with pkgs;
{
- fleet-install-secrets = callPackage ./fleet-install-secrets.nix { };
- fleet = callPackage ./fleet.nix { };
+ callPackage,
+ craneLib,
+}: rec {
+ default = fleet;
+
+ fleet-install-secrets = callPackage ./fleet-install-secrets.nix {inherit craneLib;};
+ fleet = callPackage ./fleet.nix {inherit craneLib;};
}
pkgs/fleet-install-secrets.nixdiffbeforeafterboth--- a/pkgs/fleet-install-secrets.nix
+++ b/pkgs/fleet-install-secrets.nix
@@ -1,16 +1,9 @@
-{ rustPlatform, lib }:
-
-rustPlatform.buildRustPackage rec {
+{craneLib}:
+craneLib.buildPackage rec {
pname = "fleet-install-secrets";
- version = "0.0.1";
- name = "${pname}-${version}";
- src = ../.;
- buildAndTestSubdir = "cmds/install-secrets";
- cargoLock = {
- lockFile = ../Cargo.lock;
- outputHashes = {
- "alejandra-3.0.0" = "sha256-lStDIPizbJipd1JpNKX1olBKzyIosyC2U/mVFwJPcZE=";
- };
- };
+ src = craneLib.cleanCargoSource (craneLib.path ../.);
+ strictDeps = true;
+
+ cargoExtraArgs = "--locked -p ${pname}";
}
pkgs/fleet.nixdiffbeforeafterboth--- a/pkgs/fleet.nix
+++ b/pkgs/fleet.nix
@@ -1,16 +1,9 @@
-{ rustPlatform }:
-
-rustPlatform.buildRustPackage rec {
+{craneLib}:
+craneLib.buildPackage rec {
pname = "fleet";
- version = "0.0.1";
- name = "${pname}-${version}";
- src = ../.;
- cargoBuildFlags = "-p ${pname}";
- cargoLock = {
- lockFile = ../Cargo.lock;
- outputHashes = {
- "alejandra-3.0.0" = "sha256-YSdHsJ73G7TEFzbmpZ2peuMefIa9/vNB2g+xdiyma3U=";
- };
- };
+ src = craneLib.cleanCargoSource (craneLib.path ../.);
+ strictDeps = true;
+
+ cargoExtraArgs = "--locked -p ${pname}";
}
rust-toolchain.tomldiffbeforeafterboth--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "nightly-2024-02-10"
+components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]