From 5b343db89280e0d8cf258a96a48dbf8fee5dca13 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sun, 26 Oct 2025 21:18:50 +0000 Subject: [PATCH] feat: infer secret parts from generator --- --- a/cmds/install-secrets/src/main.rs +++ b/cmds/install-secrets/src/main.rs @@ -58,7 +58,6 @@ owner: String, root_path: Option, - #[serde(flatten)] parts: BTreeMap, } --- a/lib/default.nix +++ b/lib/default.nix @@ -54,170 +54,214 @@ inherit (modules) mkFleetDefault mkFleetGeneratorDefault; - secrets = { - /** - Generate a random secret password, 32 ascii characters by default + secrets = + let + describedGenerator = + generator: {parts ? {}}: + {parts = {};} + // { + __functor = generator; + }; + in + { + inherit describedGenerator; - 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]. + /** + Generate a random secret password, 32 ascii characters by default - Output: - Resulting secret has only part: secret, which contains encrypted password. - */ - mkPassword = - { - size ? 32, - }: - { - coreutils, - mkSecretGenerator, - }: - mkSecretGenerator { - script = '' - mkdir $out - gh generate password -o $out/secret --size ${toString size} - ''; - }; + 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]. - /** - Generate a random ed25519 keypair + Output: + Resulting secret has only part: secret, which contains encrypted password. + */ + mkPassword = + { + size ? 32, + }: + describedGenerator + ( + { + coreutils, + mkSecretGenerator, + }: + mkSecretGenerator { + script = '' + mkdir $out + gh generate password -o $out/secret --size ${toString size} + ''; + } + ) + { + parts.secret.encrypted = true; + }; + + /** + 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}"} - ''; - }; + 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; + }; - /** - 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 = - { - size ? 4096, - }: - { - openssl, - mkSecretGenerator, - }: - mkSecretGenerator { - script = '' - mkdir $out + Output: + Resulting secret has two parts: public and secret, where the secret part is encrypted. + Both parts are PEM encoded. + */ + mkRsa = + { + size ? 4096, + }: + describedGenerator + ( + { + openssl, + mkSecretGenerator, + }: + 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; + }; - /** - 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, - }: - { mkSecretGenerator }: - mkSecretGenerator { - script = '' - mkdir $out - gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \ - ${optionalString noNuls "--no-nuls"} - ''; - }; - /** - 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, + }: + 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"; + }; - # Wireguard - # mkWireguard = {}: mkX25519 {encoding = "base64";}; - # mkWireguardPsk = {}: mkBase64Bytes {count = 32;}; - }; + # Wireguard + # mkWireguard = {}: mkX25519 {encoding = "base64";}; + # mkWireguardPsk = {}: mkBase64Bytes {count = 32;}; + }; inherit (secrets) mkPassword --- a/modules/nixos/secrets.nix +++ b/modules/nixos/secrets.nix @@ -6,12 +6,18 @@ ... }: let - inherit (builtins) hashString elemAt length toJSON filter; + 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; + inherit (lib.modules) mkIf mkMerge; inherit (lib.types) submodule str @@ -23,6 +29,7 @@ functionTo package listOf + bool ; inherit (fleetLib.strings) decodeRawSecret; @@ -54,6 +61,11 @@ 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"; @@ -86,28 +98,18 @@ secretType = submodule ( { config, - loc, - options, ... }: let - secretName = - # Due to config definition for freeformType, we can't just use _module.args due to infinite recursion, instead - # extract the secret name the ugly way... - let - saLoc = options._module.specialArgs.loc; - comp = elemAt saLoc; - in - assert - (length saLoc == 2 || - length saLoc == 4 && - comp 0 == "secrets" && comp 2 == "_module" && comp 3 == "specialArgs") || - throw "Unexpected module structure ${toJSON saLoc}"; - if length saLoc == 2 then "documentation generator stub" else comp 1; + secretName = config._module.args.name; in { - freeformType = lazyAttrsOf (secretPartType secretName); options = { + parts = mkOption { + type = lazyAttrsOf (secretPartType secretName); + description = "Definition of secret parts"; + default = {}; + }; generator = mkOption { type = uniq (nullOr (functionTo package)); description = "Derivation to evaluate for secret generation"; @@ -134,18 +136,11 @@ description = "Data that gets embedded into secret part"; 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"; - }; }; - config = mapAttrs (_: _: { }) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) [ "shared" ]); + config.parts = mkMerge [ + (mkIf (config.generator != null && config.generator ? parts) config.generator.parts) + (mapAttrs (_: _: {}) (removeAttrs sysConfig.data.secrets.${secretName} ["shared"])) + ]; } ); processPart = secretName: partName: part: { @@ -155,20 +150,11 @@ processSecret = secretName: secret: { - inherit (secret) group mode owner; - } - // (mapAttrs (processPart secretName) ( - removeAttrs secret [ - "shared" - "generator" - "mode" - "group" - "owner" - "expectedGenerationData" - "expectedPrivateParts" - "expectedPublicParts" - ] - )); + inherit (secret.definition) group mode owner; + parts = (mapAttrs (processPart secretName) ( + secret.definition.parts + )); + }; secretsData = (mapAttrs (processSecret) config.secrets); secretsFile = pkgs.writeTextFile { name = "secrets.json"; @@ -188,29 +174,18 @@ secrets = mkOption { type = attrsOf secretType; default = { }; + apply = v: (mapAttrs (_: secret: secret.parts // {definition = secret;}) v); description = "Host-local secrets"; }; system.secretsData = mkOption { type = unspecified; - default = {}; + default = { }; description = "secrets.json contents"; }; }; config = { - system = {inherit secretsData;}; + system = { inherit secretsData; }; environment.systemPackages = [ pkgs.fleet-install-secrets ]; - - warnings = filter (v: v!=null) (mapAttrsToList ( - name: secret: - if - secret.expectedPrivateParts == [ ] - && secret.expectedPublicParts == [ ] - && !(config.data.secrets.${name} or { shared = false; }).shared - then - "Secret ${name} has no expected parts defined, this is deprecated for better visibility" - else - null - ) config.secrets); systemd.services.fleet-install-secrets = mkIf useSysusers { wantedBy = [ "sysinit.target" ]; -- gitstuff