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.nixdiffbeforeafterboth1# Shared functions for fleet configuration, available as `fleet` module argument2{lib}: let3 inherit (lib.trivial) isFunction;4 inherit (lib.options) mkOption mergeOneOption;5 inherit (lib.modules) mkOverride;6 inherit (lib.types) listOf submodule attrsOf mkOptionType;7 inherit (lib.strings) optionalString hasPrefix removePrefix;8in rec {9 types = {10 overlay = mkOptionType {11 name = "nixpkgs-overlay";12 description = "nixpkgs overlay";13 check = isFunction;14 merge = mergeOneOption;15 };16 listOfOverlay = listOf types.overlay;1718 mkHostsType = module: attrsOf (submodule module);19 mkDataType = module: submodule module;20 };2122 options = {23 mkHostsOption = module:24 mkOption {25 type = types.mkHostsType module;26 };27 mkDataOption = module:28 mkOption {29 type = types.mkDataType module;30 };31 };3233 inherit (options) mkHostsOption;3435 modules = {36 # mkDefault = mkOverride 100037 # For places, where fleet knows better than nixpkgs defaults.38 mkFleetDefault = mkOverride 999;39 # Some generators use mkDefault, but optionDefault is set by nixpkgs.40 mkFleetGeneratorDefault = mkOverride 1001;41 };4243 inherit (modules) mkFleetDefault mkFleetGeneratorDefault;4445 secrets = {46 mkPassword = {size ? 32}: {47 coreutils,48 mkSecretGenerator,49 }:50 mkSecretGenerator {51 script = ''52 mkdir $out53 gh generate password -o $out/secret --size ${toStringsize}54 '';55 };5657 mkEd25519 = {58 noEmbedPublic ? false,59 encoding ? null,60 }: {mkSecretGenerator}:61 mkSecretGenerator {62 script = ''63 mkdir $out64 gh generate ed25519 -p $out/public -s $out/secret \65 ${optionalStringnoEmbedPublic"--no-embed-public"} \66 ${optionalString(encoding!=null)"--encoding=${encoding}"}67 '';68 };6970 mkX25519 = {encoding ? null}: {mkSecretGenerator}:71 mkSecretGenerator {72 script = ''73 mkdir $out74 gh generate x25519 -p $out/public -s $out/secret \75 ${optionalString(encoding!=null)"--encoding=${encoding}"}76 '';77 };7879 mkRsa = {size ? 4096}: {80 openssl,81 mkSecretGenerator,82 }:83 mkSecretGenerator {84 script = ''85 mkdir $out8687 ${openssl}/bin/openssl genrsa -out rsa_private.key ${toStringsize}88 ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key8990 cat rsa_private.key | gh private -o $out/secret91 cat rsa_public.key | gh public -o $out/public92 '';93 };9495 mkBytes = {96 count ? 32,97 encoding,98 noNuls ? false,99 }: {mkSecretGenerator}:100 mkSecretGenerator {101 script = ''102 mkdir $out103 gh generate bytes --count=${toStringcount} --encoding=${encoding} -o $out/secret \104 ${optionalStringnoNuls"--no-nuls"}105 '';106 };107 mkHexBytes = {count ? 32}:108 mkBytes {109 inherit count;110 encoding = "hex";111 };112 mkBase64Bytes = {count ? 32}:113 mkBytes {114 inherit count;115 encoding = "base64";116 };117118 # Wireguard119 # mkWireguard = {}: mkX25519 {encoding = "base64";};120 # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};121 };122123 inherit (secrets) mkPassword mkEd25519 mkX25519 mkRsa mkBytes mkHexBytes mkBase64Bytes;124125 strings = let126 plaintextPrefix = "<PLAINTEXT>";127 plaintextNewlinePrefix = "<PLAINTEXT-NL>";128 in {129 decodeRawSecret = raw:130 if hasPrefix plaintextPrefix raw131 then removePrefix plaintextPrefix raw132 else if hasPrefix plaintextNewlinePrefix raw133 then removePrefix plaintextNewlinePrefix raw134 else throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}";135 };136137 inherit (strings) decodeRawSecret;138}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.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 = {