--- /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) + --- 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 = ""; plaintextNewlinePrefix = "<PLAINTEXT-NL>"; in { + /** + Decode public secret part into string + */ decodeRawSecret = raw: if hasPrefix plaintextPrefix raw then removePrefix plaintextPrefix raw --- 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; } --- 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; }; }; --- 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 = {