difftreelog
refactor move parts to secret generator derivation
in: trunk
5 files changed
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -158,7 +158,7 @@
expectations: &Expectations,
) -> Result<FleetSharedSecret> {
let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);
- let value = definition.inner();
+ let value = definition.definition_value();
let (should_reencrypt, reason) = match reason {
Some(RegenerationReason::OwnersAdded(_)) => {
@@ -401,7 +401,13 @@
// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);
Ok(FleetSharedSecret {
managed: Some(true),
- secret: generate(config, display_name, secret.inner(), expectations).await?,
+ secret: generate(
+ config,
+ display_name,
+ secret.definition_value(),
+ expectations,
+ )
+ .await?,
owners: expectations.owners.clone(),
})
}
@@ -711,7 +717,9 @@
}
let definition = config.shared_secret_definition(&name)?;
- let expectations = definition.expectations()?;
+ let expectations = definition
+ .expectations()
+ .with_context(|| format!("expectations for shared {name:?}"))?;
let updated = maybe_regenerate_shared_secret(
&name,
@@ -744,7 +752,9 @@
info!("skipping unmanaged secret: {missing}");
continue;
}
- let expectations = definition.expectations()?;
+ let expectations = definition
+ .expectations()
+ .with_context(|| format!("expectations for shared {missing:?}"))?;
info!("generating secret: {missing}");
let shared = generate_shared(config, missing, definition, &expectations)
.in_current_span()
@@ -768,13 +778,18 @@
.into_iter()
.collect::<HashSet<_>>();
for missing_secret in expected_set.difference(&stored_set) {
+ let secret = host.secret_definition(missing_secret)?;
+ if secret.is_shared()? {
+ continue;
+ }
info!("generating missing secret: {missing_secret}");
- let definition = host.secret_definition(missing_secret)?;
- let expectations = definition.expectations()?;
+ let expectations = secret.expectations().with_context(|| {
+ format!("expectations for {missing_secret:?} of {:?}", host.name)
+ })?;
let generated = match generate(
config,
missing_secret,
- definition.inner(),
+ secret.definition_value()?,
&expectations,
)
.in_current_span()
@@ -796,16 +811,19 @@
)
}
for known_secret in stored_set.intersection(&expected_set) {
+ let secret = host.secret_definition(known_secret)?;
+ if secret.is_shared()? {
+ continue;
+ }
info!("updating secret: {known_secret}");
let data = config.host_secret(&host.name, known_secret)?;
- let definition = host.secret_definition(known_secret)?;
- let expectations = definition.expectations()?;
+ let expectations = secret.expectations()?;
if let Some(regen_reason) = data.needs_regeneration(&expectations) {
info!("needs regeneration: {regen_reason}");
let generated = match generate(
config,
known_secret,
- definition.inner(),
+ secret.definition_value()?,
&expectations,
)
.in_current_span()
@@ -828,6 +846,10 @@
}
}
for removed_secret in stored_set.difference(&expected_set) {
+ let definition = host.secret_definition(removed_secret)?;
+ if definition.is_shared()? {
+ continue;
+ }
info!("removing secret: {removed_secret}");
config.remove_secret(&host.name, removed_secret);
}
crates/fleet-base/src/secret.rsdiffbeforeafterboth1use std::collections::BTreeSet;23use anyhow::Result;4use chrono::{DateTime, Utc};5use nix_eval::{Value, nix_go, nix_go_json};67use crate::fleetdata::FleetSecretData;89#[derive(Debug)]10pub struct Expectations {11 pub owners: BTreeSet<String>,12 pub generation_data: serde_json::Value,13 pub public_parts: BTreeSet<String>,14 pub private_parts: BTreeSet<String>,15}1617pub struct HostSecretDefinition(pub(crate) String, pub(crate) Value);18impl HostSecretDefinition {19 pub fn is_managed(&self) -> Result<bool> {20 let value = &self.1;21 Ok(!nix_go!(value.generator).is_null())22 }23 pub fn expectations(&self) -> Result<Expectations> {24 let value = &self.1;25 Ok(Expectations {26 owners: BTreeSet::from([self.0.clone()]),27 generation_data: nix_go_json!(value.expectedGenerationData),28 public_parts: nix_go_json!(value.expectedPublicParts),29 private_parts: nix_go_json!(value.expectedPrivateParts),30 })31 }32 pub fn inner(&self) -> Value {33 self.1.clone()34 }35}3637pub struct SharedSecretDefinition(pub(crate) Value);38impl SharedSecretDefinition {39 pub fn is_managed(&self) -> Result<bool> {40 let value = &self.0;41 Ok(!nix_go!(value.generator).is_null())42 }43 pub fn expectations(&self) -> Result<Expectations> {44 let value = &self.0;45 Ok(Expectations {46 owners: nix_go_json!(value.expectedOwners),47 generation_data: nix_go_json!(value.expectedGenerationData),48 public_parts: nix_go_json!(value.expectedPublicParts),49 private_parts: nix_go_json!(value.expectedPrivateParts),50 })51 }52 pub fn inner(&self) -> Value {53 self.0.clone()54 }55}5657#[derive(thiserror::Error, Debug)]58pub enum RegenerationReason {59 #[error("owners added: {0:?}")]60 OwnersAdded(BTreeSet<String>),61 #[error("owners added: {0:?}")]62 OwnersRemoved(BTreeSet<String>),63 #[error("unexpected generation data, expected: {expected:?}, found: {found:?}")]64 GenerationData {65 expected: serde_json::Value,66 found: serde_json::Value,67 },68 #[error("unexpected part list, expected: {expected:?}, found: {found:?}")]69 PartList {70 expected: BTreeSet<String>,71 found: BTreeSet<String>,72 },73 #[error("part {0} is expected to be encrypted")]74 ExpectedPrivate(String),75 #[error("part {0} is not expected to be encrypted")]76 ExpectedPublic(String),77 #[error("secret is expired at {0}")]78 Expired(DateTime<Utc>),79}8081pub fn secret_needs_regeneration(82 secret: &FleetSecretData,83 owners: &BTreeSet<String>,84 expectations: &Expectations,85) -> Option<RegenerationReason> {86 if !owners.is_empty() {87 let added: BTreeSet<String> = expectations.owners.difference(owners).cloned().collect();88 if !added.is_empty() {89 return Some(RegenerationReason::OwnersAdded(added));90 }9192 let removed: BTreeSet<String> = owners.difference(&expectations.owners).cloned().collect();93 if !removed.is_empty() {94 return Some(RegenerationReason::OwnersRemoved(removed));95 }96 }9798 if secret.generation_data != expectations.generation_data {99 return Some(RegenerationReason::GenerationData {100 expected: expectations.generation_data.clone(),101 found: secret.generation_data.clone(),102 });103 }104105 if !expectations.public_parts.is_empty() || !expectations.private_parts.is_empty() {106 let expected: BTreeSet<String> = expectations107 .public_parts108 .union(&expectations.private_parts)109 .cloned()110 .collect();111 let found: BTreeSet<String> = secret.parts.keys().cloned().collect();112113 if found != expected {114 return Some(RegenerationReason::PartList { expected, found });115 }116117 for (name, value) in secret.parts.iter() {118 if value.raw.encrypted {119 if !expectations.private_parts.contains(name) {120 return Some(RegenerationReason::ExpectedPrivate(name.clone()));121 }122 } else if !expectations.public_parts.contains(name) {123 return Some(RegenerationReason::ExpectedPublic(name.clone()));124 }125 }126 }127128 if let Some(expiration) = secret.expires_at {129 // TODO: Leeway?130 if expiration < Utc::now() {131 return Some(RegenerationReason::Expired(expiration));132 }133 }134135 None136}lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -57,215 +57,191 @@
inherit (modules) mkFleetDefault mkFleetGeneratorDefault;
- secrets =
- let
- describedGenerator =
- generator: {parts ? {}}:
- {parts = {};}
- // {
- __functionArgs = functionArgs generator;
- __functor = _: generator;
- };
- in
- {
- inherit describedGenerator;
+ secrets = {
- /**
- Generate a random secret password, 32 ascii characters by default
+ /**
+ Generate a random secret password, 32 ascii characters by default
- Options:
- size: generated password length in ascii characters (bytes).
- noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might
- not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].
+ Options:
+ size: generated password length in ascii characters (bytes).
+ noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might
+ not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].
- Output:
- Resulting secret has only part: secret, which contains encrypted password.
- */
- mkPassword =
+ Output:
+ Resulting secret has only part: secret, which contains encrypted password.
+ */
+ mkPassword =
+ {
+ size ? 32,
+ }:
+ (
{
- size ? 32,
+ coreutils,
+ mkSecretGenerator,
}:
- describedGenerator
- (
- {
- coreutils,
- mkSecretGenerator,
- }:
- mkSecretGenerator {
- script = ''
- mkdir $out
- gh generate password -o $out/secret --size ${toString size}
- '';
- }
- )
- {
- parts.secret.encrypted = true;
- };
+ mkSecretGenerator {
+ script = ''
+ mkdir $out
+ gh generate password -o $out/secret --size ${toString size}
+ '';
+ parts.secret.encrypted = true;
+ }
+ );
- /**
- Generate a random ed25519 keypair
+ /**
+ Generate a random ed25519 keypair
- Options:
- noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)
- When noEmbedPublis is enabled - only the private scalar is included.
- encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
+ Options:
+ noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)
+ When noEmbedPublis is enabled - only the private scalar is included.
+ encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
- Output:
- Resulting secret has two parts: public and secret, where the secret part is encrypted.
+ Output:
+ Resulting secret has two parts: public and secret, where the secret part is encrypted.
+
+ This secret format is used by e.g Garage S3 server
+ */
+ mkEd25519 =
+ {
+ noEmbedPublic ? false,
+ encoding ? null,
+ }:
+ (
+ { mkSecretGenerator }:
+ mkSecretGenerator {
+ script = ''
+ mkdir $out
+ gh generate ed25519 -p $out/public -s $out/secret \
+ ${optionalString noEmbedPublic "--no-embed-public"} \
+ ${optionalString (encoding != null) "--encoding=${encoding}"}
+ '';
+ parts.secret.encrypted = true;
+ parts.public.encrypted = false;
+ }
+ );
- This secret format is used by e.g Garage S3 server
- */
- mkEd25519 =
- {
- noEmbedPublic ? false,
- encoding ? null,
- }:
- describedGenerator
- (
- { mkSecretGenerator }:
- mkSecretGenerator {
- script = ''
- mkdir $out
- gh generate ed25519 -p $out/public -s $out/secret \
- ${optionalString noEmbedPublic "--no-embed-public"} \
- ${optionalString (encoding != null) "--encoding=${encoding}"}
- '';
- }
- )
- {
- parts.secret.encrypted = true;
- parts.public.encrypted = false;
- };
+ /**
+ Generate a random x25519 keypair
- /**
- Generate a random x25519 keypair
+ Options:
+ encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
- Options:
- encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
+ Output:
+ Resulting secret has two parts: public and secret, where the secret part is encrypted.
- Output:
- Resulting secret has two parts: public and secret, where the secret part is encrypted.
+ This secret format is used by e.g Wireguard VPN for peers (base64-encoded)
+ */
+ mkX25519 =
+ {
+ encoding ? null,
+ }:
+ (
+ { mkSecretGenerator }:
+ mkSecretGenerator {
+ script = ''
+ mkdir $out
+ gh generate x25519 -p $out/public -s $out/secret \
+ ${optionalString (encoding != null) "--encoding=${encoding}"}
+ '';
- This secret format is used by e.g Wireguard VPN for peers (base64-encoded)
- */
- mkX25519 =
- {
- encoding ? null,
- }:
- describedGenerator
- (
- { mkSecretGenerator }:
- mkSecretGenerator {
- script = ''
- mkdir $out
- gh generate x25519 -p $out/public -s $out/secret \
- ${optionalString (encoding != null) "--encoding=${encoding}"}
- '';
- }
- )
- {
- parts.secret.encrypted = true;
- parts.public.encrypted = false;
- };
+ parts.secret.encrypted = true;
+ parts.public.encrypted = false;
+ }
+ );
- /**
- Generate a random RSA keypair
+ /**
+ Generate a random RSA keypair
- Options:
- size: RSA key size, 4096 by default
+ Options:
+ size: RSA key size, 4096 by default
- Output:
- Resulting secret has two parts: public and secret, where the secret part is encrypted.
- Both parts are PEM encoded.
- */
- mkRsa =
+ Output:
+ Resulting secret has two parts: public and secret, where the secret part is encrypted.
+ Both parts are PEM encoded.
+ */
+ mkRsa =
+ {
+ size ? 4096,
+ }:
+ (
{
- size ? 4096,
+ openssl,
+ mkSecretGenerator,
}:
- describedGenerator
- (
- {
- openssl,
- mkSecretGenerator,
- }:
- mkSecretGenerator {
- script = ''
- mkdir $out
+ mkSecretGenerator {
+ script = ''
+ mkdir $out
+
+ ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
+ ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
- ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
- ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
+ cat rsa_private.key | gh private -o $out/secret
+ cat rsa_public.key | gh public -o $out/public
+ '';
- cat rsa_private.key | gh private -o $out/secret
- cat rsa_public.key | gh public -o $out/public
- '';
- }
- )
- {
- parts.secret.encrypted = true;
- parts.public.encrypted = false;
- };
+ parts.secret.encrypted = true;
+ parts.public.encrypted = false;
+ }
+ );
- /**
- Generate a random byte sequence
+ /**
+ Generate a random byte sequence
- Options:
- size: generated password length in bytes, 32 by default.
- encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"
- noNuls: prevent output byte sequence from containing internal \0, useful for some C applications
- that can't handle their strings properly.
+ Options:
+ size: generated password length in bytes, 32 by default.
+ encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"
+ noNuls: prevent output byte sequence from containing internal \0, useful for some C applications
+ that can't handle their strings properly.
- Output:
- Resulting secret has only part: secret, which contains encrypted bytes.
+ Output:
+ Resulting secret has only part: secret, which contains encrypted bytes.
- Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)
- */
- mkBytes =
- {
- count ? 32,
- encoding,
- noNuls ? false,
- }:
- describedGenerator
- (
- { mkSecretGenerator }:
- mkSecretGenerator {
- script = ''
- mkdir $out
- gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \
- ${optionalString noNuls "--no-nuls"}
- '';
- }
- )
- {
- parts.secret.encrypted = true;
- };
- /**
- Shorthand for `mkBytes`, which defaults to "hex" encoding
- */
- mkHexBytes =
- {
- count ? 32,
- }:
- mkBytes {
- inherit count;
- encoding = "hex";
- };
- /**
- Shorthand for `mkBytes`, which defaults to "base64" encoding
- */
- mkBase64Bytes =
- {
- count ? 32,
- }:
- mkBytes {
- inherit count;
- encoding = "base64";
- };
+ Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)
+ */
+ mkBytes =
+ {
+ count ? 32,
+ encoding,
+ noNuls ? false,
+ }:
+ (
+ { mkSecretGenerator }:
+ mkSecretGenerator {
+ script = ''
+ mkdir $out
+ gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \
+ ${optionalString noNuls "--no-nuls"}
+ '';
+ parts.secret.encrypted = true;
+ }
+ );
+ /**
+ Shorthand for `mkBytes`, which defaults to "hex" encoding
+ */
+ mkHexBytes =
+ {
+ count ? 32,
+ }:
+ mkBytes {
+ inherit count;
+ encoding = "hex";
+ };
+ /**
+ Shorthand for `mkBytes`, which defaults to "base64" encoding
+ */
+ mkBase64Bytes =
+ {
+ count ? 32,
+ }:
+ mkBytes {
+ inherit count;
+ encoding = "base64";
+ };
- # Wireguard
- # mkWireguard = {}: mkX25519 {encoding = "base64";};
- # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};
- };
+ # Wireguard
+ # mkWireguard = {}: mkX25519 {encoding = "base64";};
+ # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};
+ };
inherit (secrets)
mkPassword
modules/nixos/secrets.nixdiffbeforeafterboth--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -105,10 +105,14 @@
in
{
options = {
+ shared = mkOption {
+ type = bool;
+ description = "Was this secret propagated from a shared secret?";
+ };
parts = mkOption {
type = lazyAttrsOf (secretPartType secretName);
description = "Definition of secret parts";
- default = {};
+ default = { };
};
generator = mkOption {
type = uniq (nullOr (functionTo package));
@@ -137,24 +141,39 @@
default = null;
};
};
- config.parts = mkMerge [
- (mkIf (config.generator != null && config.generator ? parts) config.generator.parts)
- (mapAttrs (_: _: {}) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) ["shared" "managed"]))
- ];
+ 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"
+ ]
+ ))
+ ];
+ };
}
);
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
- ));
- };
+ processSecret = secretName: secret: {
+ inherit (secret.definition) group mode owner;
+ parts = (mapAttrs (processPart secretName) (secret.definition.parts));
+ };
secretsData = (mapAttrs (processSecret) config.secrets);
secretsFile = pkgs.writeTextFile {
name = "secrets.json";
@@ -174,7 +193,7 @@
secrets = mkOption {
type = attrsOf secretType;
default = { };
- apply = v: (mapAttrs (_: secret: secret.parts // {definition = secret;}) v);
+ apply = v: (mapAttrs (_: secret: secret.parts // { definition = secret; }) v);
description = "Host-local secrets";
};
system.secretsData = mkOption {
modules/secrets.nixdiffbeforeafterboth--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -124,6 +124,7 @@
# 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,
+ parts,
}:
(prev.writeShellScript "impureGenerator.sh" ''
#!/bin/sh
@@ -151,12 +152,12 @@
'').overrideAttrs
(old: {
passthru = {
- inherit impureOn;
+ inherit impureOn parts;
generatorKind = "impure";
};
});
# Pure generators are disabled for now
- mkSecretGenerator = { script }: mkImpureSecretGenerator { inherit script; };
+ mkSecretGenerator = { script, parts }: mkImpureSecretGenerator { inherit script parts; };
# TODO: Implement consistent naming
# Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...