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.nixdiffbeforeafterboth1# Shared functions for fleet configuration, available as `fleet` module argument2{ lib }:3let4 inherit (lib.trivial) isFunction;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 = isFunction;21 merge = mergeOneOption;22 };23 listOfOverlay = listOf types.overlay;2425 mkHostsType = module: attrsOf (submodule module);26 mkDataType = module: submodule module;27 };2829 options = {30 mkHostsOption =31 module:32 mkOption {33 type = types.mkHostsType module;34 };35 mkDataOption =36 module:37 mkOption {38 type = types.mkDataType module;39 };40 };4142 inherit (options) mkHostsOption;4344 modules = {45 /**46 Use in places, where fleet might know better than nixpkgs defaults to47 */48 mkFleetDefault = mkOverride 999;49 /**50 Some generators use mkDefault, but optionDefault is set by nixpkgs.51 */52 mkFleetGeneratorDefault = mkOverride 1001;53 };5455 inherit (modules) mkFleetDefault mkFleetGeneratorDefault;5657 secrets =58 let59 describedGenerator =60 generator: {parts ? {}}:61 {parts = {};}62 // {63 __functor = generator;64 };65 in66 {67 inherit describedGenerator;6869 /**70 Generate a random secret password, 32 ascii characters by default7172 Options:73 size: generated password length in ascii characters (bytes).74 noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might75 not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].7677 Output:78 Resulting secret has only part: secret, which contains encrypted password.79 */80 mkPassword =81 {82 size ? 32,83 }:84 describedGenerator85 (86 {87 coreutils,88 mkSecretGenerator,89 }:90 mkSecretGenerator {91 script = ''92 mkdir $out93 gh generate password -o $out/secret --size ${toStringsize}94 '';95 }96 )97 {98 parts.secret.encrypted = true;99 };100101 /**102 Generate a random ed25519 keypair103104 Options:105 noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)106 When noEmbedPublis is enabled - only the private scalar is included.107 encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".108109 Output:110 Resulting secret has two parts: public and secret, where the secret part is encrypted.111112 This secret format is used by e.g Garage S3 server113 */114 mkEd25519 =115 {116 noEmbedPublic ? false,117 encoding ? null,118 }:119 describedGenerator120 (121 { mkSecretGenerator }:122 mkSecretGenerator {123 script = ''124 mkdir $out125 gh generate ed25519 -p $out/public -s $out/secret \126 ${optionalStringnoEmbedPublic"--no-embed-public"} \127 ${optionalString(encoding!=null)"--encoding=${encoding}"}128 '';129 }130 )131 {132 parts.secret.encrypted = true;133 parts.public.encrypted = false;134 };135136 /**137 Generate a random x25519 keypair138139 Options:140 encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".141142 Output:143 Resulting secret has two parts: public and secret, where the secret part is encrypted.144145 This secret format is used by e.g Wireguard VPN for peers (base64-encoded)146 */147 mkX25519 =148 {149 encoding ? null,150 }:151 describedGenerator152 (153 { mkSecretGenerator }:154 mkSecretGenerator {155 script = ''156 mkdir $out157 gh generate x25519 -p $out/public -s $out/secret \158 ${optionalString(encoding!=null)"--encoding=${encoding}"}159 '';160 }161 )162 {163 parts.secret.encrypted = true;164 parts.public.encrypted = false;165 };166167 /**168 Generate a random RSA keypair169170 Options:171 size: RSA key size, 4096 by default172173 Output:174 Resulting secret has two parts: public and secret, where the secret part is encrypted.175 Both parts are PEM encoded.176 */177 mkRsa =178 {179 size ? 4096,180 }:181 describedGenerator182 (183 {184 openssl,185 mkSecretGenerator,186 }:187 mkSecretGenerator {188 script = ''189 mkdir $out190191 ${openssl}/bin/openssl genrsa -out rsa_private.key ${toStringsize}192 ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key193194 cat rsa_private.key | gh private -o $out/secret195 cat rsa_public.key | gh public -o $out/public196 '';197 }198 )199 {200 parts.secret.encrypted = true;201 parts.public.encrypted = false;202 };203204 /**205 Generate a random byte sequence206207 Options:208 size: generated password length in bytes, 32 by default.209 encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"210 noNuls: prevent output byte sequence from containing internal \0, useful for some C applications211 that can't handle their strings properly.212213 Output:214 Resulting secret has only part: secret, which contains encrypted bytes.215216 Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)217 */218 mkBytes =219 {220 count ? 32,221 encoding,222 noNuls ? false,223 }:224 describedGenerator225 (226 { mkSecretGenerator }:227 mkSecretGenerator {228 script = ''229 mkdir $out230 gh generate bytes --count=${toStringcount} --encoding=${encoding} -o $out/secret \231 ${optionalStringnoNuls"--no-nuls"}232 '';233 }234 )235 {236 parts.secret.encrypted = true;237 };238 /**239 Shorthand for `mkBytes`, which defaults to "hex" encoding240 */241 mkHexBytes =242 {243 count ? 32,244 }:245 mkBytes {246 inherit count;247 encoding = "hex";248 };249 /**250 Shorthand for `mkBytes`, which defaults to "base64" encoding251 */252 mkBase64Bytes =253 {254 count ? 32,255 }:256 mkBytes {257 inherit count;258 encoding = "base64";259 };260261 # Wireguard262 # mkWireguard = {}: mkX25519 {encoding = "base64";};263 # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};264 };265266 inherit (secrets)267 mkPassword268 mkEd25519269 mkX25519270 mkRsa271 mkBytes272 mkHexBytes273 mkBase64Bytes274 ;275276 strings =277 let278 plaintextPrefix = "<PLAINTEXT>";279 plaintextNewlinePrefix = "<PLAINTEXT-NL>";280 in281 {282 /**283 Decode public secret part into string284 */285 decodeRawSecret =286 raw:287 if hasPrefix plaintextPrefix raw then288 removePrefix plaintextPrefix raw289 else if hasPrefix plaintextNewlinePrefix raw then290 removePrefix plaintextNewlinePrefix raw291 else292 throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}";293 };294295 inherit (strings) decodeRawSecret;296}modules/nixos/secrets.nixdiffbeforeafterboth--- 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" ];