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.rsdiffbeforeafterboth--- a/crates/fleet-base/src/secret.rs
+++ b/crates/fleet-base/src/secret.rs
@@ -17,20 +17,37 @@
pub struct HostSecretDefinition(pub(crate) String, pub(crate) Value);
impl HostSecretDefinition {
pub fn is_managed(&self) -> Result<bool> {
- let value = &self.1;
- Ok(!nix_go!(value.generator).is_null())
+ 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 value = &self.1;
+ 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!(value.expectedGenerationData),
- public_parts: nix_go_json!(value.expectedPublicParts),
- private_parts: nix_go_json!(value.expectedPrivateParts),
+ generation_data: nix_go_json!(def.expectedGenerationData),
+ public_parts,
+ private_parts,
})
}
- pub fn inner(&self) -> Value {
- self.1.clone()
+ pub fn definition_value(&self) -> Result<Value> {
+ let value = &self.1;
+ Ok(nix_go!(value.definition))
}
}
@@ -49,7 +66,7 @@
private_parts: nix_go_json!(value.expectedPrivateParts),
})
}
- pub fn inner(&self) -> Value {
+ pub fn definition_value(&self) -> Value {
self.0.clone()
}
}
lib/default.nixdiffbeforeafterboth1# Shared functions for fleet configuration, available as `fleet` module argument2{ lib }:3let4 inherit (lib.trivial) isFunction functionArgs;5 inherit (lib.options) mkOption mergeOneOption;6 inherit (lib.modules) mkOverride;7 inherit (lib.types)8 listOf9 submodule10 attrsOf11 mkOptionType12 ;13 inherit (lib.strings) optionalString hasPrefix removePrefix;14in15rec {16 types = {17 overlay = mkOptionType {18 name = "nixpkgs-overlay";19 description = "nixpkgs overlay";20 check = {21 __functor = _self: isFunction;22 isV2MergeCoherent = true;23 };24 merge = mergeOneOption;25 };26 listOfOverlay = listOf types.overlay;2728 mkHostsType = module: attrsOf (submodule module);29 mkDataType = module: submodule module;30 };3132 options = {33 mkHostsOption =34 module:35 mkOption {36 type = types.mkHostsType module;37 };38 mkDataOption =39 module:40 mkOption {41 type = types.mkDataType module;42 };43 };4445 inherit (options) mkHostsOption;4647 modules = {48 /**49 Use in places, where fleet might know better than nixpkgs defaults to50 */51 mkFleetDefault = mkOverride 999;52 /**53 Some generators use mkDefault, but optionDefault is set by nixpkgs.54 */55 mkFleetGeneratorDefault = mkOverride 1001;56 };5758 inherit (modules) mkFleetDefault mkFleetGeneratorDefault;5960 secrets =61 let62 describedGenerator =63 generator: {parts ? {}}:64 {parts = {};}65 // {66 __functionArgs = functionArgs generator;67 __functor = _: generator;68 };69 in70 {71 inherit describedGenerator;7273 /**74 Generate a random secret password, 32 ascii characters by default7576 Options:77 size: generated password length in ascii characters (bytes).78 noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might79 not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].8081 Output:82 Resulting secret has only part: secret, which contains encrypted password.83 */84 mkPassword =85 {86 size ? 32,87 }:88 describedGenerator89 (90 {91 coreutils,92 mkSecretGenerator,93 }:94 mkSecretGenerator {95 script = ''96 mkdir $out97 gh generate password -o $out/secret --size ${toStringsize}98 '';99 }100 )101 {102 parts.secret.encrypted = true;103 };104105 /**106 Generate a random ed25519 keypair107108 Options:109 noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)110 When noEmbedPublis is enabled - only the private scalar is included.111 encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".112113 Output:114 Resulting secret has two parts: public and secret, where the secret part is encrypted.115116 This secret format is used by e.g Garage S3 server117 */118 mkEd25519 =119 {120 noEmbedPublic ? false,121 encoding ? null,122 }:123 describedGenerator124 (125 { mkSecretGenerator }:126 mkSecretGenerator {127 script = ''128 mkdir $out129 gh generate ed25519 -p $out/public -s $out/secret \130 ${optionalStringnoEmbedPublic"--no-embed-public"} \131 ${optionalString(encoding!=null)"--encoding=${encoding}"}132 '';133 }134 )135 {136 parts.secret.encrypted = true;137 parts.public.encrypted = false;138 };139140 /**141 Generate a random x25519 keypair142143 Options:144 encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".145146 Output:147 Resulting secret has two parts: public and secret, where the secret part is encrypted.148149 This secret format is used by e.g Wireguard VPN for peers (base64-encoded)150 */151 mkX25519 =152 {153 encoding ? null,154 }:155 describedGenerator156 (157 { mkSecretGenerator }:158 mkSecretGenerator {159 script = ''160 mkdir $out161 gh generate x25519 -p $out/public -s $out/secret \162 ${optionalString(encoding!=null)"--encoding=${encoding}"}163 '';164 }165 )166 {167 parts.secret.encrypted = true;168 parts.public.encrypted = false;169 };170171 /**172 Generate a random RSA keypair173174 Options:175 size: RSA key size, 4096 by default176177 Output:178 Resulting secret has two parts: public and secret, where the secret part is encrypted.179 Both parts are PEM encoded.180 */181 mkRsa =182 {183 size ? 4096,184 }:185 describedGenerator186 (187 {188 openssl,189 mkSecretGenerator,190 }:191 mkSecretGenerator {192 script = ''193 mkdir $out194195 ${openssl}/bin/openssl genrsa -out rsa_private.key ${toStringsize}196 ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key197198 cat rsa_private.key | gh private -o $out/secret199 cat rsa_public.key | gh public -o $out/public200 '';201 }202 )203 {204 parts.secret.encrypted = true;205 parts.public.encrypted = false;206 };207208 /**209 Generate a random byte sequence210211 Options:212 size: generated password length in bytes, 32 by default.213 encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"214 noNuls: prevent output byte sequence from containing internal \0, useful for some C applications215 that can't handle their strings properly.216217 Output:218 Resulting secret has only part: secret, which contains encrypted bytes.219220 Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)221 */222 mkBytes =223 {224 count ? 32,225 encoding,226 noNuls ? false,227 }:228 describedGenerator229 (230 { mkSecretGenerator }:231 mkSecretGenerator {232 script = ''233 mkdir $out234 gh generate bytes --count=${toStringcount} --encoding=${encoding} -o $out/secret \235 ${optionalStringnoNuls"--no-nuls"}236 '';237 }238 )239 {240 parts.secret.encrypted = true;241 };242 /**243 Shorthand for `mkBytes`, which defaults to "hex" encoding244 */245 mkHexBytes =246 {247 count ? 32,248 }:249 mkBytes {250 inherit count;251 encoding = "hex";252 };253 /**254 Shorthand for `mkBytes`, which defaults to "base64" encoding255 */256 mkBase64Bytes =257 {258 count ? 32,259 }:260 mkBytes {261 inherit count;262 encoding = "base64";263 };264265 # Wireguard266 # mkWireguard = {}: mkX25519 {encoding = "base64";};267 # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};268 };269270 inherit (secrets)271 mkPassword272 mkEd25519273 mkX25519274 mkRsa275 mkBytes276 mkHexBytes277 mkBase64Bytes278 ;279280 strings =281 let282 plaintextPrefix = "<PLAINTEXT>";283 plaintextNewlinePrefix = "<PLAINTEXT-NL>";284 in285 {286 /**287 Decode public secret part into string288 */289 decodeRawSecret =290 raw:291 if hasPrefix plaintextPrefix raw then292 removePrefix plaintextPrefix raw293 else if hasPrefix plaintextNewlinePrefix raw then294 removePrefix plaintextNewlinePrefix raw295 else296 throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}";297 };298299 inherit (strings) decodeRawSecret;300}1# Shared functions for fleet configuration, available as `fleet` module argument2{ lib }:3let4 inherit (lib.trivial) isFunction functionArgs;5 inherit (lib.options) mkOption mergeOneOption;6 inherit (lib.modules) mkOverride;7 inherit (lib.types)8 listOf9 submodule10 attrsOf11 mkOptionType12 ;13 inherit (lib.strings) optionalString hasPrefix removePrefix;14in15rec {16 types = {17 overlay = mkOptionType {18 name = "nixpkgs-overlay";19 description = "nixpkgs overlay";20 check = {21 __functor = _self: isFunction;22 isV2MergeCoherent = true;23 };24 merge = mergeOneOption;25 };26 listOfOverlay = listOf types.overlay;2728 mkHostsType = module: attrsOf (submodule module);29 mkDataType = module: submodule module;30 };3132 options = {33 mkHostsOption =34 module:35 mkOption {36 type = types.mkHostsType module;37 };38 mkDataOption =39 module:40 mkOption {41 type = types.mkDataType module;42 };43 };4445 inherit (options) mkHostsOption;4647 modules = {48 /**49 Use in places, where fleet might know better than nixpkgs defaults to50 */51 mkFleetDefault = mkOverride 999;52 /**53 Some generators use mkDefault, but optionDefault is set by nixpkgs.54 */55 mkFleetGeneratorDefault = mkOverride 1001;56 };5758 inherit (modules) mkFleetDefault mkFleetGeneratorDefault;5960 secrets = {6162 /**63 Generate a random secret password, 32 ascii characters by default6465 Options:66 size: generated password length in ascii characters (bytes).67 noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might68 not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].6970 Output:71 Resulting secret has only part: secret, which contains encrypted password.72 */73 mkPassword =74 {75 size ? 32,76 }:77 (78 {79 coreutils,80 mkSecretGenerator,81 }:82 mkSecretGenerator {83 script = ''84 mkdir $out85 gh generate password -o $out/secret --size ${toStringsize}86 '';87 parts.secret.encrypted = true;88 }89 );9091 /**92 Generate a random ed25519 keypair9394 Options:95 noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)96 When noEmbedPublis is enabled - only the private scalar is included.97 encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".9899 Output:100 Resulting secret has two parts: public and secret, where the secret part is encrypted.101102 This secret format is used by e.g Garage S3 server103 */104 mkEd25519 =105 {106 noEmbedPublic ? false,107 encoding ? null,108 }:109 (110 { mkSecretGenerator }:111 mkSecretGenerator {112 script = ''113 mkdir $out114 gh generate ed25519 -p $out/public -s $out/secret \115 ${optionalStringnoEmbedPublic"--no-embed-public"} \116 ${optionalString(encoding!=null)"--encoding=${encoding}"}117 '';118 parts.secret.encrypted = true;119 parts.public.encrypted = false;120 }121 );122123 /**124 Generate a random x25519 keypair125126 Options:127 encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".128129 Output:130 Resulting secret has two parts: public and secret, where the secret part is encrypted.131132 This secret format is used by e.g Wireguard VPN for peers (base64-encoded)133 */134 mkX25519 =135 {136 encoding ? null,137 }:138 (139 { mkSecretGenerator }:140 mkSecretGenerator {141 script = ''142 mkdir $out143 gh generate x25519 -p $out/public -s $out/secret \144 ${optionalString(encoding!=null)"--encoding=${encoding}"}145 '';146147 parts.secret.encrypted = true;148 parts.public.encrypted = false;149 }150 );151152 /**153 Generate a random RSA keypair154155 Options:156 size: RSA key size, 4096 by default157158 Output:159 Resulting secret has two parts: public and secret, where the secret part is encrypted.160 Both parts are PEM encoded.161 */162 mkRsa =163 {164 size ? 4096,165 }:166 (167 {168 openssl,169 mkSecretGenerator,170 }:171 mkSecretGenerator {172 script = ''173 mkdir $out174175 ${openssl}/bin/openssl genrsa -out rsa_private.key ${toStringsize}176 ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key177178 cat rsa_private.key | gh private -o $out/secret179 cat rsa_public.key | gh public -o $out/public180 '';181182 parts.secret.encrypted = true;183 parts.public.encrypted = false;184 }185 );186187 /**188 Generate a random byte sequence189190 Options:191 size: generated password length in bytes, 32 by default.192 encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"193 noNuls: prevent output byte sequence from containing internal \0, useful for some C applications194 that can't handle their strings properly.195196 Output:197 Resulting secret has only part: secret, which contains encrypted bytes.198199 Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)200 */201 mkBytes =202 {203 count ? 32,204 encoding,205 noNuls ? false,206 }:207 (208 { mkSecretGenerator }:209 mkSecretGenerator {210 script = ''211 mkdir $out212 gh generate bytes --count=${toStringcount} --encoding=${encoding} -o $out/secret \213 ${optionalStringnoNuls"--no-nuls"}214 '';215 parts.secret.encrypted = true;216 }217 );218 /**219 Shorthand for `mkBytes`, which defaults to "hex" encoding220 */221 mkHexBytes =222 {223 count ? 32,224 }:225 mkBytes {226 inherit count;227 encoding = "hex";228 };229 /**230 Shorthand for `mkBytes`, which defaults to "base64" encoding231 */232 mkBase64Bytes =233 {234 count ? 32,235 }:236 mkBytes {237 inherit count;238 encoding = "base64";239 };240241 # Wireguard242 # mkWireguard = {}: mkX25519 {encoding = "base64";};243 # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};244 };245246 inherit (secrets)247 mkPassword248 mkEd25519249 mkX25519250 mkRsa251 mkBytes252 mkHexBytes253 mkBase64Bytes254 ;255256 strings =257 let258 plaintextPrefix = "<PLAINTEXT>";259 plaintextNewlinePrefix = "<PLAINTEXT-NL>";260 in261 {262 /**263 Decode public secret part into string264 */265 decodeRawSecret =266 raw:267 if hasPrefix plaintextPrefix raw then268 removePrefix plaintextPrefix raw269 else if hasPrefix plaintextNewlinePrefix raw then270 removePrefix plaintextNewlinePrefix raw271 else272 throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}";273 };274275 inherit (strings) decodeRawSecret;276}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...