difftreelog
feat use builtin for getting secret
in: trunk
12 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -723,15 +723,6 @@
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
-name = "convert_case"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
-dependencies = [
- "unicode-segmentation",
-]
-
-[[package]]
name = "cookie-factory"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -765,34 +756,6 @@
]
[[package]]
-name = "crossterm"
-version = "0.29.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
-dependencies = [
- "bitflags",
- "crossterm_winapi",
- "derive_more",
- "document-features",
- "filedescriptor",
- "mio",
- "parking_lot",
- "rustix 1.1.2",
- "signal-hook",
- "signal-hook-mio",
- "winapi",
-]
-
-[[package]]
-name = "crossterm_winapi"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
-dependencies = [
- "winapi",
-]
-
-[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -932,27 +895,6 @@
dependencies = [
"powerfmt",
"serde_core",
-]
-
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "convert_case",
- "proc-macro2",
- "quote",
- "syn",
]
[[package]]
@@ -976,15 +918,6 @@
"proc-macro2",
"quote",
"syn",
-]
-
-[[package]]
-name = "document-features"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
-dependencies = [
- "litrs",
]
[[package]]
@@ -1073,17 +1006,6 @@
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
-name = "filedescriptor"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
-dependencies = [
- "libc",
- "thiserror 1.0.69",
- "winapi",
-]
-
-[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1128,7 +1050,6 @@
"chrono",
"clap",
"clap_complete",
- "crossterm",
"fleet-base",
"fleet-shared",
"futures",
@@ -1142,7 +1063,6 @@
"openssh",
"opentelemetry",
"opentelemetry_sdk",
- "owo-colors",
"peg",
"regex",
"serde",
@@ -1503,12 +1423,6 @@
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
-name = "hermit-abi"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
-
-[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1961,24 +1875,7 @@
dependencies = [
"memchr",
"serde",
-]
-
-[[package]]
-name = "is-terminal"
-version = "0.4.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
-dependencies = [
- "hermit-abi",
- "libc",
- "windows-sys 0.59.0",
]
-
-[[package]]
-name = "is_ci"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "is_terminal_polyfill"
@@ -2083,12 +1980,6 @@
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
-
-[[package]]
-name = "litrs"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "lock_api"
@@ -2161,7 +2052,6 @@
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
- "log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -2428,16 +2318,6 @@
]
[[package]]
-name = "owo-colors"
-version = "4.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
-dependencies = [
- "supports-color 2.1.0",
- "supports-color 3.0.2",
-]
-
-[[package]]
name = "papergrid"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3335,27 +3215,6 @@
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "signal-hook"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
-dependencies = [
- "libc",
- "signal-hook-registry",
-]
-
-[[package]]
-name = "signal-hook-mio"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
-dependencies = [
- "libc",
- "mio",
- "signal-hook",
-]
[[package]]
name = "signal-hook-registry"
@@ -3449,25 +3308,6 @@
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
-name = "supports-color"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
-dependencies = [
- "is-terminal",
- "is_ci",
-]
-
-[[package]]
-name = "supports-color"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
-dependencies = [
- "is_ci",
-]
-
-[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4170,12 +4010,6 @@
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
-
-[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4448,25 +4282,9 @@
"home",
"once_cell",
"rustix 0.38.44",
-]
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
]
[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4474,12 +4292,6 @@
dependencies = [
"windows-sys 0.61.2",
]
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
README.adocdiffbeforeafterboth--- a/README.adoc
+++ b/README.adoc
@@ -211,7 +211,7 @@
];
# And finally, I have secrets, which are shared between machines.
# Note that this example is somewhat wrong, as this goes not into the machine configuration, but to fleet configuration.
- sharedSecrets = {
+ secrets = {
"ca.pem" = {
# This is just the public key, no need to regenerate it to change owner list
regenerateOnOwnerAdded = false;
cmds/fleet/Cargo.tomldiffbeforeafterboth--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -28,12 +28,10 @@
async-trait = "0.1"
base64 = "0.22.1"
chrono = { version = "0.4", features = ["serde"] }
-crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
futures = "0.3"
hostname = "0.4.1"
itertools = "0.14"
openssh = "0.11"
-owo-colors = { version = "4.2", features = ["supports-color", "supports-colors"] }
peg = "0.8"
regex = "1.11"
shlex = "1.3"
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -11,11 +11,10 @@
fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},
host::Config,
opts::FleetOpts,
- secret::{Expectations, RegenerationReason, SharedSecretDefinition, secret_needs_regeneration},
+ secret::{Expectations, RegenerationReason, secret_needs_regeneration},
};
use fleet_shared::SecretData;
use nix_eval::{NixType, Value, nix_go, nix_go_json};
-use owo_colors::OwoColorize;
use serde::Deserialize;
use tabled::{Table, Tabled};
use tokio::{fs::read, task::spawn_blocking};
@@ -69,6 +68,7 @@
},
}
+/*
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(config, secret, definition, prefer_identities))]
async fn maybe_regenerate_shared_secret(
@@ -143,6 +143,7 @@
Ok(secret)
}
}
+*/
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -314,6 +315,7 @@
}
}
}
+/*
async fn generate_shared(
config: &Config,
display_name: &str,
@@ -332,7 +334,7 @@
.await?,
owners: expectations.owners.clone(),
})
-}
+}*/
async fn parse_public(
public: Option<String>,
@@ -625,10 +627,11 @@
#[tabled(rename = "Owners")]
owners: String,
}
- let mut table = vec![];
+ // let mut table = vec![];
for name in configured.iter().cloned() {
let config = config.clone();
let data = config.shared_secret(&name).expect("exists");
+ /*
let definition = config.shared_secret_definition(&name)?;
let expectations = definition.expectations()?;
let owners = data
@@ -645,8 +648,9 @@
owners: owners.join(", "),
name,
})
+*/
}
- info!("loaded\n{}", Table::new(table).to_string())
+ // info!("loaded\n{}", Table::new(table).to_string())
}
Secret::Edit {
name,
crates/fleet-base/src/host.rsdiffbeforeafterboth1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};16use openssh::{ControlPersist, SessionBuilder};17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24 command::MyCommand,25 fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},26 secret::{HostSecretDefinition, SharedSecretDefinition},27};2829pub struct FleetConfigInternals {30 /// Fleet project directory, containing fleet.nix file.31 pub directory: PathBuf,32 /// builtins.currentSystem33 pub local_system: String,34 pub data: Mutex<FleetData>,35 pub nix_args: Vec<OsString>,36 /// fleet_config.config37 pub config_field: Value,38 /// flake.output39 pub flake_outputs: Value,40 // TODO: Remove with connectivity refactor41 pub localhost: String,4243 /// import nixpkgs {system = local};44 pub default_pkgs: Value,45 /// inputs.nixpkgs46 pub nixpkgs: Value,47}4849// TODO: Make field not pub50#[derive(Clone)]51pub struct Config(pub Arc<FleetConfigInternals>);5253impl Deref for Config {54 type Target = FleetConfigInternals;5556 fn deref(&self) -> &Self::Target {57 &self.058 }59}6061#[derive(Clone, Copy, Debug)]62pub enum EscalationStrategy {63 Sudo,64 Run0,65 Su,66}6768#[derive(Clone, PartialEq, Copy, Debug)]69pub enum DeployKind {70 /// NixOS => NixOS managed by fleet71 UpgradeToFleet,72 /// NixOS managed by fleet => NixOS managed by fleet73 Fleet,74 /// Remote host has /mnt, /mnt/boot mounted,75 /// generated config is added to fleet configuration.76 NixosInstall,77 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),78 /// generated config is added to fleet configuration,79 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.80 NixosLustrate,81}8283impl FromStr for DeployKind {84 type Err = anyhow::Error;85 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {86 match s {87 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),88 "fleet" => Ok(Self::Fleet),89 "nixos-install" => Ok(Self::NixosInstall),90 "nixos-lustrate" => Ok(Self::NixosLustrate),91 v => bail!(92 "unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""93 ),94 }95 }96}97pub struct ConfigHost {98 config: Config,99 pub name: String,100 groups: OnceCell<Vec<String>>,101102 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it103 deploy_kind: OnceCell<DeployKind>,104 session_destination: OnceCell<String>,105 legacy_ssh_store: OnceCell<bool>,106107 pub host_config: Option<Value>,108 pub nixos_config: OnceCell<Value>,109 pub nixos_unchecked_config: OnceCell<Value>,110 pub pkgs_override: Option<Value>,111112 // TODO: Move command helpers away with connectivity refactor113 pub local: bool,114 pub session: OnceLock<Arc<openssh::Session>>,115}116117#[derive(Debug, Clone, Copy)]118pub enum GenerationStorage {119 Deployer,120 Machine,121 Pusher,122}123impl GenerationStorage {124 fn prefix(&self) -> &'static str {125 match self {126 GenerationStorage::Deployer => "deployer.",127 GenerationStorage::Machine => "",128 GenerationStorage::Pusher => "pusher.",129 }130 }131}132133#[derive(Tabled, Debug)]134pub struct Generation {135 #[tabled(rename = "ID", format("{}", self.rollback_id()))]136 pub id: u32,137 #[tabled(rename = "Current")]138 pub current: bool,139 #[tabled(rename = "Created at")]140 pub datetime: UtcDateTime,141 #[tabled(format = "{:?}")]142 pub store_path: PathBuf,143 #[tabled(skip)]144 pub location: GenerationStorage,145}146impl Generation {147 pub fn rollback_id(&self) -> String {148 format!("{}{}", self.location.prefix(), self.id)149 }150}151152fn parse_generation_line(g: &str) -> Option<Generation> {153 let mut parts = g.split_whitespace();154 let id = parts.next()?;155 let id: u32 = id.parse().ok()?;156 let date = parts.next()?;157 let time = parts.next()?;158 let current = if let Some(current) = parts.next() {159 if current == "(current)" {160 Some(true)161 } else {162 None163 }164 } else {165 Some(false)166 };167 let current = current?;168 if parts.next().is_some() {169 warn!("unexpected text after generation: {g}");170 }171172 let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")173 .expect("valid format");174 let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;175176 Some(Generation {177 id,178 current,179 datetime,180 store_path: PathBuf::new(),181 location: GenerationStorage::Machine,182 })183}184// TODO: Move command helpers away with connectivity refactor185impl ConfigHost {186 pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {187 let mut cmd = self.cmd("nix-env").await?;188 cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))189 .arg("--list-generations")190 .env("TZ", "UTC");191 // Sudo is required because --list-generations tries to acquire profile lock192 let data = cmd.sudo().run_string().await?;193 let mut generations = data194 .split('\n')195 .map(|e| e.trim())196 .filter(|&l| !l.is_empty())197 .filter_map(|g| {198 let generation = parse_generation_line(g);199 if generation.is_none() {200 warn!("bad generation: {g}");201 };202 generation203 })204 .collect::<Vec<_>>();205 for ele in generations.iter_mut() {206 let mut cmd = self.cmd("readlink").await?;207 cmd.arg("--")208 .arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));209 let path = cmd.run_string().await?;210 ele.store_path = PathBuf::from(path.trim_end_matches("\n"));211 }212213 Ok(generations)214 }215216 pub fn set_session_destination(&self, dest: String) {217 self.session_destination218 .set(dest)219 .expect("session destination is already set")220 }221 pub fn set_deploy_kind(&self, kind: DeployKind) {222 self.deploy_kind223 .set(kind)224 .expect("deploy kind is already set");225 }226 pub fn set_legacy_ssh_store(&self, legacy: bool) {227 self.legacy_ssh_store228 .set(legacy)229 .expect("legacy ssh store is already set")230 }231 pub async fn deploy_kind(&self) -> Result<DeployKind> {232 if let Some(kind) = self.deploy_kind.get() {233 return Ok(*kind);234 }235 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {236 Ok(v) => v,237 Err(e) => {238 bail!("failed to query remote system kind: {e}");239 }240 };241 if !is_fleet_managed {242 bail!(243 "{}",244 indoc::indoc! {"245 host is not marked as managed by fleet246 if you're not trying to lustrate/install system from scratch,247 you should either248 1. manually create /etc/FLEET_HOST file on the target host,249 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet250 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos251 "}252 );253 }254 // TOCTOU is possible255 let _ = self.deploy_kind.set(DeployKind::Fleet);256 Ok(*self.deploy_kind.get().expect("deploy kind is just set"))257 }258 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {259 // Prefer sudo, as run0 has some gotchas with polkit260 // and too many repeating prompts.261 if (self.find_in_path("sudo").await).is_ok() {262 return Ok(EscalationStrategy::Sudo);263 }264 if (self.find_in_path("run0").await).is_ok() {265 return Ok(EscalationStrategy::Run0);266 }267 Ok(EscalationStrategy::Su)268 }269 async fn open_session(&self) -> Result<Arc<openssh::Session>> {270 assert!(!self.local, "do not open ssh connection to local session");271 // FIXME: TOCTOU272 if let Some(session) = &self.session.get() {273 return Ok((*session).clone());274 };275 let mut session = SessionBuilder::default();276 session.control_persist(ControlPersist::ClosedAfterInitialConnection);277278 let dest = self.session_destination.get().unwrap_or(&self.name);279 let session = session280 .connect(&dest)281 .await282 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;283 let session = Arc::new(session);284 self.session.set(session.clone()).expect("TOCTOU happened");285 Ok(session)286 }287 pub async fn mktemp_dir(&self) -> Result<String> {288 let mut cmd = self.cmd("mktemp").await?;289 cmd.arg("-d");290 let path = cmd.run_string().await?;291 Ok(path.trim_end().to_owned())292 }293 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {294 let mut cmd = self.cmd("sh").await?;295 cmd.arg("-c")296 .arg("test -e \"$1\" && echo true || echo false")297 .arg("_")298 .arg(path);299 cmd.run_value().await300 }301 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {302 let mut cmd = self.cmd("cat").await?;303 cmd.arg(path);304 cmd.run_bytes().await305 }306 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {307 let mut cmd = self.cmd("cat").await?;308 cmd.arg(path);309 cmd.run_string().await310 }311 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {312 let mut cmd = self.cmd("ls").await?;313 cmd.arg(path);314 let out = cmd.run_string().await?;315 let mut lines = out.split('\n');316 if let Some(last) = lines.next_back() {317 ensure!(last.is_empty(), "output of ls should end with newline");318 }319 Ok(lines.map(ToOwned::to_owned).collect())320 }321 #[allow(dead_code)]322 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {323 let text = self.read_file_text(path).await?;324 Ok(serde_json::from_str(&text)?)325 }326 pub async fn read_env(&self, env: &str) -> Result<String> {327 let mut cmd = self.cmd("printenv").await?;328 cmd.arg(env);329 cmd.run_string().await330 }331 pub async fn find_in_path(&self, command: &str) -> Result<String> {332 // // `which` is not a part of coreutils, and it might not exist on machine.333 // let path = self.read_env("PATH").await?;334 // // Assuming delimiter is :, we don't work with windows host, this check will be much335 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)336 // for ele in path.split(':') {337 // let test_path = format!("{ele}/{cmd}");338 // test -x etc339 // }340 // let mut cmd = self.cmd("printenv").await?;341 // cmd.arg(env);342 // Ok(cmd.run_string().await?)343 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.344 let mut cmd = self345 .cmd_escalation(346 // Not used347 EscalationStrategy::Su,348 "which",349 )350 .await?;351 cmd.arg(command);352 cmd.run_string().await353 }354 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>355 where356 <D as FromStr>::Err: Display,357 {358 let text = self.read_file_text(path).await?;359 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))360 }361 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {362 self.cmd_escalation(self.escalation_strategy().await?, cmd)363 .await364 }365 pub async fn cmd_escalation(366 &self,367 escalation: EscalationStrategy,368 cmd: impl AsRef<OsStr>,369 ) -> Result<MyCommand> {370 if self.local {371 Ok(MyCommand::new(escalation, cmd))372 } else {373 let session = self.open_session().await?;374 Ok(MyCommand::new_on(escalation, cmd, session))375 }376 }377 pub async fn nix_cmd(&self) -> Result<MyCommand> {378 let mut nix = self.cmd("nix").await?;379 nix.args([380 "--extra-experimental-features",381 "nix-command",382 "--extra-experimental-features",383 "flakes",384 ]);385 Ok(nix)386 }387388 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {389 ensure!(data.encrypted, "secret is not encrypted");390 let mut cmd = self.cmd("fleet-install-secrets").await?;391 cmd.arg("decrypt").eqarg("--secret", data.to_string());392 let encoded = cmd393 .sudo()394 .run_string()395 .await396 .context("failed to call remote host for decrypt")?;397 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;398 ensure!(!data.encrypted, "secret came out encrypted");399 Ok(data.data)400 }401 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {402 ensure!(data.encrypted, "secret is not encrypted");403 let mut cmd = self.cmd("fleet-install-secrets").await?;404 cmd.arg("reencrypt").eqarg("--secret", data.to_string());405 for target in targets {406 let key = self.config.key(&target).await?;407 cmd.eqarg("--targets", key);408 }409 let encoded = cmd410 .sudo()411 .run_string()412 .await413 .context("failed to call remote host for decrypt")?;414 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;415 ensure!(data.encrypted, "secret came out not encrypted");416 Ok(data)417 }418 /// Returns path for futureproofing, as path might change i.e on conversion to CA419 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {420 if self.local {421 // Path is located locally, thus already trusted.422 return Ok(path.to_owned());423 }424 let mut nix = MyCommand::new(425 // Not used426 EscalationStrategy::Su,427 "nix",428 );429 nix.arg("copy").arg("--substitute-on-destination");430431 let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {432 "ssh"433 } else {434 "ssh-ng"435 };436437 match self.deploy_kind().await? {438 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {439 nix.comparg("--to", format!("{proto}://{}", self.name));440 }441 DeployKind::NixosInstall => {442 nix443 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon444 .arg("--no-check-sigs")445 .comparg(446 "--to",447 format!("{proto}://root@{}?remote-store=/mnt", self.name),448 );449 }450 }451 nix.arg(path);452 nix.run_nix().await.context("nix copy")?;453 Ok(path.to_owned())454 }455 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {456 let mut cmd = self.cmd("systemctl").await?;457 cmd.arg("stop").arg(name);458 cmd.sudo().run().await459 }460 pub async fn systemctl_start(&self, name: &str) -> Result<()> {461 let mut cmd = self.cmd("systemctl").await?;462 cmd.arg("start").arg(name);463 cmd.sudo().run().await464 }465466 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {467 let mut cmd = self.cmd("rm").await?;468 cmd.arg("-f").arg(path);469 if sudo {470 cmd = cmd.sudo()471 }472 cmd.run().await473 }474}475impl ConfigHost {476 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,477 // assuming getting tags always returns the same value.478 pub async fn tags(&self) -> Result<Vec<String>> {479 if let Some(v) = self.groups.get() {480 return Ok(v.clone());481 }482 let Some(host_config) = &self.host_config else {483 return Ok(vec![]);484 };485 let tags: Vec<String> = nix_go_json!(host_config.tags);486487 let _ = self.groups.set(tags.clone());488489 Ok(tags)490 }491 pub async fn nixos_config(&self) -> Result<Value> {492 if let Some(v) = self.nixos_config.get() {493 return Ok(v.clone());494 }495 let Some(host_config) = &self.host_config else {496 bail!("local host has no nixos_config");497 };498 let nixos_config = nix_go!(host_config.nixos.config);499 assert_warn("nixos config evaluation", &nixos_config).await?;500501 let _ = self.nixos_config.set(nixos_config.clone());502503 Ok(nixos_config)504 }505 pub fn nixos_unchecked_config(&self) -> Result<Value> {506 if let Some(v) = self.nixos_unchecked_config.get() {507 return Ok(v.clone());508 }509 let Some(host_config) = &self.host_config else {510 bail!("local host has no nixos_config");511 };512 let nixos_config = nix_go!(host_config.nixos_unchecked.config);513514 let _ = self.nixos_unchecked_config.set(nixos_config.clone());515516 Ok(nixos_config)517 }518519 pub fn list_defined_secrets(&self) -> Result<Vec<String>> {520 let nixos = self.nixos_unchecked_config()?;521 let secrets = nix_go!(nixos.secrets);522 secrets.list_fields()523 }524 pub fn secret_definition(&self, name: &str) -> Result<HostSecretDefinition> {525 let nixos = self.nixos_unchecked_config()?;526 Ok(HostSecretDefinition(527 self.name.clone(),528 nix_go!(nixos.secrets[{ name }]),529 ))530 }531532 /// Packages for this host, resolved with nixpkgs overlays533 pub async fn pkgs(&self) -> Result<Value> {534 if let Some(value) = &self.pkgs_override {535 return Ok(value.clone());536 }537 let Some(host_config) = &self.host_config else {538 bail!("local host has no host_config");539 };540 // TODO: Should nixos.options be cached?541 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))542 }543}544545impl Config {546 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {547 let config = &self.config_field;548 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);549 Ok(tagged)550 }551 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {552 let mut out = BTreeSet::new();553 for owner in owners {554 if let Some(tag) = owner.strip_prefix('@') {555 let hosts = self.tagged_hostnames(tag).await?;556 out.extend(hosts);557 } else {558 out.insert(owner);559 }560 }561 Ok(out)562 }563 pub fn local_host(&self) -> ConfigHost {564 ConfigHost {565 config: self.clone(),566 name: "<virtual localhost>".to_owned(),567 host_config: None,568 nixos_config: OnceCell::new(),569 nixos_unchecked_config: OnceCell::new(),570 groups: {571 let cell = OnceCell::new();572 let _ = cell.set(vec![]);573 cell574 },575 pkgs_override: Some(self.default_pkgs.clone()),576577 local: true,578 session: OnceLock::new(),579 deploy_kind: OnceCell::new(),580 session_destination: OnceCell::new(),581 legacy_ssh_store: OnceCell::new(),582 }583 }584585 pub async fn host(&self, name: &str) -> Result<ConfigHost> {586 let config = &self.config_field;587 let host_config = nix_go!(config.hosts[{ name }]);588589 Ok(ConfigHost {590 config: self.clone(),591 name: name.to_owned(),592 host_config: Some(host_config),593 nixos_config: OnceCell::new(),594 nixos_unchecked_config: OnceCell::new(),595 groups: OnceCell::new(),596 pkgs_override: None,597598 // TODO: Remove with connectivit refactor599 local: self.localhost == name,600 session: OnceLock::new(),601 deploy_kind: OnceCell::new(),602 session_destination: OnceCell::new(),603 legacy_ssh_store: OnceCell::new(),604 })605 }606 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {607 let config = &self.config_field;608 let names = nix_go!(config.hosts).list_fields()?;609 let mut out = vec![];610 for name in names {611 out.push(self.host(&name).await?);612 }613 Ok(out)614 }615 // TODO: Replace usages with .host().nixos_config616 pub async fn system_config(&self, host: &str) -> Result<Value> {617 let fleet_field = &self.config_field;618 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))619 }620621 /// Shared secrets configured in fleet.nix or in flake622 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {623 let config_field = &self.config_field;624 nix_go!(config_field.sharedSecrets).list_fields()625 }626 pub fn has_shared(&self, name: &str) -> bool {627 let data = self.data();628 data.secrets.contains(name)629 }630 pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {631 let mut data = self.data_mut();632 data.secrets.set_data(name, shared);633 }634 pub fn remove_shared(&self, secret: &str) {635 let mut data = self.data_mut();636 data.secrets.remove(secret);637 }638639 pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {640 let data = self.data_mut();641 data.secrets.keys_for_owner(host).cloned().collect()642 }643 pub fn list_secrets(&self) -> Vec<String> {644 let data = self.data_mut();645 data.secrets.keys().cloned().collect()646 }647648 pub fn has_secret(&self, host: &str, secret: &str) -> bool {649 let data = self.data();650 data.secrets.contains_for_owner(secret, host)651 }652 pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {653 let mut data = self.data_mut();654 data.secrets.set_single_data(secret, host, value);655 }656 pub fn remove_secret(&self, host: &str, secret: &str) {657 let mut data = self.data_mut();658 data.secrets.drop_owner_no_reencrypt(secret, host);659 }660661 pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {662 let data = self.data();663 data.secrets.get_single(secret, host).cloned()664 }665 pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {666 let data = self.data();667 data.secrets.get(secret).cloned()668 }669 pub fn shared_secret_definition(&self, secret: &str) -> Result<SharedSecretDefinition> {670 let config_field = &self.config_field;671 Ok(SharedSecretDefinition(nix_go!(672 config_field.sharedSecrets[{ secret }]673 )))674 }675676 // TODO: Should this be something modifiable from other processes?677 // E.g terraform provider might want to update FleetData (e.g secrets),678 // and current implementation assumes only one process holds current fleet.nix679 // Given that it is no longer needs to be a file for nix evaluation,680 // maybe it can be a .nix file for persistence, but accessible only681 // thru some shared state controller? Might it be stored in terraform682 // state provider?683 pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {684 self.data.lock().unwrap()685 }686 pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {687 self.data.lock().unwrap()688 }689 pub fn save(&self) -> Result<()> {690 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.")?;691 let data = nixlike::serialize(&self.data() as &FleetData)?;692 tempfile.write_all(693 format!(694 "# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"695 )696 .as_bytes(),697 )?;698 let mut fleet_data_path = self.directory.clone();699 fleet_data_path.push("fleet.nix");700 tempfile.persist(fleet_data_path)?;701 Ok(())702 }703}1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};16use openssh::{ControlPersist, SessionBuilder};17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24 command::MyCommand,25 fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},26};2728pub struct FleetConfigInternals {29 /// Fleet project directory, containing fleet.nix file.30 pub directory: PathBuf,31 /// builtins.currentSystem32 pub local_system: String,33 pub data: Arc<Mutex<FleetData>>,34 pub nix_args: Vec<OsString>,35 /// fleet_config.config36 pub config_field: Value,37 /// flake.output38 pub flake_outputs: Value,39 // TODO: Remove with connectivity refactor40 pub localhost: String,4142 /// import nixpkgs {system = local};43 pub default_pkgs: Value,44 /// inputs.nixpkgs45 pub nixpkgs: Value,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53 type Target = FleetConfigInternals;5455 fn deref(&self) -> &Self::Target {56 &self.057 }58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62 Sudo,63 Run0,64 Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69 /// NixOS => NixOS managed by fleet70 UpgradeToFleet,71 /// NixOS managed by fleet => NixOS managed by fleet72 Fleet,73 /// Remote host has /mnt, /mnt/boot mounted,74 /// generated config is added to fleet configuration.75 NixosInstall,76 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77 /// generated config is added to fleet configuration,78 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79 NixosLustrate,80}8182impl FromStr for DeployKind {83 type Err = anyhow::Error;84 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85 match s {86 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87 "fleet" => Ok(Self::Fleet),88 "nixos-install" => Ok(Self::NixosInstall),89 "nixos-lustrate" => Ok(Self::NixosLustrate),90 v => bail!(91 "unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""92 ),93 }94 }95}96pub struct ConfigHost {97 config: Config,98 pub name: String,99 groups: OnceCell<Vec<String>>,100101 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it102 deploy_kind: OnceCell<DeployKind>,103 session_destination: OnceCell<String>,104 legacy_ssh_store: OnceCell<bool>,105106 pub host_config: Option<Value>,107 pub nixos_config: OnceCell<Value>,108 pub nixos_unchecked_config: OnceCell<Value>,109 pub pkgs_override: Option<Value>,110111 // TODO: Move command helpers away with connectivity refactor112 pub local: bool,113 pub session: OnceLock<Arc<openssh::Session>>,114}115116#[derive(Debug, Clone, Copy)]117pub enum GenerationStorage {118 Deployer,119 Machine,120 Pusher,121}122impl GenerationStorage {123 fn prefix(&self) -> &'static str {124 match self {125 GenerationStorage::Deployer => "deployer.",126 GenerationStorage::Machine => "",127 GenerationStorage::Pusher => "pusher.",128 }129 }130}131132#[derive(Tabled, Debug)]133pub struct Generation {134 #[tabled(rename = "ID", format("{}", self.rollback_id()))]135 pub id: u32,136 #[tabled(rename = "Current")]137 pub current: bool,138 #[tabled(rename = "Created at")]139 pub datetime: UtcDateTime,140 #[tabled(format = "{:?}")]141 pub store_path: PathBuf,142 #[tabled(skip)]143 pub location: GenerationStorage,144}145impl Generation {146 pub fn rollback_id(&self) -> String {147 format!("{}{}", self.location.prefix(), self.id)148 }149}150151fn parse_generation_line(g: &str) -> Option<Generation> {152 let mut parts = g.split_whitespace();153 let id = parts.next()?;154 let id: u32 = id.parse().ok()?;155 let date = parts.next()?;156 let time = parts.next()?;157 let current = if let Some(current) = parts.next() {158 if current == "(current)" {159 Some(true)160 } else {161 None162 }163 } else {164 Some(false)165 };166 let current = current?;167 if parts.next().is_some() {168 warn!("unexpected text after generation: {g}");169 }170171 let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")172 .expect("valid format");173 let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;174175 Some(Generation {176 id,177 current,178 datetime,179 store_path: PathBuf::new(),180 location: GenerationStorage::Machine,181 })182}183// TODO: Move command helpers away with connectivity refactor184impl ConfigHost {185 pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {186 let mut cmd = self.cmd("nix-env").await?;187 cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))188 .arg("--list-generations")189 .env("TZ", "UTC");190 // Sudo is required because --list-generations tries to acquire profile lock191 let data = cmd.sudo().run_string().await?;192 let mut generations = data193 .split('\n')194 .map(|e| e.trim())195 .filter(|&l| !l.is_empty())196 .filter_map(|g| {197 let generation = parse_generation_line(g);198 if generation.is_none() {199 warn!("bad generation: {g}");200 };201 generation202 })203 .collect::<Vec<_>>();204 for ele in generations.iter_mut() {205 let mut cmd = self.cmd("readlink").await?;206 cmd.arg("--")207 .arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));208 let path = cmd.run_string().await?;209 ele.store_path = PathBuf::from(path.trim_end_matches("\n"));210 }211212 Ok(generations)213 }214215 pub fn set_session_destination(&self, dest: String) {216 self.session_destination217 .set(dest)218 .expect("session destination is already set")219 }220 pub fn set_deploy_kind(&self, kind: DeployKind) {221 self.deploy_kind222 .set(kind)223 .expect("deploy kind is already set");224 }225 pub fn set_legacy_ssh_store(&self, legacy: bool) {226 self.legacy_ssh_store227 .set(legacy)228 .expect("legacy ssh store is already set")229 }230 pub async fn deploy_kind(&self) -> Result<DeployKind> {231 if let Some(kind) = self.deploy_kind.get() {232 return Ok(*kind);233 }234 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {235 Ok(v) => v,236 Err(e) => {237 bail!("failed to query remote system kind: {e}");238 }239 };240 if !is_fleet_managed {241 bail!(242 "{}",243 indoc::indoc! {"244 host is not marked as managed by fleet245 if you're not trying to lustrate/install system from scratch,246 you should either247 1. manually create /etc/FLEET_HOST file on the target host,248 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet249 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos250 "}251 );252 }253 // TOCTOU is possible254 let _ = self.deploy_kind.set(DeployKind::Fleet);255 Ok(*self.deploy_kind.get().expect("deploy kind is just set"))256 }257 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {258 // Prefer sudo, as run0 has some gotchas with polkit259 // and too many repeating prompts.260 if (self.find_in_path("sudo").await).is_ok() {261 return Ok(EscalationStrategy::Sudo);262 }263 if (self.find_in_path("run0").await).is_ok() {264 return Ok(EscalationStrategy::Run0);265 }266 Ok(EscalationStrategy::Su)267 }268 async fn open_session(&self) -> Result<Arc<openssh::Session>> {269 assert!(!self.local, "do not open ssh connection to local session");270 // FIXME: TOCTOU271 if let Some(session) = &self.session.get() {272 return Ok((*session).clone());273 };274 let mut session = SessionBuilder::default();275 session.control_persist(ControlPersist::ClosedAfterInitialConnection);276277 let dest = self.session_destination.get().unwrap_or(&self.name);278 let session = session279 .connect(&dest)280 .await281 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;282 let session = Arc::new(session);283 self.session.set(session.clone()).expect("TOCTOU happened");284 Ok(session)285 }286 pub async fn mktemp_dir(&self) -> Result<String> {287 let mut cmd = self.cmd("mktemp").await?;288 cmd.arg("-d");289 let path = cmd.run_string().await?;290 Ok(path.trim_end().to_owned())291 }292 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {293 let mut cmd = self.cmd("sh").await?;294 cmd.arg("-c")295 .arg("test -e \"$1\" && echo true || echo false")296 .arg("_")297 .arg(path);298 cmd.run_value().await299 }300 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {301 let mut cmd = self.cmd("cat").await?;302 cmd.arg(path);303 cmd.run_bytes().await304 }305 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {306 let mut cmd = self.cmd("cat").await?;307 cmd.arg(path);308 cmd.run_string().await309 }310 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {311 let mut cmd = self.cmd("ls").await?;312 cmd.arg(path);313 let out = cmd.run_string().await?;314 let mut lines = out.split('\n');315 if let Some(last) = lines.next_back() {316 ensure!(last.is_empty(), "output of ls should end with newline");317 }318 Ok(lines.map(ToOwned::to_owned).collect())319 }320 #[allow(dead_code)]321 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {322 let text = self.read_file_text(path).await?;323 Ok(serde_json::from_str(&text)?)324 }325 pub async fn read_env(&self, env: &str) -> Result<String> {326 let mut cmd = self.cmd("printenv").await?;327 cmd.arg(env);328 cmd.run_string().await329 }330 pub async fn find_in_path(&self, command: &str) -> Result<String> {331 // // `which` is not a part of coreutils, and it might not exist on machine.332 // let path = self.read_env("PATH").await?;333 // // Assuming delimiter is :, we don't work with windows host, this check will be much334 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)335 // for ele in path.split(':') {336 // let test_path = format!("{ele}/{cmd}");337 // test -x etc338 // }339 // let mut cmd = self.cmd("printenv").await?;340 // cmd.arg(env);341 // Ok(cmd.run_string().await?)342 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.343 let mut cmd = self344 .cmd_escalation(345 // Not used346 EscalationStrategy::Su,347 "which",348 )349 .await?;350 cmd.arg(command);351 cmd.run_string().await352 }353 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>354 where355 <D as FromStr>::Err: Display,356 {357 let text = self.read_file_text(path).await?;358 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))359 }360 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {361 self.cmd_escalation(self.escalation_strategy().await?, cmd)362 .await363 }364 pub async fn cmd_escalation(365 &self,366 escalation: EscalationStrategy,367 cmd: impl AsRef<OsStr>,368 ) -> Result<MyCommand> {369 if self.local {370 Ok(MyCommand::new(escalation, cmd))371 } else {372 let session = self.open_session().await?;373 Ok(MyCommand::new_on(escalation, cmd, session))374 }375 }376 pub async fn nix_cmd(&self) -> Result<MyCommand> {377 let mut nix = self.cmd("nix").await?;378 nix.args([379 "--extra-experimental-features",380 "nix-command",381 "--extra-experimental-features",382 "flakes",383 ]);384 Ok(nix)385 }386387 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {388 ensure!(data.encrypted, "secret is not encrypted");389 let mut cmd = self.cmd("fleet-install-secrets").await?;390 cmd.arg("decrypt").eqarg("--secret", data.to_string());391 let encoded = cmd392 .sudo()393 .run_string()394 .await395 .context("failed to call remote host for decrypt")?;396 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;397 ensure!(!data.encrypted, "secret came out encrypted");398 Ok(data.data)399 }400 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {401 ensure!(data.encrypted, "secret is not encrypted");402 let mut cmd = self.cmd("fleet-install-secrets").await?;403 cmd.arg("reencrypt").eqarg("--secret", data.to_string());404 for target in targets {405 let key = self.config.key(&target).await?;406 cmd.eqarg("--targets", key);407 }408 let encoded = cmd409 .sudo()410 .run_string()411 .await412 .context("failed to call remote host for decrypt")?;413 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;414 ensure!(data.encrypted, "secret came out not encrypted");415 Ok(data)416 }417 /// Returns path for futureproofing, as path might change i.e on conversion to CA418 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {419 if self.local {420 // Path is located locally, thus already trusted.421 return Ok(path.to_owned());422 }423 let mut nix = MyCommand::new(424 // Not used425 EscalationStrategy::Su,426 "nix",427 );428 nix.arg("copy").arg("--substitute-on-destination");429430 let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {431 "ssh"432 } else {433 "ssh-ng"434 };435436 match self.deploy_kind().await? {437 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {438 nix.comparg("--to", format!("{proto}://{}", self.name));439 }440 DeployKind::NixosInstall => {441 nix442 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon443 .arg("--no-check-sigs")444 .comparg(445 "--to",446 format!("{proto}://root@{}?remote-store=/mnt", self.name),447 );448 }449 }450 nix.arg(path);451 nix.run_nix().await.context("nix copy")?;452 Ok(path.to_owned())453 }454 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {455 let mut cmd = self.cmd("systemctl").await?;456 cmd.arg("stop").arg(name);457 cmd.sudo().run().await458 }459 pub async fn systemctl_start(&self, name: &str) -> Result<()> {460 let mut cmd = self.cmd("systemctl").await?;461 cmd.arg("start").arg(name);462 cmd.sudo().run().await463 }464465 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {466 let mut cmd = self.cmd("rm").await?;467 cmd.arg("-f").arg(path);468 if sudo {469 cmd = cmd.sudo()470 }471 cmd.run().await472 }473}474impl ConfigHost {475 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,476 // assuming getting tags always returns the same value.477 pub async fn tags(&self) -> Result<Vec<String>> {478 if let Some(v) = self.groups.get() {479 return Ok(v.clone());480 }481 let Some(host_config) = &self.host_config else {482 return Ok(vec![]);483 };484 let tags: Vec<String> = nix_go_json!(host_config.tags);485486 let _ = self.groups.set(tags.clone());487488 Ok(tags)489 }490 pub async fn nixos_config(&self) -> Result<Value> {491 if let Some(v) = self.nixos_config.get() {492 return Ok(v.clone());493 }494 let Some(host_config) = &self.host_config else {495 bail!("local host has no nixos_config");496 };497 let nixos_config = nix_go!(host_config.nixos.config);498 assert_warn("nixos config evaluation", &nixos_config).await?;499500 let _ = self.nixos_config.set(nixos_config.clone());501502 Ok(nixos_config)503 }504 pub fn nixos_unchecked_config(&self) -> Result<Value> {505 if let Some(v) = self.nixos_unchecked_config.get() {506 return Ok(v.clone());507 }508 let Some(host_config) = &self.host_config else {509 bail!("local host has no nixos_config");510 };511 let nixos_config = nix_go!(host_config.nixos_unchecked.config);512513 let _ = self.nixos_unchecked_config.set(nixos_config.clone());514515 Ok(nixos_config)516 }517518 pub fn list_defined_secrets(&self) -> Result<Vec<String>> {519 let nixos = self.nixos_unchecked_config()?;520 let secrets = nix_go!(nixos.secrets);521 secrets.list_fields()522 }523524 /// Packages for this host, resolved with nixpkgs overlays525 pub async fn pkgs(&self) -> Result<Value> {526 if let Some(value) = &self.pkgs_override {527 return Ok(value.clone());528 }529 let Some(host_config) = &self.host_config else {530 bail!("local host has no host_config");531 };532 // TODO: Should nixos.options be cached?533 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))534 }535}536537impl Config {538 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {539 let config = &self.config_field;540 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);541 Ok(tagged)542 }543 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {544 let mut out = BTreeSet::new();545 for owner in owners {546 if let Some(tag) = owner.strip_prefix('@') {547 let hosts = self.tagged_hostnames(tag).await?;548 out.extend(hosts);549 } else {550 out.insert(owner);551 }552 }553 Ok(out)554 }555 pub fn local_host(&self) -> ConfigHost {556 ConfigHost {557 config: self.clone(),558 name: "<virtual localhost>".to_owned(),559 host_config: None,560 nixos_config: OnceCell::new(),561 nixos_unchecked_config: OnceCell::new(),562 groups: {563 let cell = OnceCell::new();564 let _ = cell.set(vec![]);565 cell566 },567 pkgs_override: Some(self.default_pkgs.clone()),568569 local: true,570 session: OnceLock::new(),571 deploy_kind: OnceCell::new(),572 session_destination: OnceCell::new(),573 legacy_ssh_store: OnceCell::new(),574 }575 }576577 pub async fn host(&self, name: &str) -> Result<ConfigHost> {578 let config = &self.config_field;579 let host_config = nix_go!(config.hosts[{ name }]);580581 Ok(ConfigHost {582 config: self.clone(),583 name: name.to_owned(),584 host_config: Some(host_config),585 nixos_config: OnceCell::new(),586 nixos_unchecked_config: OnceCell::new(),587 groups: OnceCell::new(),588 pkgs_override: None,589590 // TODO: Remove with connectivit refactor591 local: self.localhost == name,592 session: OnceLock::new(),593 deploy_kind: OnceCell::new(),594 session_destination: OnceCell::new(),595 legacy_ssh_store: OnceCell::new(),596 })597 }598 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {599 let config = &self.config_field;600 let names = nix_go!(config.hosts).list_fields()?;601 let mut out = vec![];602 for name in names {603 out.push(self.host(&name).await?);604 }605 Ok(out)606 }607 // TODO: Replace usages with .host().nixos_config608 pub async fn system_config(&self, host: &str) -> Result<Value> {609 let fleet_field = &self.config_field;610 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))611 }612613 /// Shared secrets configured in fleet.nix or in flake614 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {615 let config_field = &self.config_field;616 nix_go!(config_field.sharedSecrets).list_fields()617 }618 pub fn has_shared(&self, name: &str) -> bool {619 let data = self.data();620 data.secrets.contains(name)621 }622 pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {623 let mut data = self.data_mut();624 data.secrets.set_data(name, shared);625 }626 pub fn remove_shared(&self, secret: &str) {627 let mut data = self.data_mut();628 data.secrets.remove(secret);629 }630631 pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {632 let data = self.data_mut();633 data.secrets.keys_for_owner(host).cloned().collect()634 }635 pub fn list_secrets(&self) -> Vec<String> {636 let data = self.data_mut();637 data.secrets.keys().cloned().collect()638 }639640 pub fn has_secret(&self, host: &str, secret: &str) -> bool {641 let data = self.data();642 data.secrets.contains_for_owner(secret, host)643 }644 pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {645 let mut data = self.data_mut();646 data.secrets.set_single_data(secret, host, value);647 }648 pub fn remove_secret(&self, host: &str, secret: &str) {649 let mut data = self.data_mut();650 data.secrets.drop_owner_no_reencrypt(secret, host);651 }652653 pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {654 let data = self.data();655 data.secrets.get_single(secret, host).cloned()656 }657 pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {658 let data = self.data();659 data.secrets.get(secret).cloned()660 }661662 // TODO: Should this be something modifiable from other processes?663 // E.g terraform provider might want to update FleetData (e.g secrets),664 // and current implementation assumes only one process holds current fleet.nix665 // Given that it is no longer needs to be a file for nix evaluation,666 // maybe it can be a .nix file for persistence, but accessible only667 // thru some shared state controller? Might it be stored in terraform668 // state provider?669 pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {670 self.data.lock().unwrap()671 }672 pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {673 self.data.lock().unwrap()674 }675 pub fn save(&self) -> Result<()> {676 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.")?;677 let data = nixlike::serialize(&self.data() as &FleetData)?;678 tempfile.write_all(679 format!(680 "# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"681 )682 .as_bytes(),683 )?;684 let mut fleet_data_path = self.directory.clone();685 fleet_data_path.push("fleet.nix");686 tempfile.persist(fleet_data_path)?;687 Ok(())688 }689}crates/fleet-base/src/opts.rsdiffbeforeafterboth--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -211,7 +211,7 @@
}
let bytes =
std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
- let data = Mutex::new(FleetData::from_str(&bytes)?);
+ let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));
let mut fetch_settings = FetchSettings::new();
fetch_settings.set(c"warn-dirty", c"false");
@@ -239,8 +239,7 @@
let builtins_field = Value::eval("builtins")?;
let fleet_root = flake.get_field("fleetConfigurations")?;
- let data_val = Value::serialized(&data)?;
- let fleet_field = nix_go!(fleet_root.default(data_val));
+ let fleet_field = nix_go!(fleet_root.default(Obj {}));
let config_field = nix_go!(fleet_field.config);
crates/fleet-base/src/primops.rsdiffbeforeafterboth--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -1,4 +1,9 @@
-use nix_eval::NativeFn;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+
+use nix_eval::{NativeFn, Value};
+
+use crate::fleetdata::{FleetData, FleetSecrets};
#[derive(thiserror::Error, Debug)]
enum Error {}
@@ -15,30 +20,32 @@
fn host_parts(&self, host: &str, name: &str) -> Parts;
}
-struct FsSecretsBackend {
+struct FsSecretsBackend {}
-}
-
-pub fn init_primops() {
+pub fn init_primops(secrets: Arc<Mutex<FleetData>>) {
NativeFn::new(
- c"fleet_ensure_secret",
+ c"fleet_ensure_host_secret",
c"Ensure secret existence for a host, regenerating it in case of some mismatch",
- [
- c"host",
- c"secret",
- c"expected_parts",
- c"expected_encrypted_parts",
- c"generator",
- ],
- |[
- host,
- secret,
- expected_parts,
- expected_encrypted_parts,
- generator,
- ]| {
-
- todo!()
+ [c"host", c"secret", c"generator"],
+ |[host, secret, generator]| {
+ todo!("ensure secret");
+ Ok(Value::new_attrs(HashMap::from_iter([(
+ "raw",
+ Value::new_str("rawData"),
+ )])))
+ },
+ )
+ .register();
+ NativeFn::new(
+ c"fleet_ensure_host_secret",
+ c"Ensure secret existence for a host, regenerating it in case of some mismatch",
+ [c"host", c"secret", c"generator"],
+ |[host, secret, generator]| {
+ todo!("ensure secret");
+ Ok(Value::new_attrs(HashMap::from_iter([(
+ "raw",
+ Value::new_str("rawData"),
+ )])))
},
)
.register();
crates/fleet-base/src/secret.rsdiffbeforeafterboth--- a/crates/fleet-base/src/secret.rs
+++ b/crates/fleet-base/src/secret.rs
@@ -1,8 +1,6 @@
use std::collections::BTreeSet;
-use anyhow::Result;
use chrono::{DateTime, Utc};
-use nix_eval::{Value, nix_go, nix_go_json};
use crate::fleetdata::FleetSecretData;
@@ -12,63 +10,6 @@
pub generation_data: serde_json::Value,
pub public_parts: BTreeSet<String>,
pub private_parts: BTreeSet<String>,
-}
-
-pub struct HostSecretDefinition(pub(crate) String, pub(crate) Value);
-impl HostSecretDefinition {
- pub fn is_managed(&self) -> Result<bool> {
- let def = self.definition_value()?;
- Ok(!nix_go!(def.generator).is_null())
- }
- pub fn is_shared(&self) -> Result<bool> {
- let def = self.definition_value()?;
- Ok(nix_go_json!(def.shared))
- }
- pub fn expectations(&self) -> Result<Expectations> {
- let def = self.definition_value()?;
- let parts = nix_go!(def.parts);
-
- let mut public_parts = BTreeSet::new();
- let mut private_parts = BTreeSet::new();
- for part in parts.list_fields()? {
- if nix_go_json!(parts[&part].encrypted) {
- private_parts.insert(part.clone());
- } else {
- public_parts.insert(part.clone());
- }
- }
-
- Ok(Expectations {
- owners: BTreeSet::from([self.0.clone()]),
- generation_data: nix_go_json!(def.expectedGenerationData),
- public_parts,
- private_parts,
- })
- }
- pub fn definition_value(&self) -> Result<Value> {
- let value = &self.1;
- Ok(nix_go!(value.definition))
- }
-}
-
-pub struct SharedSecretDefinition(pub(crate) Value);
-impl SharedSecretDefinition {
- pub fn is_managed(&self) -> Result<bool> {
- let value = &self.0;
- Ok(!nix_go!(value.generator).is_null())
- }
- pub fn expectations(&self) -> Result<Expectations> {
- let value = &self.0;
- Ok(Expectations {
- owners: nix_go_json!(value.expectedOwners),
- generation_data: nix_go_json!(value.expectedGenerationData),
- public_parts: nix_go_json!(value.expectedPublicParts),
- private_parts: nix_go_json!(value.expectedPrivateParts),
- })
- }
- pub fn definition_value(&self) -> Value {
- self.0.clone()
- }
}
#[derive(thiserror::Error, Debug)]
modules/module-list.nixdiffbeforeafterboth--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -6,5 +6,4 @@
./nixos.nix
./nixpkgs.nix
./secrets.nix
- ./secrets-data.nix
]
modules/nixos/secrets.nixdiffbeforeafterboth--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -8,50 +8,26 @@
let
inherit (builtins)
hashString
- elemAt
- length
toJSON
- filter
;
inherit (lib.stringsWithDeps) stringAfter;
inherit (lib.options) mkOption literalExpression;
inherit (lib.lists) optional;
- inherit (lib.attrsets) mapAttrs mapAttrsToList;
- inherit (lib.modules) mkIf mkMerge;
+ inherit (lib.attrsets) mapAttrs;
+ inherit (lib.modules) mkIf;
inherit (lib.types)
submodule
str
attrsOf
nullOr
unspecified
- lazyAttrsOf
uniq
functionTo
package
- listOf
- bool
;
inherit (fleetLib.strings) decodeRawSecret;
sysConfig = config;
- secretPartDataType = submodule {
- options = {
- raw = mkOption {
- type = str;
- internal = true;
- description = "Encoded & Encrypted secret part data, passed from fleet.nix";
- };
- };
- };
- secretDataType = submodule {
- freeformType = lazyAttrsOf secretPartDataType;
- options = {
- shared = mkOption {
- description = "Is this secret owned by this machine, or propagated from shared secrets";
- default = false;
- };
- };
- };
secretPartType =
secretName:
submodule (
@@ -61,11 +37,6 @@
in
{
options = {
- encrypted = mkOption {
- type = bool;
- description = "Is this secret part supposed to be encrypted?";
- };
-
hash = mkOption {
type = str;
description = "Hash of secret in encoded format";
@@ -82,17 +53,17 @@
type = str;
description = "Secret public data (only available for plaintext)";
};
+ raw = mkOption {
+ type = str;
+ description = "Raw (encoded/encrypted secret part data)";
+ };
+ };
+ config = {
+ hash = hashString "sha1" config.raw;
+ data = decodeRawSecret config.raw;
+ path = "/run/secrets/${secretName}/${config.hash}-${partName}";
+ stablePath = "/run/secrets/${secretName}/${partName}";
};
- config =
- let
- raw = sysConfig.data.secrets.${secretName}.${partName}.raw;
- in
- {
- hash = hashString "sha1" raw;
- data = decodeRawSecret raw;
- path = "/run/secrets/${secretName}/${config.hash}-${partName}";
- stablePath = "/run/secrets/${secretName}/${partName}";
- };
}
);
secretType = submodule (
@@ -105,14 +76,9 @@
in
{
options = {
- shared = mkOption {
- type = bool;
- description = "Was this secret propagated from a shared secret?";
- };
parts = mkOption {
- type = lazyAttrsOf (secretPartType secretName);
+ type = attrsOf (secretPartType secretName);
description = "Definition of secret parts";
- default = { };
};
generator = mkOption {
type = uniq (nullOr (functionTo package));
@@ -134,47 +100,14 @@
description = "Group of the secret";
default = sysConfig.users.users.${config.owner}.group;
defaultText = literalExpression "config.users.users.$${owner}.group";
- };
- expectedGenerationData = mkOption {
- type = unspecified;
- description = "Data that gets embedded into secret part";
- default = null;
};
};
config = {
- shared = (sysConfig.data.secrets.${secretName} or { shared = false; }).shared;
- parts = mkMerge [
- (mkIf (config.generator != null)
- (
- # Get fake derivation body, in future it should be implemented the same way as in Rust.
- lib.callPackageWith (
- pkgs
- // {
- mkSecretGenerator = pkgs.stdenv.mkDerivation;
- mkImpureSecretGenerator = pkgs.stdenv.mkDerivation;
- }
- ) config.generator { }
- ).parts
- )
- (mapAttrs (_: _: { }) (
- removeAttrs (sysConfig.data.secrets.${secretName} or { }) [
- "shared"
- "managed"
- ]
- ))
- ];
+ parts = builtins.fleet_ensure_host_secret sysConfig.networking.hostName secretName config.generator;
};
}
);
- processPart = secretName: partName: part: {
- inherit (part) path stablePath;
- raw = config.data.secrets.${secretName}.${partName}.raw;
- };
- processSecret = secretName: secret: {
- inherit (secret.definition) group mode owner;
- parts = (mapAttrs (processPart secretName) (secret.definition.parts));
- };
- secretsData = (mapAttrs (processSecret) config.secrets);
+ secretsData = (mapAttrs (_: s: s.definition) config.secrets);
secretsFile = pkgs.writeTextFile {
name = "secrets.json";
text = toJSON secretsData;
@@ -185,11 +118,6 @@
in
{
options = {
- data.secrets = mkOption {
- type = attrsOf secretDataType;
- default = { };
- description = "Host-local secret data";
- };
secrets = mkOption {
type = attrsOf secretType;
default = { };
modules/secrets-data.nixdiffbeforeafterboth--- a/modules/secrets-data.nix
+++ /dev/null
@@ -1,95 +0,0 @@
-{
- lib,
- fleetLib,
- ...
-}:
-let
- inherit (fleetLib.options) mkDataOption;
- inherit (lib.options) mkOption;
- inherit (lib.types)
- nullOr
- listOf
- str
- attrsOf
- submodule
- bool
- unspecified
- ;
-
- secretDataValue = {
- options = {
- raw = mkOption {
- type = nullOr str;
- description = "Raw secret data in unspecified encoded and optionally encrypted format.";
- default = null;
- };
- };
- };
-
- sharedSecretData = {
- freeformType = attrsOf (submodule secretDataValue);
- options = {
- managed = mkOption {
- type = nullOr bool;
- description = "Is current fleet data value is generated by generator";
- default = null;
- };
-
- createdAt = mkOption {
- type = str;
- description = "Timestamp of secret generation/last rotation.";
- default = null;
- };
- expiresAt = mkOption {
- type = nullOr str;
- description = "Expiration timestamp triggering mandatory secret rotation.";
- default = null;
- };
-
- owners = mkOption {
- type = listOf str;
- description = ''
- List of hosts currently authorized to decrypt this shared secret.
-
- If owners differ from expected owners, the secret is considered outdated
- and requires regeneration or re-encryption.
- '';
- default = [ ];
- };
- generationData = mkOption {
- type = unspecified;
- description = "Contextual metadata associated with secret part.";
- default = null;
- };
- };
- };
-
- managerKey = {
- options = {
- name = mkOption {
- type = str;
- description = "Who does this manager key belongs to.";
- };
- key = mkOption {
- type = str;
- description = "Age-compatible key";
- };
- };
- config = { };
- };
-in
-{
- options.data = mkDataOption ({ config, ... }:
- {
- options = {
- managerKeys = mkOption {
- type = listOf (submodule managerKey);
- };
- secrets = mkOption {
- type = attrsOf (listOf submodule sharedSecretData);
- default = { };
- description = "Shared secret data.";
- };
- };
- });
-}
modules/secrets.nixdiffbeforeafterboth--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -5,7 +5,6 @@
let
inherit (lib.options) mkOption literalExpression;
inherit (lib.types)
- unspecified
nullOr
listOf
str
@@ -66,22 +65,7 @@
An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,
use `mkSecretGenerator` helpers to implement own generators.
'';
- default = null;
- };
- expectedGenerationData = mkOption {
- type = unspecified;
- description = "Contextual metadata embedded within the secret part value";
default = null;
- };
- expectedPrivateParts = mkOption {
- type = listOf str;
- default = [ ];
- description = "List of parts that are expected to be encrypted";
- };
- expectedPublicParts = mkOption {
- type = listOf str;
- default = [ ];
- description = "List of parts that are expected to be public";
};
};
};