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.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.nixdiffbeforeafterboth1{2 lib,3 config,4 ...5}:6let7 inherit (lib.options) mkOption literalExpression;8 inherit (lib.types)9 unspecified10 nullOr11 listOf12 str13 bool14 attrsOf15 submodule16 functionTo17 package18 uniq19 ;20 inherit (lib.strings) concatStringsSep;21 inherit (lib.attrsets) mapAttrs;2223 sharedSecret =24 { config, ... }:25 {26 options = {27 expectedOwners = mkOption {28 type = nullOr (listOf str);29 description = ''30 Specifies the list of hosts authorized to decrypt and access this shared secret.3132 When null, secret ownership is managed manually via fleet.nix and CLI.33 Decrypted secrets will be stored at /run/secrets/$\{name} on authorized hosts.34 '';35 default = null;36 };37 regenerateOnOwnerAdded = mkOption {38 type = bool;39 description = ''40 Controls whether the secret must be regenerated when new owners are added.4142 Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).43 When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.44 '';45 };46 regenerateOnOwnerRemoved = mkOption {47 default = config.regenerateOnOwnerAdded;48 defaultText = literalExpression "regenerateOnOwnerAdded";49 type = bool;50 description = ''51 Determines secret behavior when owners are removed from the configuration.5253 Typically mirrors regenerateOnOwnerAdded. Override cautiously.54 Set to false if host permissions are revoked through alternative mechanisms like firewall rules.55 '';56 };57 generator = mkOption {58 type = uniq (nullOr (functionTo package));59 description = ''60 Function evaluating to nix derivation responsible for (re)generating the secret's content.6162 An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,63 use `mkSecretGenerator` helpers to implement own generators.64 '';65 default = null;66 };67 expectedGenerationData = mkOption {68 type = unspecified;69 description = "Contextual metadata embedded within the secret part value";70 default = null;71 };72 expectedPrivateParts = mkOption {73 type = listOf str;74 default = [ ];75 description = "List of parts that are expected to be encrypted";76 };77 expectedPublicParts = mkOption {78 type = listOf str;79 default = [ ];80 description = "List of parts that are expected to be public";81 };82 };83 };84in85{86 options = {87 sharedSecrets = mkOption {88 type = attrsOf (submodule sharedSecret);89 default = { };90 description = "Collection of secrets shared across multiple hosts with configurable ownership";91 };92 };93 config = {94 hosts = mapAttrs (95 _: secretMap:96 let97 partsOf =98 s:99 removeAttrs s [100 "createdAt"101 "expiresAt"102 "generationData"103 ];104105 in106 {107 nixos.data.secrets = mapAttrs (_: s: partsOf s) secretMap;108 # nixos.secrets = mapAttrs (109 # _: s: mapAttrs (_: _: {}) (partsOf s)110 # ) secretMap;111 }112 ) config.data.hostSecrets;113 nixpkgs.overlays = [114 (final: prev: {115 mkSecretGenerators =116 { recipients }:117 rec {118 # TODO: Merge both generators to one with consistent options syntax?119 # Impure generator is built on local machine, then built closure is copied to remote machine,120 # and then it is ran in inpure context, so that this generator may access HSMs and other things.121 mkImpureSecretGenerator =122 {123 script,124 # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD125 # (Some secrets-encryption-in-git/managed PKI solution is expected)126 impureOn ? null,127 }:128 (prev.writeShellScript "impureGenerator.sh" ''129 #!/bin/sh130 set -eu131132 export GENERATOR_HELPER_IDENTITIES="${concatStringsSep"\n"recipients}";133 export PATH=${final.fleet-generator-helper}/bin:$PATH134135 # TODO: Provide tempdir from outside, to make it securely erasurable as needed?136 tmp=mktemp-d137 cd $tmp138 # cd /var/empty139140 created_at=date-u"%Y-%m-%dT%H:%M:%S.%NZ"141142 ${script}143144 if ! test -d $out; then145 echo "impure generator script did not produce expected \$out output"146 exit 1147 fi148149 echo -n $created_at > $out/created_at150 echo -n SUCCESS > $out/marker151 '').overrideAttrs152 (old: {153 passthru = {154 inherit impureOn;155 generatorKind = "impure";156 };157 });158 # Pure generators are disabled for now159 mkSecretGenerator = { script }: mkImpureSecretGenerator { inherit script; };160161 # TODO: Implement consistent naming162 # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...163 # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.164 # mkSecretGenerator = {script}:165 # (prev.writeShellScript "generator.sh" ''166 # #!/bin/sh167 # set -eu168 # # TODO: make nix daemon build secret, not just the script.169 # cd /var/empty170 #171 # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")172 #173 # ${script}174 # if ! test -d $out; then175 # echo "impure generator script did not produce expected \$out output"176 # exit 1177 # fi178 #179 # echo -n $created_at > $out/created_at180 # echo -n SUCCESS > $out/marker181 # '')182 # .overrideAttrs (old: {183 # passthru = {184 # generatorKind = "pure";185 # };186 # # TODO: make nix daemon build secret, not just the script.187 # # __impure = true;188 # });189 };190 })191 ];192 };193}