difftreelog
feat infer secret parts from generator
in: trunk
3 files changed
cmds/install-secrets/src/main.rsdiffbeforeafterboth--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -58,7 +58,6 @@
owner: String,
root_path: Option<PathBuf>,
- #[serde(flatten)]
parts: BTreeMap<String, Part>,
}
lib/default.nixdiffbeforeafterboth--- 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
modules/nixos/secrets.nixdiffbeforeafterboth1{2 lib,3 fleetLib,4 config,5 pkgs,6 ...7}:8let9 inherit (builtins) hashString elemAt length toJSON filter;10 inherit (lib.stringsWithDeps) stringAfter;11 inherit (lib.options) mkOption literalExpression;12 inherit (lib.lists) optional;13 inherit (lib.attrsets) mapAttrs mapAttrsToList;14 inherit (lib.modules) mkIf;15 inherit (lib.types)16 submodule17 str18 attrsOf19 nullOr20 unspecified21 lazyAttrsOf22 uniq23 functionTo24 package25 listOf26 ;27 inherit (fleetLib.strings) decodeRawSecret;2829 sysConfig = config;30 secretPartDataType = submodule {31 options = {32 raw = mkOption {33 type = str;34 internal = true;35 description = "Encoded & Encrypted secret part data, passed from fleet.nix";36 };37 };38 };39 secretDataType = submodule {40 freeformType = lazyAttrsOf secretPartDataType;41 options = {42 shared = mkOption {43 description = "Is this secret owned by this machine, or propagated from shared secrets";44 default = false;45 };46 };47 };48 secretPartType =49 secretName:50 submodule (51 { config, ... }:52 let53 partName = config._module.args.name;54 in55 {56 options = {57 hash = mkOption {58 type = str;59 description = "Hash of secret in encoded format";60 };61 path = mkOption {62 type = str;63 description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)";64 };65 stablePath = mkOption {66 type = str;67 description = "Path to secret part, stable path (users are expected to watch for file changes/re-read secret on demand)";68 };69 data = mkOption {70 type = str;71 description = "Secret public data (only available for plaintext)";72 };73 };74 config =75 let76 raw = sysConfig.data.secrets.${secretName}.${partName}.raw;77 in78 {79 hash = hashString "sha1" raw;80 data = decodeRawSecret raw;81 path = "/run/secrets/${secretName}/${config.hash}-${partName}";82 stablePath = "/run/secrets/${secretName}/${partName}";83 };84 }85 );86 secretType = submodule (87 {88 config,89 loc,90 options,91 ...92 }:93 let94 secretName =95 # Due to config definition for freeformType, we can't just use _module.args due to infinite recursion, instead96 # extract the secret name the ugly way...97 let98 saLoc = options._module.specialArgs.loc;99 comp = elemAt saLoc;100 in101 assert102 (length saLoc == 2 ||103 length saLoc == 4 &&104 comp 0 == "secrets" && comp 2 == "_module" && comp 3 == "specialArgs") ||105 throw "Unexpected module structure ${toJSONsaLoc}";106 if length saLoc == 2 then "documentation generator stub" else comp 1;107 in108 {109 freeformType = lazyAttrsOf (secretPartType secretName);110 options = {111 generator = mkOption {112 type = uniq (nullOr (functionTo package));113 description = "Derivation to evaluate for secret generation";114 default = null;115 };116 mode = mkOption {117 type = str;118 description = "Secret mode";119 default = "0440";120 };121 owner = mkOption {122 type = str;123 description = "Owner of the secret";124 default = "root";125 };126 group = mkOption {127 type = str;128 description = "Group of the secret";129 default = sysConfig.users.users.${config.owner}.group;130 defaultText = literalExpression "config.users.users.$${owner}.group";131 };132 expectedGenerationData = mkOption {133 type = unspecified;134 description = "Data that gets embedded into secret part";135 default = null;136 };137 expectedPrivateParts = mkOption {138 type = listOf str;139 default = [ ];140 description = "List of parts that are expected to be encrypted";141 };142 expectedPublicParts = mkOption {143 type = listOf str;144 default = [ ];145 description = "List of parts that are expected to be public";146 };147 };148 config = mapAttrs (_: _: { }) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) [ "shared" ]);149 }150 );151 processPart = secretName: partName: part: {152 inherit (part) path stablePath;153 raw = config.data.secrets.${secretName}.${partName}.raw;154 };155 processSecret =156 secretName: secret:157 {158 inherit (secret) group mode owner;159 }160 // (mapAttrs (processPart secretName) (161 removeAttrs secret [162 "shared"163 "generator"164 "mode"165 "group"166 "owner"167 "expectedGenerationData"168 "expectedPrivateParts"169 "expectedPublicParts"170 ]171 ));172 secretsData = (mapAttrs (processSecret) config.secrets);173 secretsFile = pkgs.writeTextFile {174 name = "secrets.json";175 text = toJSON secretsData;176 };177 useSysusers =178 (config.systemd ? sysusers && config.systemd.sysusers.enable)179 || (config ? userborn && config.userborn.enable);180in181{182 options = {183 data.secrets = mkOption {184 type = attrsOf secretDataType;185 default = { };186 description = "Host-local secret data";187 };188 secrets = mkOption {189 type = attrsOf secretType;190 default = { };191 description = "Host-local secrets";192 };193 system.secretsData = mkOption {194 type = unspecified;195 default = {};196 description = "secrets.json contents";197 };198 };199 config = {200 system = {inherit secretsData;};201 environment.systemPackages = [ pkgs.fleet-install-secrets ];202203 warnings = filter (v: v!=null) (mapAttrsToList (204 name: secret:205 if206 secret.expectedPrivateParts == [ ]207 && secret.expectedPublicParts == [ ]208 && !(config.data.secrets.${name} or { shared = false; }).shared209 then210 "Secret ${name} has no expected parts defined, this is deprecated for better visibility"211 else212 null213 ) config.secrets);214215 systemd.services.fleet-install-secrets = mkIf useSysusers {216 wantedBy = [ "sysinit.target" ];217 after = [ "systemd-sysusers.service" ];218 restartTriggers = [219 secretsFile220 ];221 aliases = [222 "sops-install-secrets"223 "agenix-install-secrets"224 ];225226 unitConfig.DefaultDependencies = false;227228 serviceConfig = {229 Type = "oneshot";230 RemainAfterExit = true;231 ExecStart = "${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}";232 };233 };234 system.activationScripts.decryptSecrets = mkIf (!useSysusers) (235 stringAfter236 (237 [238 # secrets are owned by user/group, thus we need to refer to those239 "users"240 "groups"241 "specialfs"242 ]243 # nixos-impermanence compatibility: secrets are encrypted by host-key,244 # but with impermanence we expect that the host-key is installed by245 # persist-file activation script.246 ++ (optional (config.system.activationScripts ? "persist-files") "persist-files")247 )248 ''249 1>&2 echo "setting up secrets"250 ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}251 ''252 );253 };254}1{2 lib,3 fleetLib,4 config,5 pkgs,6 ...7}:8let9 inherit (builtins)10 hashString11 elemAt12 length13 toJSON14 filter15 ;16 inherit (lib.stringsWithDeps) stringAfter;17 inherit (lib.options) mkOption literalExpression;18 inherit (lib.lists) optional;19 inherit (lib.attrsets) mapAttrs mapAttrsToList;20 inherit (lib.modules) mkIf mkMerge;21 inherit (lib.types)22 submodule23 str24 attrsOf25 nullOr26 unspecified27 lazyAttrsOf28 uniq29 functionTo30 package31 listOf32 bool33 ;34 inherit (fleetLib.strings) decodeRawSecret;3536 sysConfig = config;37 secretPartDataType = submodule {38 options = {39 raw = mkOption {40 type = str;41 internal = true;42 description = "Encoded & Encrypted secret part data, passed from fleet.nix";43 };44 };45 };46 secretDataType = submodule {47 freeformType = lazyAttrsOf secretPartDataType;48 options = {49 shared = mkOption {50 description = "Is this secret owned by this machine, or propagated from shared secrets";51 default = false;52 };53 };54 };55 secretPartType =56 secretName:57 submodule (58 { config, ... }:59 let60 partName = config._module.args.name;61 in62 {63 options = {64 encrypted = mkOption {65 type = bool;66 description = "Is this secret part supposed to be encrypted?";67 };6869 hash = mkOption {70 type = str;71 description = "Hash of secret in encoded format";72 };73 path = mkOption {74 type = str;75 description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)";76 };77 stablePath = mkOption {78 type = str;79 description = "Path to secret part, stable path (users are expected to watch for file changes/re-read secret on demand)";80 };81 data = mkOption {82 type = str;83 description = "Secret public data (only available for plaintext)";84 };85 };86 config =87 let88 raw = sysConfig.data.secrets.${secretName}.${partName}.raw;89 in90 {91 hash = hashString "sha1" raw;92 data = decodeRawSecret raw;93 path = "/run/secrets/${secretName}/${config.hash}-${partName}";94 stablePath = "/run/secrets/${secretName}/${partName}";95 };96 }97 );98 secretType = submodule (99 {100 config,101 ...102 }:103 let104 secretName = config._module.args.name;105 in106 {107 options = {108 parts = mkOption {109 type = lazyAttrsOf (secretPartType secretName);110 description = "Definition of secret parts";111 default = {};112 };113 generator = mkOption {114 type = uniq (nullOr (functionTo package));115 description = "Derivation to evaluate for secret generation";116 default = null;117 };118 mode = mkOption {119 type = str;120 description = "Secret mode";121 default = "0440";122 };123 owner = mkOption {124 type = str;125 description = "Owner of the secret";126 default = "root";127 };128 group = mkOption {129 type = str;130 description = "Group of the secret";131 default = sysConfig.users.users.${config.owner}.group;132 defaultText = literalExpression "config.users.users.$${owner}.group";133 };134 expectedGenerationData = mkOption {135 type = unspecified;136 description = "Data that gets embedded into secret part";137 default = null;138 };139 };140 config.parts = mkMerge [141 (mkIf (config.generator != null && config.generator ? parts) config.generator.parts)142 (mapAttrs (_: _: {}) (removeAttrs sysConfig.data.secrets.${secretName} ["shared"]))143 ];144 }145 );146 processPart = secretName: partName: part: {147 inherit (part) path stablePath;148 raw = config.data.secrets.${secretName}.${partName}.raw;149 };150 processSecret =151 secretName: secret:152 {153 inherit (secret.definition) group mode owner;154 parts = (mapAttrs (processPart secretName) (155 secret.definition.parts156 ));157 };158 secretsData = (mapAttrs (processSecret) config.secrets);159 secretsFile = pkgs.writeTextFile {160 name = "secrets.json";161 text = toJSON secretsData;162 };163 useSysusers =164 (config.systemd ? sysusers && config.systemd.sysusers.enable)165 || (config ? userborn && config.userborn.enable);166in167{168 options = {169 data.secrets = mkOption {170 type = attrsOf secretDataType;171 default = { };172 description = "Host-local secret data";173 };174 secrets = mkOption {175 type = attrsOf secretType;176 default = { };177 apply = v: (mapAttrs (_: secret: secret.parts // {definition = secret;}) v);178 description = "Host-local secrets";179 };180 system.secretsData = mkOption {181 type = unspecified;182 default = { };183 description = "secrets.json contents";184 };185 };186 config = {187 system = { inherit secretsData; };188 environment.systemPackages = [ pkgs.fleet-install-secrets ];189190 systemd.services.fleet-install-secrets = mkIf useSysusers {191 wantedBy = [ "sysinit.target" ];192 after = [ "systemd-sysusers.service" ];193 restartTriggers = [194 secretsFile195 ];196 aliases = [197 "sops-install-secrets"198 "agenix-install-secrets"199 ];200201 unitConfig.DefaultDependencies = false;202203 serviceConfig = {204 Type = "oneshot";205 RemainAfterExit = true;206 ExecStart = "${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}";207 };208 };209 system.activationScripts.decryptSecrets = mkIf (!useSysusers) (210 stringAfter211 (212 [213 # secrets are owned by user/group, thus we need to refer to those214 "users"215 "groups"216 "specialfs"217 ]218 # nixos-impermanence compatibility: secrets are encrypted by host-key,219 # but with impermanence we expect that the host-key is installed by220 # persist-file activation script.221 ++ (optional (config.system.activationScripts ? "persist-files") "persist-files")222 )223 ''224 1>&2 echo "setting up secrets"225 ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}226 ''227 );228 };229}