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.nixdiffbeforeafterboth1{2 lib,3 fleetLib,4 config,5 ...6}: let7 inherit (fleetLib.options) mkDataOption;8 inherit (lib.options) mkOption;9 inherit (lib.types) nullOr listOf str attrsOf submodule bool unspecified;10 inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs;11 inherit (lib.lists) sort unique concatLists;12 inherit (lib.strings) toJSON;1314 secretDataValue = {15 options = {16 raw = mkOption {17 type = nullOr str;18 description = "Encrypted + encoded secret data";19 default = null;20 };21 };22 };2324 sharedSecretData = {25 freeformType = attrsOf (submodule secretDataValue);26 options = {27 createdAt = mkOption {28 type = str;29 description = "When this secret was (re)generated";30 default = null;31 };32 expiresAt = mkOption {33 type = nullOr str;34 description = "On which date this secret will expire, someone should regenerate this secret before it expires.";35 default = null;36 };3738 owners = mkOption {39 type = listOf str;40 description = ''41 For which owners this secret is currently encrypted,42 if not matches expectedOwners - then this secret is considered outdated, and43 should be regenerated/reencrypted.4445 Imported from fleet.nix46 '';47 default = [];48 };49 generationData = mkOption {50 type = unspecified;51 description = "Data that is embedded into secret part";52 default = null;53 };54 };55 config = {};56 };5758 hostSecretData = {59 freeformType = attrsOf (submodule secretDataValue);60 options = {61 createdAt = mkOption {62 type = str;63 description = "When this secret was (re)generated";64 default = null;65 };66 expiresAt = mkOption {67 type = nullOr str;68 description = "On which date this secret will expire, someone should regenerate this secret before it expires.";69 default = null;70 };71 shared = mkOption {72 type = bool;73 description = "On which date this secret will expire, someone should regenerate this secret before it expires.";74 default = false;75 };76 generationData = mkOption {77 type = unspecified;78 description = "Data that is embedded into secret part";79 default = null;80 };81 };82 config = {};83 };84in {85 options.data = mkDataOption ({config, ...}: {86 options = {87 sharedSecrets = mkOption {88 type = attrsOf (submodule sharedSecretData);89 default = {};90 description = "Stored shared secret data.";91 };92 hostSecrets = mkOption {93 type = attrsOf (attrsOf (submodule hostSecretData));94 default = {};95 description = "Host secrets.";96 internal = true;97 };98 };99 config.hostSecrets = let100 hostsWithSharedSecrets = unique (concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets));101 secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;102 toHostSecret = _: secret: (removeAttrs secret ["owners"]) // {shared = true;};103 in104 genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));105 });106 config = {107 assertions =108 (mapAttrsToList109 (name: secret: {110 assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;111 message = "Shared secret ${name} is expected to be encrypted for ${toJSONsecret.expectedOwners}, but it is encrypted for ${toJSONconfig.data.sharedSecrets.${name}.owners}. Run fleet secrets regenerate to fix";112 })113 config.sharedSecrets)114 ++ (mapAttrsToList115 (name: secret: {116 # TODO: Same aassertion should be in host secrets117 assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData;118 message = "Shared secret ${name} has unexpected generation data ${toJSONsecret.expectedGenerationData} != ${toJSONconfig.data.sharedSecrets.${name}.expectedGenerationData}. Run fleet secrets regenerate to fix";119 })120 config.sharedSecrets);121 sharedSecrets =122 mapAttrs (_: _: {}) config.data.sharedSecrets;123 };124}1{2 lib,3 fleetLib,4 config,5 ...6}: let7 inherit (fleetLib.options) mkDataOption;8 inherit (lib.options) mkOption;9 inherit (lib.types) nullOr listOf str attrsOf submodule bool unspecified;10 inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs;11 inherit (lib.lists) sort unique concatLists;12 inherit (lib.strings) toJSON;1314 secretDataValue = {15 options = {16 raw = mkOption {17 type = nullOr str;18 description = "Raw secret data in unspecified encoded and optionally encrypted format.";19 default = null;20 };21 };22 };2324 sharedSecretData = {25 freeformType = attrsOf (submodule secretDataValue);26 options = {27 createdAt = mkOption {28 type = str;29 description = "Timestamp of secret generation/last rotation.";30 default = null;31 };32 expiresAt = mkOption {33 type = nullOr str;34 description = "Expiration timestamp triggering mandatory secret rotation.";35 default = null;36 };3738 owners = mkOption {39 type = listOf str;40 description = ''41 List of hosts currently authorized to decrypt this shared secret.4243 If owners differ from expected owners, the secret is considered outdated44 and requires regeneration or re-encryption.45 '';46 default = [];47 };48 generationData = mkOption {49 type = unspecified;50 description = "Contextual metadata associated with secret part.";51 default = null;52 };53 };54 config = {};55 };5657 hostSecretData = {58 freeformType = attrsOf (submodule secretDataValue);59 options = {60 createdAt = mkOption {61 type = str;62 description = "Timestamp of secret generation/last rotation.";63 default = null;64 };65 expiresAt = mkOption {66 type = nullOr str;67 description = "Expiration timestamp triggering mandatory secret rotation.";68 default = null;69 };70 shared = mkOption {71 type = bool;72 description = "Indicates if secret is a shared secret, so other hosts might have the same piece of secret data.";73 default = false;74 };75 generationData = mkOption {76 type = unspecified;77 description = "Contextual metadata associated with secret part.";78 default = null;79 };80 };81 config = {};82 };83in {84 options.data = mkDataOption ({config, ...}: {85 options = {86 sharedSecrets = mkOption {87 type = attrsOf (submodule sharedSecretData);88 default = {};89 description = "Shared secret data.";90 };91 hostSecrets = mkOption {92 type = attrsOf (attrsOf (submodule hostSecretData));93 default = {};94 description = "Host-specific secrets.";95 internal = true;96 };97 };98 config.hostSecrets = let99 hostsWithSharedSecrets = unique (concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets));100 secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;101 toHostSecret = _: secret: (removeAttrs secret ["owners"]) // {shared = true;};102 in103 genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));104 });105 config = {106 assertions =107 (mapAttrsToList108 (name: secret: {109 assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;110 message = "Shared secret ${name} is expected to be encrypted for ${toJSONsecret.expectedOwners}, but it is encrypted for ${toJSONconfig.data.sharedSecrets.${name}.owners}. Run fleet secrets regenerate to fix";111 })112 config.sharedSecrets)113 ++ (mapAttrsToList114 (name: secret: {115 # TODO: Same aassertion should be in host secrets116 assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData;117 message = "Shared secret ${name} has unexpected generation data ${toJSONsecret.expectedGenerationData} != ${toJSONconfig.data.sharedSecrets.${name}.expectedGenerationData}. Run fleet secrets regenerate to fix";118 })119 config.sharedSecrets);120 sharedSecrets =121 mapAttrs (_: _: {}) config.data.sharedSecrets;122 };123}modules/secrets.nixdiffbeforeafterboth--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -4,7 +4,7 @@
...
}: let
inherit (lib.options) mkOption literalExpression;
- inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule;
+ inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule functionTo package;
inherit (lib.strings) concatStringsSep;
inherit (lib.attrsets) mapAttrs;
@@ -13,19 +13,20 @@
expectedOwners = mkOption {
type = nullOr (listOf str);
description = ''
- List of hosts to encrypt secret for. null if managed by user (= via owners field from fleet.nix)
+ Specifies the list of hosts authorized to decrypt and access this shared secret.
- Secrets would be decrypted and stored to /run/secrets/$\{name} on owners
+ When null, secret ownership is managed manually via fleet.nix and CLI.
+ Decrypted secrets will be stored at /run/secrets/$\{name} on authorized hosts.
'';
default = null;
};
- # TODO: Aren't those options may be just desugared to data/expectedData?
regenerateOnOwnerAdded = mkOption {
type = bool;
description = ''
- Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted.
+ Controls whether the secret must be regenerated when new owners are added.
- You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs.
+ Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).
+ When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.
'';
};
regenerateOnOwnerRemoved = mkOption {
@@ -33,21 +34,25 @@
defaultText = literalExpression "regenerateOnOwnerAdded";
type = bool;
description = ''
- Should this secret be removed on owner removal, or it may be just reencrypted
+ Determines secret behavior when owners are removed from the configuration.
- Most probably its value should be equal to regenerateOnOwnerAdded, override only if you know what are you doing.
- Contrary to regenerateOnOwnerAdded, you may want to set this option to false, when host permissions are revoked
- in some other way than by this secret ownership, I.e by firewall/etc.
+ Typically mirrors regenerateOnOwnerAdded. Override cautiously.
+ Set to false if host permissions are revoked through alternative mechanisms like firewall rules.
'';
};
generator = mkOption {
- type = nullOr unspecified;
- description = "Derivation to evaluate for secret generation";
+ type = nullOr (functionTo package);
+ description = ''
+ Function evaluating to nix derivation responsible for (re)generating the secret's content.
+
+ An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,
+ use `mkSecretGenerator` helpers to implement own generators.
+ '';
default = null;
};
expectedGenerationData = mkOption {
type = unspecified;
- description = "Data that gets embedded into secret part";
+ description = "Contextual metadata embedded within the secret part value";
default = null;
};
};
@@ -57,7 +62,7 @@
sharedSecrets = mkOption {
type = attrsOf (submodule sharedSecret);
default = {};
- description = "Shared secrets";
+ description = "Collection of secrets shared across multiple hosts with configurable ownership";
};
};
config = {