difftreelog
doc: tried to improve docs, realised I'm still too lazy for that
in: trunk
5 files changed
docs/secrets.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/secrets.adoc
@@ -0,0 +1,35 @@
+= Fleet Secrets Management System
+
+== Overview
+
+Secret management system is a built-in way to deploy secrets to remote systems, similar to agenix and other similar systems.
+
+Secrets are encrypted using system's host ssh key (/etc/ssh/ssh_host_ed25519_key), which is not required to build the
+remote system/add secret to fleet configuration, fleet users are encrypting secrets using received public key instead,
+they don't need the root access to receive the public encryption key.
+
+== Example
+
+[source,nix]
+----
+{
+ fleet.secrets = {
+ "my-secret" = {
+ expectedOwners = [ "host1" "host2" ];
+ regenerateOnOwnerAdded = true;
+ generator = {mkImpureSecretGenerator}:
+ mkImpureSecretGenerator {
+ script = ''
+ echo "secret content" | gh private -o $out/secret
+ '';
+ };
+ };
+ }
+}
+----
+
+== Limitations and Future Improvements
+
+- Pure secret generators are currently disabled
+- Support for other secret management systems (e.g systemd-creds has planned asymmetric encryption support)
+
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -33,16 +33,30 @@
inherit (options) mkHostsOption;
modules = {
- # mkDefault = mkOverride 1000
- # For places, where fleet knows better than nixpkgs defaults.
+ /**
+ Use in places, where fleet might know better than nixpkgs defaults to
+ */
mkFleetDefault = mkOverride 999;
- # Some generators use mkDefault, but optionDefault is set by nixpkgs.
+ /**
+ Some generators use mkDefault, but optionDefault is set by nixpkgs.
+ */
mkFleetGeneratorDefault = mkOverride 1001;
};
inherit (modules) mkFleetDefault mkFleetGeneratorDefault;
secrets = {
+ /**
+ 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].
+
+ Output:
+ Resulting secret has only part: secret, which contains encrypted password.
+ */
mkPassword = {size ? 32}: {
coreutils,
mkSecretGenerator,
@@ -54,6 +68,19 @@
'';
};
+ /**
+ 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".
+
+ 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,
@@ -67,6 +94,17 @@
'';
};
+ /**
+ Generate a random x25519 keypair
+
+ 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.
+
+ This secret format is used by e.g Wireguard VPN for peers (base64-encoded)
+ */
mkX25519 = {encoding ? null}: {mkSecretGenerator}:
mkSecretGenerator {
script = ''
@@ -76,6 +114,16 @@
'';
};
+ /**
+ Generate a random RSA keypair
+
+ 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,
@@ -92,6 +140,20 @@
'';
};
+ /**
+ 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.
+
+ 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,
@@ -104,11 +166,17 @@
${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;
@@ -126,6 +194,9 @@
plaintextPrefix = "<PLAINTEXT>";
plaintextNewlinePrefix = "<PLAINTEXT-NL>";
in {
+ /**
+ Decode public secret part into string
+ */
decodeRawSecret = raw:
if hasPrefix plaintextPrefix raw
then removePrefix plaintextPrefix raw
modules/hosts.nixdiffbeforeafterboth--- a/modules/hosts.nix
+++ b/modules/hosts.nix
@@ -11,6 +11,8 @@
inherit (lib.attrsets) mapAttrsToList mapAttrs;
inherit (lib.lists) flatten groupBy;
in {
+ # Fleet Meta Configuration Module
+
options = {
data = mkOption {
type = mkDataType {
@@ -18,74 +20,94 @@
version = mkOption {
type = str;
internal = true;
+ description = "Internal version identifier for saved fleet state";
};
+
gcRootPrefix = mkOption {
type = str;
internal = true;
+ description = "Prefix for fleet-generated gc garbage collection roots";
};
+
hosts = mkOption {
type = attrsOf (submodule {
options.encryptionKey = mkOption {
type = str;
- description = "Rage SSH encryption key for secrets.";
+ description = "Rage SSH encryption key for host-bound secrets";
};
});
};
};
};
description = ''
- Configuration provided from outside.
- Usually used to persist fleet data between runs.
+ Persistent configuration data for fleet management.
+ Typically used to maintain state between fleet configuration runs.
'';
};
+
taggedWith = mkOption {
type = attrsOf (listOf str);
internal = true;
+ description = "Mapping of hosts grouped by tags, used by fleet CLI";
};
+
hosts = mkOption {
type = mkHostsType ({config, ...}: {
options = {
system = mkOption {
- description = "Type of the system.";
+ description = "System architecture and platform identifier";
type = str;
example = "x86_64-linux";
};
+
tags = mkOption {
- description = "Host tag. In CLI, you can refer to all hosts having this tag using @tag syntax.";
+ description = ''
+ Tags for host classification.
+ Used for host selection via @tag syntax in CLI tools.
+ '';
type = listOf str;
};
+
+ # Network configuration details
network = mkOption {
type = submodule {
options = {
internalIps = mkOption {
- description = "Internal ips";
+ description = "List of internal IP addresses for the host";
type = listOf str;
default = [];
};
+
externalIps = mkOption {
- description = "External ips";
+ description = "List of external IP addresses for the host";
type = listOf str;
default = [];
};
};
};
- description = "Network definition of host";
};
};
config = {
+ # Default hostname generation
nixos.networking.hostName = mkFleetGeneratorDefault config._module.args.name;
+ # Default 'all' tag for every host
tags = ["all"];
};
_file = ./meta.nix;
});
default = {};
- description = "Configurations of individual hosts";
};
};
+
+ # Generate a mapping of hosts indexed by their tags
config.taggedWith = let
+ # Flatten host tags into a list of {hostname, tag} pairs
hostTagList = flatten (mapAttrsToList (hostname: host: map (tag: {inherit hostname tag;}) host.tags) config.hosts);
+ # Group hostnames by their tags
grouped = mapAttrs (_: hosts: lib.map (pair: pair.hostname) hosts) (groupBy (elem: elem.tag) hostTagList);
in
grouped;
+
+ # Source file reference
_file = ./meta.nix;
}
modules/secrets-data.nixdiffbeforeafterboth--- a/modules/secrets-data.nix
+++ b/modules/secrets-data.nix
@@ -15,7 +15,7 @@
options = {
raw = mkOption {
type = nullOr str;
- description = "Encrypted + encoded secret data";
+ description = "Raw secret data in unspecified encoded and optionally encrypted format.";
default = null;
};
};
@@ -26,29 +26,28 @@
options = {
createdAt = mkOption {
type = str;
- description = "When this secret was (re)generated";
+ description = "Timestamp of secret generation/last rotation.";
default = null;
};
expiresAt = mkOption {
type = nullOr str;
- description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
+ description = "Expiration timestamp triggering mandatory secret rotation.";
default = null;
};
owners = mkOption {
type = listOf str;
description = ''
- For which owners this secret is currently encrypted,
- if not matches expectedOwners - then this secret is considered outdated, and
- should be regenerated/reencrypted.
+ List of hosts currently authorized to decrypt this shared secret.
- Imported from fleet.nix
+ If owners differ from expected owners, the secret is considered outdated
+ and requires regeneration or re-encryption.
'';
default = [];
};
generationData = mkOption {
type = unspecified;
- description = "Data that is embedded into secret part";
+ description = "Contextual metadata associated with secret part.";
default = null;
};
};
@@ -60,22 +59,22 @@
options = {
createdAt = mkOption {
type = str;
- description = "When this secret was (re)generated";
+ description = "Timestamp of secret generation/last rotation.";
default = null;
};
expiresAt = mkOption {
type = nullOr str;
- description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
+ description = "Expiration timestamp triggering mandatory secret rotation.";
default = null;
};
shared = mkOption {
type = bool;
- description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
+ description = "Indicates if secret is a shared secret, so other hosts might have the same piece of secret data.";
default = false;
};
generationData = mkOption {
type = unspecified;
- description = "Data that is embedded into secret part";
+ description = "Contextual metadata associated with secret part.";
default = null;
};
};
@@ -87,12 +86,12 @@
sharedSecrets = mkOption {
type = attrsOf (submodule sharedSecretData);
default = {};
- description = "Stored shared secret data.";
+ description = "Shared secret data.";
};
hostSecrets = mkOption {
type = attrsOf (attrsOf (submodule hostSecretData));
default = {};
- description = "Host secrets.";
+ description = "Host-specific secrets.";
internal = true;
};
};
modules/secrets.nixdiffbeforeafterboth1{2 lib,3 config,4 ...5}: let6 inherit (lib.options) mkOption literalExpression;7 inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule;8 inherit (lib.strings) concatStringsSep;9 inherit (lib.attrsets) mapAttrs;1011 sharedSecret = {config, ...}: {12 options = {13 expectedOwners = mkOption {14 type = nullOr (listOf str);15 description = ''16 List of hosts to encrypt secret for. null if managed by user (= via owners field from fleet.nix)1718 Secrets would be decrypted and stored to /run/secrets/$\{name} on owners19 '';20 default = null;21 };22 # TODO: Aren't those options may be just desugared to data/expectedData?23 regenerateOnOwnerAdded = mkOption {24 type = bool;25 description = ''26 Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted.2728 You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs.29 '';30 };31 regenerateOnOwnerRemoved = mkOption {32 default = config.regenerateOnOwnerAdded;33 defaultText = literalExpression "regenerateOnOwnerAdded";34 type = bool;35 description = ''36 Should this secret be removed on owner removal, or it may be just reencrypted3738 Most probably its value should be equal to regenerateOnOwnerAdded, override only if you know what are you doing.39 Contrary to regenerateOnOwnerAdded, you may want to set this option to false, when host permissions are revoked40 in some other way than by this secret ownership, I.e by firewall/etc.41 '';42 };43 generator = mkOption {44 type = nullOr unspecified;45 description = "Derivation to evaluate for secret generation";46 default = null;47 };48 expectedGenerationData = mkOption {49 type = unspecified;50 description = "Data that gets embedded into secret part";51 default = null;52 };53 };54 };55in {56 options = {57 sharedSecrets = mkOption {58 type = attrsOf (submodule sharedSecret);59 default = {};60 description = "Shared secrets";61 };62 };63 config = {64 hosts =65 mapAttrs (_: secretMap: {66 nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt" "generationData"]) secretMap;67 })68 config.data.hostSecrets;69 nixpkgs.overlays = [70 (final: prev: {71 mkSecretGenerators = {recipients}: rec {72 # TODO: Merge both generators to one with consistent options syntax?73 # Impure generator is built on local machine, then built closure is copied to remote machine,74 # and then it is ran in inpure context, so that this generator may access HSMs and other things.75 mkImpureSecretGenerator = {76 script,77 # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD78 # (Some secrets-encryption-in-git/managed PKI solution is expected)79 impureOn ? null,80 }:81 (prev.writeShellScript "impureGenerator.sh" ''82 #!/bin/sh83 set -eu8485 export GENERATOR_HELPER_IDENTITIES="${concatStringsSep"\n"recipients}";86 export PATH=${final.fleet-generator-helper}/bin:$PATH8788 # TODO: Provide tempdir from outside, to make it securely erasurable as needed?89 tmp=mktemp-d90 cd $tmp91 # cd /var/empty9293 created_at=date-u"%Y-%m-%dT%H:%M:%S.%NZ"9495 ${script}9697 if ! test -d $out; then98 echo "impure generator script did not produce expected \$out output"99 exit 1100 fi101102 echo -n $created_at > $out/created_at103 echo -n SUCCESS > $out/marker104 '')105 .overrideAttrs (old: {106 passthru = {107 inherit impureOn;108 generatorKind = "impure";109 };110 });111 # Pure generators are disabled for now112 mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;};113114 # TODO: Implement consistent naming115 # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...116 # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.117 # mkSecretGenerator = {script}:118 # (prev.writeShellScript "generator.sh" ''119 # #!/bin/sh120 # set -eu121 # # TODO: make nix daemon build secret, not just the script.122 # cd /var/empty123 #124 # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")125 #126 # ${script}127 # if ! test -d $out; then128 # echo "impure generator script did not produce expected \$out output"129 # exit 1130 # fi131 #132 # echo -n $created_at > $out/created_at133 # echo -n SUCCESS > $out/marker134 # '')135 # .overrideAttrs (old: {136 # passthru = {137 # generatorKind = "pure";138 # };139 # # TODO: make nix daemon build secret, not just the script.140 # # __impure = true;141 # });142 };143 })144 ];145 };146}1{2 lib,3 config,4 ...5}: let6 inherit (lib.options) mkOption literalExpression;7 inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule functionTo package;8 inherit (lib.strings) concatStringsSep;9 inherit (lib.attrsets) mapAttrs;1011 sharedSecret = {config, ...}: {12 options = {13 expectedOwners = mkOption {14 type = nullOr (listOf str);15 description = ''16 Specifies the list of hosts authorized to decrypt and access this shared secret.1718 When null, secret ownership is managed manually via fleet.nix and CLI.19 Decrypted secrets will be stored at /run/secrets/$\{name} on authorized hosts.20 '';21 default = null;22 };23 regenerateOnOwnerAdded = mkOption {24 type = bool;25 description = ''26 Controls whether the secret must be regenerated when new owners are added.2728 Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).29 When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.30 '';31 };32 regenerateOnOwnerRemoved = mkOption {33 default = config.regenerateOnOwnerAdded;34 defaultText = literalExpression "regenerateOnOwnerAdded";35 type = bool;36 description = ''37 Determines secret behavior when owners are removed from the configuration.3839 Typically mirrors regenerateOnOwnerAdded. Override cautiously.40 Set to false if host permissions are revoked through alternative mechanisms like firewall rules.41 '';42 };43 generator = mkOption {44 type = nullOr (functionTo package);45 description = ''46 Function evaluating to nix derivation responsible for (re)generating the secret's content.4748 An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,49 use `mkSecretGenerator` helpers to implement own generators.50 '';51 default = null;52 };53 expectedGenerationData = mkOption {54 type = unspecified;55 description = "Contextual metadata embedded within the secret part value";56 default = null;57 };58 };59 };60in {61 options = {62 sharedSecrets = mkOption {63 type = attrsOf (submodule sharedSecret);64 default = {};65 description = "Collection of secrets shared across multiple hosts with configurable ownership";66 };67 };68 config = {69 hosts =70 mapAttrs (_: secretMap: {71 nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt" "generationData"]) secretMap;72 })73 config.data.hostSecrets;74 nixpkgs.overlays = [75 (final: prev: {76 mkSecretGenerators = {recipients}: rec {77 # TODO: Merge both generators to one with consistent options syntax?78 # Impure generator is built on local machine, then built closure is copied to remote machine,79 # and then it is ran in inpure context, so that this generator may access HSMs and other things.80 mkImpureSecretGenerator = {81 script,82 # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD83 # (Some secrets-encryption-in-git/managed PKI solution is expected)84 impureOn ? null,85 }:86 (prev.writeShellScript "impureGenerator.sh" ''87 #!/bin/sh88 set -eu8990 export GENERATOR_HELPER_IDENTITIES="${concatStringsSep"\n"recipients}";91 export PATH=${final.fleet-generator-helper}/bin:$PATH9293 # TODO: Provide tempdir from outside, to make it securely erasurable as needed?94 tmp=mktemp-d95 cd $tmp96 # cd /var/empty9798 created_at=date-u"%Y-%m-%dT%H:%M:%S.%NZ"99100 ${script}101102 if ! test -d $out; then103 echo "impure generator script did not produce expected \$out output"104 exit 1105 fi106107 echo -n $created_at > $out/created_at108 echo -n SUCCESS > $out/marker109 '')110 .overrideAttrs (old: {111 passthru = {112 inherit impureOn;113 generatorKind = "impure";114 };115 });116 # Pure generators are disabled for now117 mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;};118119 # TODO: Implement consistent naming120 # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...121 # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.122 # mkSecretGenerator = {script}:123 # (prev.writeShellScript "generator.sh" ''124 # #!/bin/sh125 # set -eu126 # # TODO: make nix daemon build secret, not just the script.127 # cd /var/empty128 #129 # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")130 #131 # ${script}132 # if ! test -d $out; then133 # echo "impure generator script did not produce expected \$out output"134 # exit 1135 # fi136 #137 # echo -n $created_at > $out/created_at138 # echo -n SUCCESS > $out/marker139 # '')140 # .overrideAttrs (old: {141 # passthru = {142 # generatorKind = "pure";143 # };144 # # TODO: make nix daemon build secret, not just the script.145 # # __impure = true;146 # });147 };148 })149 ];150 };151}