--- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ nix-eval = { path = "./crates/nix-eval" } tokio = { version = "1.36.0", features = [ - "fs", - "rt", - "macros", - "sync", - "time", - "rt-multi-thread", + "fs", + "rt", + "macros", + "sync", + "time", + "rt-multi-thread", ] } tokio-util = { version = "0.7.11", features = ["codec"] } clap = { version = "4.5", features = ["derive", "env", "wrap_help", "unicode"] } --- a/cmds/fleet/Cargo.toml +++ b/cmds/fleet/Cargo.toml @@ -32,8 +32,8 @@ shlex = "1.3" tabled = { version = "0.16" } owo-colors = { version = "4.0", features = [ - "supports-color", - "supports-colors", + "supports-color", + "supports-colors", ] } abort-on-drop = "0.2" regex = "1.10" @@ -52,8 +52,8 @@ default = ["indicatif"] # Not quite stable indicatif = [ - "dep:tracing-indicatif", - "dep:indicatif", - "dep:human-repr", - "better-command/indicatif", + "dep:tracing-indicatif", + "dep:indicatif", + "dep:human-repr", + "better-command/indicatif", ] --- a/cmds/install-secrets/src/main.rs +++ b/cmds/install-secrets/src/main.rs @@ -200,7 +200,7 @@ if data.is_empty() { info!("no secrets to install"); - return Ok(()) + return Ok(()); } let identity = host_identity()?; --- a/crates/nixlike/fuzz/fuzz_targets/fuzz_target_1.rs +++ b/crates/nixlike/fuzz/fuzz_targets/fuzz_target_1.rs @@ -2,8 +2,8 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: String| { - let serialized = nixlike::serialize(data.clone()).unwrap(); - let deserialized: String = nixlike::parse_str(&serialized).unwrap(); - - assert_eq!(data, deserialized); + let serialized = nixlike::serialize(data.clone()).unwrap(); + let deserialized: String = nixlike::parse_str(&serialized).unwrap(); + + assert_eq!(data, deserialized); }); --- a/flake.lock +++ b/flake.lock @@ -57,7 +57,8 @@ "flake-parts": "flake-parts", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay", - "shelly": "shelly" + "shelly": "shelly", + "treefmt-nix": "treefmt-nix" } }, "rust-overlay": { @@ -94,6 +95,26 @@ "repo": "shelly", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1744961264, + "narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "8d404a69efe76146368885110f29a2ca3700bee6", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "NixOS configuration management"; + description = "NixOS cluster configuration management"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/release-24.11"; @@ -13,6 +13,10 @@ }; crane.url = "github:ipetkov/crane"; shelly.url = "github:CertainLach/shelly"; + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs: @@ -75,6 +79,7 @@ config, system, pkgs, + self, ... }: let @@ -92,6 +97,7 @@ lib = pkgs.lib; rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rust; + treefmt = (inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix).config.build; in { _module.args.pkgs = import inputs.nixpkgs { @@ -128,7 +134,10 @@ # with rust in nixpkgs. (prefixAttrs "nixpkgs-" { inherit (packages) fleet-install-secrets; - }); + }) + // { + checks.formatting = treefmt.check self; + }; # TODO: It should be possible to move lib.mkIf to default attribute, instead of disabling the whole # devShells block, yet nix flake check fails here, due to no default shell found. It is nix or flake-parts bug? shelly.shells.default = lib.mkIf deployerSystem { @@ -151,7 +160,7 @@ ]; environment.PROTOC = "${pkgs.protobuf}/bin/protoc"; }; - formatter = pkgs.alejandra; + formatter = treefmt.wrapper; }; }; } --- a/lib/default.nix +++ b/lib/default.nix @@ -1,11 +1,18 @@ # Shared functions for fleet configuration, available as `fleet` module argument -{lib}: let +{ lib }: +let inherit (lib.trivial) isFunction; inherit (lib.options) mkOption mergeOneOption; inherit (lib.modules) mkOverride; - inherit (lib.types) listOf submodule attrsOf mkOptionType; + inherit (lib.types) + listOf + submodule + attrsOf + mkOptionType + ; inherit (lib.strings) optionalString hasPrefix removePrefix; -in rec { +in +rec { types = { overlay = mkOptionType { name = "nixpkgs-overlay"; @@ -20,11 +27,13 @@ }; options = { - mkHostsOption = module: + mkHostsOption = + module: mkOption { type = types.mkHostsType module; }; - mkDataOption = module: + mkDataOption = + module: mkOption { type = types.mkDataType module; }; @@ -57,10 +66,14 @@ Output: Resulting secret has only part: secret, which contains encrypted password. */ - mkPassword = {size ? 32}: { - coreutils, - mkSecretGenerator, - }: + mkPassword = + { + size ? 32, + }: + { + coreutils, + mkSecretGenerator, + }: mkSecretGenerator { script = '' mkdir $out @@ -81,10 +94,12 @@ This secret format is used by e.g Garage S3 server */ - mkEd25519 = { - noEmbedPublic ? false, - encoding ? null, - }: {mkSecretGenerator}: + mkEd25519 = + { + noEmbedPublic ? false, + encoding ? null, + }: + { mkSecretGenerator }: mkSecretGenerator { script = '' mkdir $out @@ -105,7 +120,11 @@ This secret format is used by e.g Wireguard VPN for peers (base64-encoded) */ - mkX25519 = {encoding ? null}: {mkSecretGenerator}: + mkX25519 = + { + encoding ? null, + }: + { mkSecretGenerator }: mkSecretGenerator { script = '' mkdir $out @@ -124,10 +143,14 @@ Resulting secret has two parts: public and secret, where the secret part is encrypted. Both parts are PEM encoded. */ - mkRsa = {size ? 4096}: { - openssl, - mkSecretGenerator, - }: + mkRsa = + { + size ? 4096, + }: + { + openssl, + mkSecretGenerator, + }: mkSecretGenerator { script = '' mkdir $out @@ -154,11 +177,13 @@ Might be used for e.g. Wireguard VPN PSK keys (base64-encoded) */ - mkBytes = { - count ? 32, - encoding, - noNuls ? false, - }: {mkSecretGenerator}: + mkBytes = + { + count ? 32, + encoding, + noNuls ? false, + }: + { mkSecretGenerator }: mkSecretGenerator { script = '' mkdir $out @@ -169,7 +194,10 @@ /** Shorthand for `mkBytes`, which defaults to "hex" encoding */ - mkHexBytes = {count ? 32}: + mkHexBytes = + { + count ? 32, + }: mkBytes { inherit count; encoding = "hex"; @@ -177,7 +205,10 @@ /** Shorthand for `mkBytes`, which defaults to "base64" encoding */ - mkBase64Bytes = {count ? 32}: + mkBase64Bytes = + { + count ? 32, + }: mkBytes { inherit count; encoding = "base64"; @@ -188,22 +219,34 @@ # mkWireguardPsk = {}: mkBase64Bytes {count = 32;}; }; - inherit (secrets) mkPassword mkEd25519 mkX25519 mkRsa mkBytes mkHexBytes mkBase64Bytes; + inherit (secrets) + mkPassword + mkEd25519 + mkX25519 + mkRsa + mkBytes + mkHexBytes + mkBase64Bytes + ; - strings = let - plaintextPrefix = ""; - plaintextNewlinePrefix = "<PLAINTEXT-NL>"; - in { - /** - Decode public secret part into string - */ - decodeRawSecret = raw: - if hasPrefix plaintextPrefix raw - then removePrefix plaintextPrefix raw - else if hasPrefix plaintextNewlinePrefix raw - then removePrefix plaintextNewlinePrefix raw - else throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}"; - }; + strings = + let + plaintextPrefix = "<PLAINTEXT>"; + plaintextNewlinePrefix = "<PLAINTEXT-NL>"; + in + { + /** + Decode public secret part into string + */ + decodeRawSecret = + raw: + if hasPrefix plaintextPrefix raw then + removePrefix plaintextPrefix raw + else if hasPrefix plaintextNewlinePrefix raw then + removePrefix plaintextNewlinePrefix raw + else + throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}"; + }; inherit (strings) decodeRawSecret; } --- a/lib/flakePart.nix +++ b/lib/flakePart.nix @@ -1,20 +1,28 @@ -{crane}: { +{ crane }: +{ fleetLib, lib, config, inputs, self, ... -}: let +}: +let inherit (lib.options) mkOption mkEnableOption; inherit (lib.attrsets) mapAttrs; - inherit (lib.types) lazyAttrsOf deferredModule unspecified str; + inherit (lib.types) + lazyAttrsOf + deferredModule + unspecified + str + ; inherit (lib.strings) isPath; inherit (lib.modules) mkIf mkOptionDefault; -in { +in +{ options.fleetModules = mkOption { type = lazyAttrsOf unspecified; - default = {}; + default = { }; }; options.fleetNixosConfigurationsCompat = { enable = mkEnableOption "Create nixosConfiguration output based on fleetConfiguration"; @@ -30,9 +38,11 @@ }; options.fleetConfigurations = mkOption { type = lazyAttrsOf deferredModule; - apply = nameToModule: + apply = + nameToModule: mapAttrs ( - name: module: data: let + name: module: data: + let # To use user-provided nixpkgs, we first need to extract wanted nixpkgs attribute, # to do that, evaluate all the modules with only needed option declared. bootstrapEval = lib.evalModules { @@ -53,28 +63,27 @@ }; bootstrapNixpkgs = bootstrapEval.config.nixpkgs.buildUsing; normalEval = bootstrapNixpkgs.lib.evalModules { - modules = - (import ../modules/module-list.nix) - ++ [ - module - { - config = { - data = - if isPath data - then import data - else data; - nixpkgs.buildUsing = mkOptionDefault bootstrapNixpkgs; - nixpkgs.overlays = [ - (final: prev: { - inherit (import ../pkgs { + modules = (import ../modules/module-list.nix) ++ [ + module + { + config = { + data = if isPath data then import data else data; + nixpkgs.buildUsing = mkOptionDefault bootstrapNixpkgs; + nixpkgs.overlays = [ + (final: prev: { + inherit + (import ../pkgs { inherit (prev) callPackage; craneLib = crane.mkLib prev; - }) fleet-install-secrets fleet-generator-helper; - }) - ]; - }; - } - ]; + }) + fleet-install-secrets + fleet-generator-helper + ; + }) + ]; + }; + } + ]; specialArgs = { inherit inputs self; fleetLib = import ../lib { @@ -84,21 +93,19 @@ }; }; in - normalEval - ) - nameToModule; + normalEval + ) nameToModule; }; config = { - _module.args.fleetLib = import ../lib {inherit lib;}; + _module.args.fleetLib = import ../lib { inherit lib; }; flake.fleetConfigurations = config.fleetConfigurations; - flake.nixosConfigurations = let - cfg = config.fleetNixosConfigurationsCompat; - in - mkIf cfg.enable - ( - mapAttrs - (name: host: host.nixos) - (config.fleetConfigurations.${cfg.configuration} cfg.data).config.hosts + flake.nixosConfigurations = + let + cfg = config.fleetNixosConfigurationsCompat; + in + mkIf cfg.enable ( + mapAttrs (name: host: host.nixos) + (config.fleetConfigurations.${cfg.configuration} cfg.data).config.hosts ); flake.fleetModules = config.fleetModules; }; --- a/modules/assertions.nix +++ b/modules/assertions.nix @@ -2,7 +2,8 @@ lib, config, ... -}: let +}: +let inherit (lib.options) mkOption; inherit (lib.types) listOf unspecified str; inherit (lib.lists) map filter; @@ -14,12 +15,13 @@ Similar to warnings, however build will fail if any error exists. ''; }; -in { +in +{ options = { assertions = mkOption { type = listOf unspecified; internal = true; - default = []; + default = [ ]; example = [ { assertion = false; @@ -35,9 +37,9 @@ warnings = mkOption { internal = true; - default = []; + default = [ ]; type = listOf str; - example = ["The `foo' service is deprecated and will go away soon!"]; + example = [ "The `foo' service is deprecated and will go away soon!" ]; description = '' This option allows modules to show warnings to users during the evaluation of the system configuration. @@ -47,18 +49,16 @@ inherit errors; }; config = { - errors = - map (v: v.message) - (filter (v: !v.assertion) config.assertions); + errors = map (v: v.message) (filter (v: !v.assertion) config.assertions); - nixos = {config, ...}: { - _file = ./assertions.nix; - options = { - inherit errors; + nixos = + { config, ... }: + { + _file = ./assertions.nix; + options = { + inherit errors; + }; + config.errors = map (v: v.message) (filter (v: !v.assertion) config.assertions); }; - config.errors = - map (v: v.message) - (filter (v: !v.assertion) config.assertions); - }; }; } --- a/modules/extras/tf.nix +++ b/modules/extras/tf.nix @@ -4,15 +4,18 @@ fleetLib, inputs, ... -}: let +}: +let inherit (lib.options) mkOption; inherit (lib.types) deferredModule attrsOf unspecified; inherit (fleetLib.options) mkDataOption; -in { +in +{ options = { tf = mkOption { type = deferredModule; - apply = module: system: + apply = + module: system: inputs.terranix.lib.terranixConfiguration { inherit system; pkgs = config.nixpkgs.buildUsing.legacyPackages.${system}; @@ -24,7 +27,7 @@ data = mkDataOption { # host => hostData options.extra.terraformHosts = mkOption { - default = {}; + default = { }; type = attrsOf (attrsOf unspecified); description = "Hosts data provided by fleet tf"; }; --- a/modules/fleetLib.nix +++ b/modules/fleetLib.nix @@ -1,4 +1,5 @@ -{lib, ...}: { +{ lib, ... }: +{ _module.args.fleetLib = import ../../lib { inherit lib; }; --- a/modules/hosts.nix +++ b/modules/hosts.nix @@ -3,14 +3,21 @@ fleetLib, config, ... -}: let +}: +let inherit (fleetLib.modules) mkFleetGeneratorDefault; inherit (fleetLib.types) mkHostsType mkDataType; inherit (lib.options) mkOption; - inherit (lib.types) str listOf attrsOf submodule; + inherit (lib.types) + str + listOf + attrsOf + submodule + ; inherit (lib.attrsets) mapAttrsToList mapAttrs; inherit (lib.lists) flatten groupBy; -in { +in +{ # Fleet Meta Configuration Module options = { @@ -52,60 +59,68 @@ }; hosts = mkOption { - type = mkHostsType ({config, ...}: { - options = { - system = mkOption { - description = "System architecture and platform identifier"; - type = str; - example = "x86_64-linux"; - }; + type = mkHostsType ( + { config, ... }: + { + options = { + system = mkOption { + description = "System architecture and platform identifier"; + type = str; + example = "x86_64-linux"; + }; - tags = mkOption { - description = '' - Tags for host classification. - Used for host selection via @tag syntax in CLI tools. - ''; - type = listOf str; - }; + tags = mkOption { + 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 = "List of internal IP addresses for the host"; - type = listOf str; - default = []; - }; + # Network configuration details + network = mkOption { + type = submodule { + options = { + internalIps = mkOption { + description = "List of internal IP addresses for the host"; + type = listOf str; + default = [ ]; + }; - externalIps = mkOption { - description = "List of external IP addresses for the host"; - type = listOf str; - default = []; + externalIps = mkOption { + description = "List of external IP addresses for the host"; + type = listOf str; + default = [ ]; + }; }; }; }; }; - }; - config = { - # Default hostname generation - nixos.networking.hostName = mkFleetGeneratorDefault config._module.args.name; - # Default 'all' tag for every host - tags = ["all"]; - }; - _file = ./meta.nix; - }); - default = {}; + config = { + # Default hostname generation + nixos.networking.hostName = mkFleetGeneratorDefault config._module.args.name; + # Default 'all' tag for every host + tags = [ "all" ]; + }; + _file = ./meta.nix; + } + ); + default = { }; }; }; # 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 + 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 --- a/modules/meta.nix +++ b/modules/meta.nix @@ -1,7 +1,9 @@ -{lib, ...}: let +{ lib, ... }: +let inherit (lib.modules) mkRemovedOptionModule; -in { +in +{ imports = [ - (mkRemovedOptionModule ["fleetModules"] "replaced with imports.") + (mkRemovedOptionModule [ "fleetModules" ] "replaced with imports.") ]; } --- a/modules/nixos.nix +++ b/modules/nixos.nix @@ -6,7 +6,8 @@ config, _fleetFlakeRootConfig, ... -}: let +}: +let inherit (lib.attrsets) mapAttrs; inherit (lib.options) mkOption; inherit (lib.types) deferredModule unspecified; @@ -15,7 +16,8 @@ inherit (fleetLib.options) mkHostsOption; _file = ./nixos.nix; -in { +in +{ options = { nixos = mkOption { description = '' @@ -31,26 +33,33 @@ Nixos configuration for the current host. ''; type = deferredModule; - apply = module: let - inherit (hostArgs.config) system; - in + apply = + module: + let + inherit (hostArgs.config) system; + in config.nixpkgs.buildUsing.lib.nixosSystem { inherit system; modules = [ - (module // {key = "attr<host.nixos>";}) - (config.nixos // {key = "attr<fleet.nixos>";}) + (module // { key = "attr<host.nixos>"; }) + (config.nixos // { key = "attr<fleet.nixos>"; }) ]; specialArgs = { inherit fleetLib inputs self; - inputs' = mapAttrs (inputName: input: - builtins.addErrorContext "while retrieving system-dependent attributes for input ${escapeNixIdentifier inputName}" - ( - if input._type or null == "flake" - then _fleetFlakeRootConfig.perInput system input - else "input is not a flake, perhaps flake = false was added to te input declaration?" - )) - inputs; - self' = builtins.addErrorContext "while retrieving system-dependent attributes for a flake's own outputs" (_fleetFlakeRootConfig.perInput system self); + inputs' = mapAttrs ( + inputName: input: + builtins.addErrorContext + "while retrieving system-dependent attributes for input ${escapeNixIdentifier inputName}" + ( + if input._type or null == "flake" then + _fleetFlakeRootConfig.perInput system input + else + "input is not a flake, perhaps flake = false was added to te input declaration?" + ) + ) inputs; + self' = builtins.addErrorContext "while retrieving system-dependent attributes for a flake's own outputs" ( + _fleetFlakeRootConfig.perInput system self + ); }; }; }; @@ -80,8 +89,7 @@ }); }; imports = [ - (mkRemovedOptionModule ["nixosModules"] "replaced with nixos.imports.") + (mkRemovedOptionModule [ "nixosModules" ] "replaced with nixos.imports.") ]; - config.nixos.imports = - import ./nixos/module-list.nix; + config.nixos.imports = import ./nixos/module-list.nix; } --- a/modules/nixos/nix-sign.nix +++ b/modules/nixos/nix-sign.nix @@ -3,15 +3,17 @@ lib, config, ... -}: let +}: +let inherit (lib.modules) mkIf; hasPersistentHostname = config.networking.hostName != ""; -in { +in +{ # https://github.com/NixOS/nix/issues/3023 systemd.services.generate-nix-cache-key = mkIf hasPersistentHostname { - wantedBy = ["multi-user.target"]; + wantedBy = [ "multi-user.target" ]; serviceConfig.Type = "oneshot"; - path = [config.nix.package]; + path = [ config.nix.package ]; script = '' [[ -f /etc/nix/private-key ]] && exit nix-store --generate-binary-cache-key ${config.networking.hostName}-1 /etc/nix/private-key /etc/nix/public-key --- a/modules/nixos/online.nix +++ b/modules/nixos/online.nix @@ -2,29 +2,41 @@ config, lib, ... -}: let +}: +let inherit (lib.options) mkOption; - inherit (lib.types) attrsOf str submodule either listOf lines bool; + inherit (lib.types) + attrsOf + str + submodule + either + listOf + lines + bool + ; inherit (lib.attrsets) mapAttrs; inherit (lib.trivial) isString; -in { +in +{ options.system.onlineActivationScripts = mkOption { - default = {}; - type = attrsOf (either str (submodule { - options = { - deps = mkOption { - type = listOf str; - default = []; + default = { }; + type = attrsOf ( + either str (submodule { + options = { + deps = mkOption { + type = listOf str; + default = [ ]; + }; + text = mkOption { + type = lines; + }; + supportsDryActivation = mkOption { + type = bool; + default = false; + }; }; - text = mkOption { - type = lines; - }; - supportsDryActivation = mkOption { - type = bool; - default = false; - }; - }; - })); + }) + ); description = '' Same as activation scripts, but only ran on online activation (i.e when operator is actively running fleet deploy, and not on system restart) @@ -32,42 +44,40 @@ we should not apply outdated ceph monmap. ''; - apply = set: + apply = + set: mapAttrs ( name: value: - if isString value - then { + if isString value then + { + text = '' + if [ ! -z ''${FLEET_ONLINE_ACTIVATION+x} ]; then + ${value} + fi + ''; + deps = [ "onlineActivation" ]; + } + else + value + // { + deps = [ "onlineActivation" ] ++ value.deps; text = '' if [ ! -z ''${FLEET_ONLINE_ACTIVATION+x} ]; then - ${value} + ${value.text} fi ''; - deps = ["onlineActivation"]; } - else - value - // { - deps = ["onlineActivation"] ++ value.deps; - text = '' - if [ ! -z ''${FLEET_ONLINE_ACTIVATION+x} ]; then - ${value.text} - fi - ''; - } - ) - set; + ) set; }; - config.system.activationScripts = - { - onlineActivation = { - text = '' - if [ ! -z ''${FLEET_ONLINE_ACTIVATION+x} ]; then - 1>&2 echo "online activation; hello, fleet!" - fi - ''; - supportsDryActivation = true; - }; - } - // config.system.onlineActivationScripts; + config.system.activationScripts = { + onlineActivation = { + text = '' + if [ ! -z ''${FLEET_ONLINE_ACTIVATION+x} ]; then + 1>&2 echo "online activation; hello, fleet!" + fi + ''; + supportsDryActivation = true; + }; + } // config.system.onlineActivationScripts; } --- a/modules/nixos/rollback.nix +++ b/modules/nixos/rollback.nix @@ -1,5 +1,6 @@ # Tied to build_systems.rs -{config, ...}: { +{ config, ... }: +{ # TODO: Make it work with systemd-initrd approach. # In this case we can't just switch generation and re-run activation script, since the root filesystem might not be # mounted yet. We need to explicitly remove the last generation, and this needs deeper integration with systemd/grub/ @@ -36,7 +37,7 @@ systemd.timers.rollback-watchdog = { description = "Timer for rollback watchdog"; - wantedBy = ["timers.target"]; + wantedBy = [ "timers.target" ]; timerConfig = { OnActiveSec = "3min"; RemainAfterElapse = false; --- a/modules/nixos/secrets.nix +++ b/modules/nixos/secrets.nix @@ -4,125 +4,149 @@ config, pkgs, ... -}: let +}: +let inherit (builtins) hashString; inherit (lib.stringsWithDeps) stringAfter; inherit (lib.options) mkOption literalExpression; inherit (lib.lists) optional; inherit (lib.attrsets) mapAttrs; inherit (lib.modules) mkIf; - inherit (lib.types) submodule str attrsOf nullOr unspecified lazyAttrsOf uniq functionTo package; + inherit (lib.types) + submodule + str + attrsOf + nullOr + unspecified + lazyAttrsOf + uniq + functionTo + package + ; inherit (fleetLib.strings) decodeRawSecret; sysConfig = config; - secretPartType = secretName: - submodule ({config, ...}: let - partName = config._module.args.name; - in { + secretPartType = + secretName: + submodule ( + { config, ... }: + let + partName = config._module.args.name; + in + { + options = { + raw = mkOption { + type = str; + internal = true; + description = "Encoded & Encrypted secret part data, passed from fleet.nix"; + }; + hash = mkOption { + type = str; + description = "Hash of secret in encoded format"; + }; + path = mkOption { + type = str; + description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)"; + }; + stablePath = mkOption { + type = str; + description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)"; + }; + data = mkOption { + type = str; + description = "Secret public data (only available for plaintext)"; + }; + }; + config = { + hash = hashString "sha1" config.raw; + data = decodeRawSecret config.raw; + path = "/run/secrets/${secretName}/${config.hash}-${partName}"; + stablePath = "/run/secrets/${secretName}/${partName}"; + }; + } + ); + secretType = submodule ( + { config, ... }: + let + secretName = config._module.args.name; + in + { + freeformType = lazyAttrsOf (secretPartType secretName); options = { - raw = mkOption { - type = str; - internal = true; - description = "Encoded & Encrypted secret part data, passed from fleet.nix"; + shared = mkOption { + description = "Is this secret owned by this machine, or propagated from shared secrets"; + default = false; + }; + + generator = mkOption { + type = uniq (nullOr (functionTo package)); + description = "Derivation to evaluate for secret generation"; + default = null; }; - hash = mkOption { + mode = mkOption { type = str; - description = "Hash of secret in encoded format"; + description = "Secret mode"; + default = "0440"; }; - path = mkOption { + owner = mkOption { type = str; - description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)"; + description = "Owner of the secret"; + default = "root"; }; - stablePath = mkOption { + group = mkOption { type = str; - description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)"; + description = "Group of the secret"; + default = sysConfig.users.users.${config.owner}.group; + defaultText = literalExpression "config.users.users.$${owner}.group"; }; - data = mkOption { - type = str; - description = "Secret public data (only available for plaintext)"; + expectedGenerationData = mkOption { + type = unspecified; + description = "Data that gets embedded into secret part"; + default = null; }; - }; - config = { - hash = hashString "sha1" config.raw; - data = decodeRawSecret config.raw; - path = "/run/secrets/${secretName}/${config.hash}-${partName}"; - stablePath = "/run/secrets/${secretName}/${partName}"; }; - }); - secretType = submodule ({config, ...}: let - secretName = config._module.args.name; - in { - freeformType = lazyAttrsOf (secretPartType secretName); - options = { - shared = mkOption { - description = "Is this secret owned by this machine, or propagated from shared secrets"; - default = false; - }; - - generator = mkOption { - type = uniq (nullOr (functionTo package)); - description = "Derivation to evaluate for secret generation"; - default = null; - }; - mode = mkOption { - type = str; - description = "Secret mode"; - default = "0440"; - }; - owner = mkOption { - type = str; - description = "Owner of the secret"; - default = "root"; - }; - group = mkOption { - type = str; - description = "Group of the secret"; - default = sysConfig.users.users.${config.owner}.group; - defaultText = literalExpression "config.users.users.$${owner}.group"; - }; - expectedGenerationData = mkOption { - type = unspecified; - description = "Data that gets embedded into secret part"; - default = null; - }; - }; - }); + } + ); processPart = part: { inherit (part) raw path stablePath; }; - processSecret = secret: + processSecret = + secret: { inherit (secret) group mode owner; } - // (mapAttrs (_: processPart) (removeAttrs secret [ - "shared" - "generator" - "mode" - "group" - "owner" - "expectedGenerationData" - ])); + // (mapAttrs (_: processPart) ( + removeAttrs secret [ + "shared" + "generator" + "mode" + "group" + "owner" + "expectedGenerationData" + ] + )); secretsFile = pkgs.writeTextFile { name = "secrets.json"; - text = - builtins.toJSON (mapAttrs (_: processSecret) - config.secrets); + text = builtins.toJSON (mapAttrs (_: processSecret) config.secrets); }; - useSysusers = (config.systemd ? sysusers && config.systemd.sysusers.enable) || (config ? userborn && config.userborn.enable); -in { + useSysusers = + (config.systemd ? sysusers && config.systemd.sysusers.enable) + || (config ? userborn && config.userborn.enable); +in +{ options = { secrets = mkOption { type = attrsOf secretType; - default = {}; + default = { }; description = "Host-local secrets"; }; }; config = { - environment.systemPackages = [pkgs.fleet-install-secrets]; + environment.systemPackages = [ pkgs.fleet-install-secrets ]; systemd.services.fleet-install-secrets = mkIf useSysusers { - wantedBy = ["sysinit.target"]; - after = ["systemd-sysusers.service"]; + wantedBy = [ "sysinit.target" ]; + after = [ "systemd-sysusers.service" ]; restartTriggers = [ secretsFile ]; @@ -139,10 +163,9 @@ ExecStart = "${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}"; }; }; - system.activationScripts.decryptSecrets = - mkIf (!useSysusers) - ( - stringAfter ( + system.activationScripts.decryptSecrets = mkIf (!useSysusers) ( + stringAfter + ( [ # secrets are owned by user/group, thus we need to refer to those "users" @@ -153,10 +176,11 @@ # but with impermanence we expect that the host-key is installed by # persist-file activation script. ++ (optional (config.system.activationScripts ? "persist-files") "persist-files") - ) '' + ) + '' 1>&2 echo "setting up secrets" ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile} '' - ); + ); }; } --- a/modules/nixpkgs.nix +++ b/modules/nixpkgs.nix @@ -3,7 +3,8 @@ fleetLib, config, ... -}: let +}: +let inherit (lib.options) mkOption literalExpression; inherit (lib.types) path; inherit (lib.modules) mkRemovedOptionModule; @@ -11,7 +12,8 @@ inherit (fleetLib.types) listOfOverlay; _file = ./nixpkgs.lib; -in { +in +{ options = { nixpkgs = { buildUsing = mkOption { @@ -48,7 +50,10 @@ inherit _file; nixpkgs.overlays = config.nixpkgs.overlays; imports = [ - (mkRemovedOptionModule ["nixpkgs" "buildUsing"] "this option should be specified at the host level, not the nixosModules level") + (mkRemovedOptionModule [ + "nixpkgs" + "buildUsing" + ] "this option should be specified at the host level, not the nixosModules level") ]; }; }; --- a/modules/secrets-data.nix +++ b/modules/secrets-data.nix @@ -3,11 +3,25 @@ fleetLib, config, ... -}: let +}: +let inherit (fleetLib.options) mkDataOption; inherit (lib.options) mkOption; - inherit (lib.types) nullOr listOf str attrsOf submodule bool unspecified; - inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs; + inherit (lib.types) + nullOr + listOf + str + attrsOf + submodule + bool + unspecified + ; + inherit (lib.attrsets) + mapAttrsToList + mapAttrs + filterAttrs + genAttrs + ; inherit (lib.lists) sort unique concatLists; inherit (lib.strings) toJSON; @@ -43,7 +57,7 @@ If owners differ from expected owners, the secret is considered outdated and requires regeneration or re-encryption. ''; - default = []; + default = [ ]; }; generationData = mkOption { type = unspecified; @@ -51,7 +65,7 @@ default = null; }; }; - config = {}; + config = { }; }; hostSecretData = { @@ -78,46 +92,56 @@ default = null; }; }; - config = {}; + config = { }; }; -in { - options.data = mkDataOption ({config, ...}: { - options = { - sharedSecrets = mkOption { - type = attrsOf (submodule sharedSecretData); - default = {}; - description = "Shared secret data."; +in +{ + options.data = mkDataOption ( + { config, ... }: + { + options = { + sharedSecrets = mkOption { + type = attrsOf (submodule sharedSecretData); + default = { }; + description = "Shared secret data."; + }; + hostSecrets = mkOption { + type = attrsOf (attrsOf (submodule hostSecretData)); + default = { }; + description = "Host-specific secrets."; + internal = true; + }; }; - hostSecrets = mkOption { - type = attrsOf (attrsOf (submodule hostSecretData)); - default = {}; - description = "Host-specific secrets."; - internal = true; - }; - }; - config.hostSecrets = let - hostsWithSharedSecrets = unique (concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets)); - secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets; - toHostSecret = _: secret: (removeAttrs secret ["owners"]) // {shared = true;}; - in - genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host)); - }); + config.hostSecrets = + let + hostsWithSharedSecrets = unique ( + concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets) + ); + secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets; + toHostSecret = _: secret: (removeAttrs secret [ "owners" ]) // { shared = true; }; + in + genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host)); + } + ); config = { assertions = - (mapAttrsToList - (name: secret: { - assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners; - message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${toJSON config.data.sharedSecrets.${name}.owners}. Run fleet secrets regenerate to fix"; - }) - config.sharedSecrets) - ++ (mapAttrsToList - (name: secret: { - # TODO: Same aassertion should be in host secrets - assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData; - message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${toJSON config.data.sharedSecrets.${name}.expectedGenerationData}. Run fleet secrets regenerate to fix"; - }) - config.sharedSecrets); - sharedSecrets = - mapAttrs (_: _: {}) config.data.sharedSecrets; + (mapAttrsToList (name: secret: { + assertion = + secret.expectedOwners == null + || + sort (a: b: a < b) (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners + == sort (a: b: a < b) secret.expectedOwners; + message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${ + toJSON config.data.sharedSecrets.${name}.owners + }. Run fleet secrets regenerate to fix"; + }) config.sharedSecrets) + ++ (mapAttrsToList (name: secret: { + # TODO: Same aassertion should be in host secrets + assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData; + message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${ + toJSON config.data.sharedSecrets.${name}.expectedGenerationData + }. Run fleet secrets regenerate to fix"; + }) config.sharedSecrets); + sharedSecrets = mapAttrs (_: _: { }) config.data.sharedSecrets; }; } --- a/modules/secrets.nix +++ b/modules/secrets.nix @@ -2,149 +2,172 @@ lib, config, ... -}: let +}: +let inherit (lib.options) mkOption literalExpression; - inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule functionTo package uniq; + inherit (lib.types) + unspecified + nullOr + listOf + str + bool + attrsOf + submodule + functionTo + package + uniq + ; inherit (lib.strings) concatStringsSep; inherit (lib.attrsets) mapAttrs; - sharedSecret = {config, ...}: { - options = { - expectedOwners = mkOption { - type = nullOr (listOf str); - description = '' - Specifies the list of hosts authorized to decrypt and access this shared secret. + sharedSecret = + { config, ... }: + { + options = { + expectedOwners = mkOption { + type = nullOr (listOf str); + description = '' + Specifies the list of hosts authorized to decrypt and access this shared secret. - 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; - }; - regenerateOnOwnerAdded = mkOption { - type = bool; - description = '' - Controls whether the secret must be regenerated when new owners are added. + 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; + }; + regenerateOnOwnerAdded = mkOption { + type = bool; + description = '' + Controls whether the secret must be regenerated when new owners are added. - 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 { - default = config.regenerateOnOwnerAdded; - defaultText = literalExpression "regenerateOnOwnerAdded"; - type = bool; - description = '' - Determines secret behavior when owners are removed from the configuration. + 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 { + default = config.regenerateOnOwnerAdded; + defaultText = literalExpression "regenerateOnOwnerAdded"; + type = bool; + description = '' + Determines secret behavior when owners are removed from the configuration. - Typically mirrors regenerateOnOwnerAdded. Override cautiously. - Set to false if host permissions are revoked through alternative mechanisms like firewall rules. - ''; - }; - generator = mkOption { - type = uniq (nullOr (functionTo package)); - description = '' - Function evaluating to nix derivation responsible for (re)generating the secret's content. + Typically mirrors regenerateOnOwnerAdded. Override cautiously. + Set to false if host permissions are revoked through alternative mechanisms like firewall rules. + ''; + }; + generator = mkOption { + type = uniq (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 = "Contextual metadata embedded within the secret part value"; - default = null; + 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 = "Contextual metadata embedded within the secret part value"; + default = null; + }; }; }; - }; -in { +in +{ options = { sharedSecrets = mkOption { type = attrsOf (submodule sharedSecret); - default = {}; + default = { }; description = "Collection of secrets shared across multiple hosts with configurable ownership"; }; }; config = { - hosts = - mapAttrs (_: secretMap: { - nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt" "generationData"]) secretMap; - }) - config.data.hostSecrets; + hosts = mapAttrs (_: secretMap: { + nixos.secrets = mapAttrs ( + _: s: + removeAttrs s [ + "createdAt" + "expiresAt" + "generationData" + ] + ) secretMap; + }) config.data.hostSecrets; nixpkgs.overlays = [ (final: prev: { - mkSecretGenerators = {recipients}: rec { - # TODO: Merge both generators to one with consistent options syntax? - # Impure generator is built on local machine, then built closure is copied to remote machine, - # and then it is ran in inpure context, so that this generator may access HSMs and other things. - mkImpureSecretGenerator = { - script, - # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD - # (Some secrets-encryption-in-git/managed PKI solution is expected) - impureOn ? null, - }: - (prev.writeShellScript "impureGenerator.sh" '' - #!/bin/sh - set -eu + mkSecretGenerators = + { recipients }: + rec { + # TODO: Merge both generators to one with consistent options syntax? + # Impure generator is built on local machine, then built closure is copied to remote machine, + # and then it is ran in inpure context, so that this generator may access HSMs and other things. + mkImpureSecretGenerator = + { + script, + # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD + # (Some secrets-encryption-in-git/managed PKI solution is expected) + impureOn ? null, + }: + (prev.writeShellScript "impureGenerator.sh" '' + #!/bin/sh + set -eu - export GENERATOR_HELPER_IDENTITIES="${concatStringsSep "\n" recipients}"; - export PATH=${final.fleet-generator-helper}/bin:$PATH + export GENERATOR_HELPER_IDENTITIES="${concatStringsSep "\n" recipients}"; + export PATH=${final.fleet-generator-helper}/bin:$PATH - # TODO: Provide tempdir from outside, to make it securely erasurable as needed? - tmp=$(mktemp -d) - cd $tmp - # cd /var/empty + # TODO: Provide tempdir from outside, to make it securely erasurable as needed? + tmp=$(mktemp -d) + cd $tmp + # cd /var/empty - created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") + created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") - ${script} + ${script} - if ! test -d $out; then - echo "impure generator script did not produce expected \$out output" - exit 1 - fi + if ! test -d $out; then + echo "impure generator script did not produce expected \$out output" + exit 1 + fi - echo -n $created_at > $out/created_at - echo -n SUCCESS > $out/marker - '') - .overrideAttrs (old: { - passthru = { - inherit impureOn; - generatorKind = "impure"; - }; - }); - # Pure generators are disabled for now - mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;}; + echo -n $created_at > $out/created_at + echo -n SUCCESS > $out/marker + '').overrideAttrs + (old: { + passthru = { + inherit impureOn; + generatorKind = "impure"; + }; + }); + # Pure generators are disabled for now + mkSecretGenerator = { script }: mkImpureSecretGenerator { inherit script; }; - # TODO: Implement consistent naming - # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type... - # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine. - # mkSecretGenerator = {script}: - # (prev.writeShellScript "generator.sh" '' - # #!/bin/sh - # set -eu - # # TODO: make nix daemon build secret, not just the script. - # cd /var/empty - # - # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") - # - # ${script} - # if ! test -d $out; then - # echo "impure generator script did not produce expected \$out output" - # exit 1 - # fi - # - # echo -n $created_at > $out/created_at - # echo -n SUCCESS > $out/marker - # '') - # .overrideAttrs (old: { - # passthru = { - # generatorKind = "pure"; - # }; - # # TODO: make nix daemon build secret, not just the script. - # # __impure = true; - # }); - }; + # TODO: Implement consistent naming + # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type... + # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine. + # mkSecretGenerator = {script}: + # (prev.writeShellScript "generator.sh" '' + # #!/bin/sh + # set -eu + # # TODO: make nix daemon build secret, not just the script. + # cd /var/empty + # + # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") + # + # ${script} + # if ! test -d $out; then + # echo "impure generator script did not produce expected \$out output" + # exit 1 + # fi + # + # echo -n $created_at > $out/created_at + # echo -n SUCCESS > $out/marker + # '') + # .overrideAttrs (old: { + # passthru = { + # generatorKind = "pure"; + # }; + # # TODO: make nix daemon build secret, not just the script. + # # __impure = true; + # }); + }; }) ]; }; --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -1,8 +1,9 @@ { callPackage, craneLib, -}: { - fleet = callPackage ./fleet.nix {inherit craneLib;}; - fleet-install-secrets = callPackage ./fleet-install-secrets.nix {inherit craneLib;}; - fleet-generator-helper = callPackage ./fleet-generator-helper.nix {inherit craneLib;}; +}: +{ + fleet = callPackage ./fleet.nix { inherit craneLib; }; + fleet-install-secrets = callPackage ./fleet-install-secrets.nix { inherit craneLib; }; + fleet-generator-helper = callPackage ./fleet-generator-helper.nix { inherit craneLib; }; } --- a/pkgs/fleet-generator-helper.nix +++ b/pkgs/fleet-generator-helper.nix @@ -1,4 +1,4 @@ -{craneLib}: +{ craneLib }: craneLib.buildPackage rec { pname = "fleet-generator-helper"; --- a/pkgs/fleet-install-secrets.nix +++ b/pkgs/fleet-install-secrets.nix @@ -1,4 +1,4 @@ -{craneLib}: +{ craneLib }: craneLib.buildPackage rec { pname = "fleet-install-secrets"; --- a/pkgs/fleet.nix +++ b/pkgs/fleet.nix @@ -10,7 +10,7 @@ cargoExtraArgs = "--locked -p ${pname}"; - nativeBuildInputs = [installShellFiles]; + nativeBuildInputs = [ installShellFiles ]; postInstall = '' for shell in bash fish zsh; do --- a/scripts/install-trusted-cert.sh +++ b/scripts/install-trusted-cert.sh @@ -11,7 +11,7 @@ echo remote_conf = \"\"\" echo "$remote_conf" echo \"\"\" -echo "$remote_conf" > "$edited_conf" +echo "$remote_conf" >"$edited_conf" sed -i 's/\. Do not edit it!/\. Then it was altered by install-trusted-cert. Do not edit!/g' "$edited_conf" sed -i "s|^trusted-public-keys =.*|& $pubkey|g" "$edited_conf" @@ -22,5 +22,5 @@ # Make nix.conf editable ssh "$1" sudo mv /etc/nix/nix.conf /etc/nix/nix.conf.bk ssh "$1" sudo cp /etc/nix/nix.conf.bk /etc/nix/nix.conf -ssh "$1" "cat | sudo dd of=/etc/nix/nix.conf" < "$edited_conf" +ssh "$1" "cat | sudo dd of=/etc/nix/nix.conf" <"$edited_conf" ssh "$1" sudo systemctl restart nix-daemon --- /dev/null +++ b/treefmt.nix @@ -0,0 +1,12 @@ +{ + settings.global.excludes = [ + "*.adoc" + "*.png" + "crates/nixlike/fuzz/.gitignore" + ]; + + programs.nixfmt.enable = true; + programs.shfmt.enable = true; + programs.rustfmt.enable = true; + programs.taplo.enable = true; +}