--- a/flake.lock +++ b/flake.lock @@ -66,28 +66,11 @@ "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" } }, - "nixpkgs-stable-for-tests": { - "locked": { - "lastModified": 1721548954, - "narHash": "sha256-7cCC8+Tdq1+3OPyc3+gVo9dzUNkNIQfwSDJ2HSi2u3o=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "63d37ccd2d178d54e7fb691d7ec76000740ea24a", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { "crane": "crane", "flake-parts": "flake-parts", "nixpkgs": "nixpkgs", - "nixpkgs-stable-for-tests": "nixpkgs-stable-for-tests", "rust-overlay": "rust-overlay" } }, --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,6 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/master"; - nixpkgs-stable-for-tests.url = "github:nixos/nixpkgs/nixos-24.05"; rust-overlay = { url = "github:oxalica/rust-overlay"; inputs = { @@ -33,9 +32,9 @@ // { fleetConfiguration = throw "function-based interface is deprecated, use flake-parts syntax instead"; }; - flakeModules.default = (import ./lib/flakePart.nix { + flakeModules.default = import ./lib/flakePart.nix { inherit crane; - }); + }; flakeModule = flakeModules.default; # To be used with https://github.com/NixOS/nix/pull/8892 @@ -92,8 +91,7 @@ }; # Reference fleet package should be built with nightly rust, specified in rust-toolchain.toml. packages = lib.mkIf deployerSystem (let - packages = import ./pkgs { - inherit (pkgs) callPackage; + packages = pkgs.callPackages ./pkgs { inherit craneLib; }; in @@ -121,14 +119,7 @@ # fleet-install-secrets will not be built normally, because they are not ran directly by user most of the time. # checks there build packages for default nixpkgs rustPlatform packages. checks = let - packages = import ./pkgs { - inherit (pkgs) callPackage; - craneLib = crane.mkLib pkgs; - }; - packages-with-nixpkgs-stable = import ./pkgs { - inherit (pkgs) callPackage; - craneLib = crane.mkLib (import inputs.nixpkgs-stable-for-tests {inherit system;}); - }; + packages = pkgs.callPackages ./pkgs {}; prefixAttrs = prefix: attrs: mapAttrs' (name: value: { name = "${prefix}${name}"; @@ -139,8 +130,7 @@ attrs; in # `fleet` crate wants nightly rust, also little sense of supporting it on stable nixpkgs. - (prefixAttrs "nixpkgs-" (removeAttrs packages ["fleet"])) - // (prefixAttrs "nixpkgs-stable-" (removeAttrs packages-with-nixpkgs-stable ["fleet"])); + (prefixAttrs "nixpkgs-" (removeAttrs packages ["fleet"])); formatter = pkgs.alejandra; }; }; --- a/lib/default.nix +++ b/lib/default.nix @@ -4,7 +4,7 @@ inherit (lib.options) mkOption mergeOneOption; inherit (lib.modules) mkOverride; inherit (lib.types) listOf submodule attrsOf mkOptionType; - inherit (lib.strings) optionalString; + inherit (lib.strings) optionalString hasPrefix removePrefix; in rec { types = { overlay = mkOptionType { @@ -16,6 +16,7 @@ listOfOverlay = listOf types.overlay; mkHostsType = module: attrsOf (submodule module); + mkDataType = module: submodule module; }; options = { @@ -23,6 +24,10 @@ mkOption { type = types.mkHostsType module; }; + mkDataOption = module: + mkOption { + type = types.mkDataType module; + }; }; inherit (options) mkHostsOption; @@ -118,4 +123,18 @@ }; inherit (secrets) mkPassword mkEd25519 mkX25519 mkRsa mkBytes mkHexBytes mkBase64Bytes; + + strings = let + plaintextPrefix = ""; + plaintextNewlinePrefix = "<PLAINTEXT-NL>"; + in { + 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 @@ -7,6 +7,7 @@ inherit (lib.options) mkOption; inherit (lib.attrsets) mapAttrs; inherit (lib.types) lazyAttrsOf deferredModule unspecified; + inherit (lib.strings) isPath; inherit (fleetLib.options) mkHostsOption; in { options.fleetModules = mkOption { @@ -36,19 +37,25 @@ bootstrapNixpkgs = bootstrapEval.config.nixpkgs.buildUsing; normalEval = bootstrapNixpkgs.lib.evalModules { modules = - (import ../modules/fleet/_modules.nix) + (import ../modules/module-list.nix) ++ [ - data module { options.hosts = mkHostsOption { nixos.nixpkgs.overlays = [ - (final: prev: { - # FIXME: make this name not conflicting - craneLib = crane.mkLib prev; - }) + (final: prev: + import ../pkgs { + inherit (prev) callPackage; + craneLib = crane.mkLib prev; + }) ]; }; + config = { + data = + if isPath data + then import data + else data; + }; } ]; specialArgs.fleetLib = import ../lib { --- /dev/null +++ b/modules/assertions.nix @@ -0,0 +1,64 @@ +{ + lib, + config, + ... +}: let + inherit (lib.options) mkOption; + inherit (lib.types) listOf unspecified str; + inherit (lib.lists) map filter; + + errors = mkOption { + type = listOf str; + internal = true; + description = '' + Similar to warnings, however build will fail if any error exists. + ''; + }; +in { + options = { + assertions = mkOption { + type = listOf unspecified; + internal = true; + default = []; + example = [ + { + assertion = false; + message = "you can't enable this for that reason"; + } + ]; + description = '' + This option allows modules to express conditions that must + hold for the evaluation of the system configuration to + succeed, along with associated error messages for the user. + ''; + }; + + warnings = mkOption { + internal = true; + default = []; + type = listOf str; + 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. + ''; + }; + + inherit errors; + }; + config = { + errors = + map (v: v.message) + (filter (v: !v.assertion) config.assertions); + + nixos = {config, ...}: { + _file = ./assertions.nix; + options = { + inherit errors; + }; + config.errors = + map (v: v.message) + (filter (v: !v.assertion) config.assertions); + }; + }; +} --- a/modules/fleet/_modules.nix +++ /dev/null @@ -1,9 +0,0 @@ -[ - ./assertions.nix - ./fleetLib.nix - ./hosts.nix - ./meta.nix - ./nixos.nix - ./nixpkgs.nix - ./secrets.nix -] --- a/modules/fleet/assertions.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ - lib, - config, - ... -}: let - inherit (lib.options) mkOption; - inherit (lib.types) listOf unspecified str; - inherit (lib.lists) map filter; -in { - options = { - assertions = mkOption { - type = listOf unspecified; - internal = true; - default = []; - example = [ - { - assertion = false; - message = "you can't enable this for that reason"; - } - ]; - description = '' - This option allows modules to express conditions that must - hold for the evaluation of the system configuration to - succeed, along with associated error messages for the user. - ''; - }; - - warnings = mkOption { - internal = true; - default = []; - type = listOf str; - 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. - ''; - }; - errors = mkOption { - type = listOf str; - internal = true; - description = '' - Similar to warnings, however build will fail if any error exists. - ''; - }; - }; - config.errors = - map (v: v.message) - (filter (v: !v.assertion) config.assertions); -} --- a/modules/fleet/fleetLib.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ - lib, - config, - ... -}: { - _module.args.fleetLib = import ../../lib { - inherit lib; - }; -} --- a/modules/fleet/hosts.nix +++ /dev/null @@ -1,40 +0,0 @@ -{ - lib, - fleetLib, - ... -}: let - inherit (fleetLib.modules) mkFleetGeneratorDefault; - inherit (fleetLib.types) mkHostsType; - inherit (lib.options) mkOption; - inherit (lib.types) str listOf; -in { - options = { - hosts = mkOption { - type = mkHostsType ({config, ...}: { - options = { - system = mkOption { - type = str; - description = "Type of the system."; - }; - # TODO: This is part of fleet.nix, move it to separate toplevel data config option. - encryptionKey = mkOption { - type = str; - description = "Rage SSH encryption key for secrets."; - }; - tags = mkOption { - type = listOf str; - description = "Host tag. In CLI, you can refer to all hosts having this tag using @tag syntax."; - }; - }; - config = { - nixos.networking.hostName = mkFleetGeneratorDefault config._module.args.name; - tags = ["all"]; - }; - _file = ./meta.nix; - }); - default = {}; - description = "Configurations of individual hosts"; - }; - }; - _file = ./meta.nix; -} --- a/modules/fleet/meta.nix +++ /dev/null @@ -1,8 +0,0 @@ -{lib, ...}: let - inherit (lib.modules) mkRemovedOptionModule; -in { - imports = [ - (mkRemovedOptionModule ["fleetModules"] "replaced with imports.") - (mkRemovedOptionModule ["data"] "data is now provided by fleet itself, you can remove your import.") - ]; -} --- a/modules/fleet/nixos.nix +++ /dev/null @@ -1,55 +0,0 @@ -{ - lib, - fleetLib, - config, - ... -}: let - inherit (lib.attrsets) mapAttrs; - inherit (lib.options) mkOption; - inherit (lib.types) deferredModule deferredModuleWith; - inherit (lib.modules) mkRemovedOptionModule; - inherit (fleetLib.options) mkHostsOption; - - _file = ./nixos.nix; -in { - options = { - nixos = mkOption { - description = '' - Nixos configuration for all hosts. - ''; - type = deferredModule; - }; - hosts = mkHostsOption (hostArgs: { - inherit _file; - options = { - nixos = mkOption { - description = '' - Nixos configuration for the current host. - ''; - type = deferredModuleWith { - staticModules = import ../../nixos/modules/module-list.nix; - }; - apply = module: - config.nixpkgs.buildUsing.lib.nixosSystem { - inherit (hostArgs.config) system; - modules = [module]; - }; - }; - }; - config = { - # imports = [ - # (mkRemovedOptionModule ["nixosModules"] "replaced with hosts.*.nixos.imports.") - # ]; - nixos = { - imports = [ - config.nixos - ]; - config._module.args.fleet = mapAttrs (_: value: value.nixos.config) config.hosts; - }; - }; - }); - }; - imports = [ - (mkRemovedOptionModule ["nixosModules"] "replaced with nixos.imports.") - ]; -} --- a/modules/fleet/nixpkgs.nix +++ /dev/null @@ -1,58 +0,0 @@ -{ - lib, - fleetLib, - config, - ... -}: let - inherit (lib.options) mkOption; - inherit (lib.types) path; - inherit (lib.modules) mkRemovedOptionModule; - inherit (fleetLib.options) mkHostsOption; - inherit (fleetLib.types) listOfOverlay; - - _file = ./nixpkgs.lib; -in { - options = { - nixpkgs = { - buildUsing = mkOption { - description = '' - Default nixpkgs to use for building the systems. - ''; - type = path; - }; - overlays = mkOption { - description = '' - Package overlays to apply for all the hosts, gets propagated into - `hosts.*.nixosModules.nixpkgs.overlays`. - ''; - type = listOfOverlay; - }; - }; - hosts = mkHostsOption { - inherit _file; - options.nixpkgs.buildUsing = mkOption { - description = '' - Nixpkgs to use for building the system. - - Note that this option is defined at the host level, not the nixosModules level, - nixosModules will be evaluated using this flake input. - ''; - type = path; - default = config.nixpkgs.buildUsing; - }; - # imports = [ - # (mkRemovedOptionModule ["nixpkgs" "overlays"] "this option needs to be specified at nixosModules level") - # ]; - config.nixos = { - inherit _file; - nixpkgs.overlays = config.nixpkgs.overlays; - imports = [ - (mkRemovedOptionModule ["nixpkgs" "buildUsing"] "this option should be specified at the host level, not the nixosModules level") - ]; - }; - }; - }; - config.nixpkgs.overlays = [ - (final: prev: import ../../pkgs {inherit (final) callPackage craneLib;}) - ]; -} --- a/modules/fleet/secrets.nix +++ /dev/null @@ -1,210 +0,0 @@ -{ - lib, - fleetLib, - config, - ... -}: let - inherit (fleetLib.options) mkHostsOption; - inherit (lib.options) mkOption; - inherit (lib.types) lazyAttrsOf unspecified nullOr listOf str bool attrsOf submodule; - inherit (lib.lists) sort elem; - inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs; - inherit (lib.strings) toJSON concatStringsSep; - - sharedSecret = {config, ...}: { - freeformType = lazyAttrsOf unspecified; - options = { - 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) - - Secrets would be decrypted and stored to /run/secrets/$\{name} on owners - ''; - 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. - - You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs. - ''; - }; - regenerateOnOwnerRemoved = mkOption { - default = config.regenerateOnOwnerAdded; - type = bool; - description = '' - Should this secret be removed on owner removal, or it may be just reencrypted - - 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. - ''; - }; - generator = mkOption { - type = nullOr unspecified; - description = "Derivation to evaluate for secret generation"; - default = null; - }; - createdAt = mkOption { - type = nullOr str; - description = "When this secret was (re)generated"; - default = null; - }; - expiresAt = mkOption { - type = nullOr str; - description = "On which date this secret will expire, someone should regenerate this secret before it expires."; - 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. - - Imported from fleet.nix - ''; - default = []; - }; - }; - }; - hostSecret = { - freeformType = lazyAttrsOf unspecified; - options = { - createdAt = mkOption { - type = nullOr str; - default = null; - }; - expiresAt = mkOption { - type = nullOr str; - default = null; - }; - }; - }; - inherit (config) hostSecrets sharedSecrets; -in { - options = { - version = mkOption { - type = str; - internal = true; - }; - sharedSecrets = mkOption { - type = attrsOf (submodule sharedSecret); - default = {}; - description = "Shared secrets"; - }; - hostSecrets = mkOption { - type = attrsOf (attrsOf (submodule hostSecret)); - default = {}; - description = "Host secrets. Imported from fleet.nix"; - internal = true; - }; - hosts = mkHostsOption ({config, ...}: { - nixos = { - secrets = let - host = config._module.args.name; - processSecret = v: - (removeAttrs v ["createdAt" "expiresAt" "expectedOwners" "owners" "regenerateOnOwnerAdded" "regenerateOnOwnerRemoved"]) - // { - shared = true; - }; - in - ( - mapAttrs (_: processSecret) - (filterAttrs (_: v: elem host v.owners) sharedSecrets) - ) - // (mapAttrs (_: processSecret) (hostSecrets.${host} or {})); - _file = ./secrets.nix; - }; - }); - }; - config = { - assertions = - mapAttrsToList - (name: secret: { - assertion = secret.expectedOwners == null || sort (a: b: a < b) secret.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 secret.owners}. Run fleet secrets regenerate to fix"; - }) - config.sharedSecrets; - 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 - - 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 - - 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 = { - 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; - # }); - }; - }) - ]; - }; -} --- /dev/null +++ b/modules/fleetLib.nix @@ -0,0 +1,9 @@ +{ + lib, + config, + ... +}: { + _module.args.fleetLib = import ../../lib { + inherit lib; + }; +} --- /dev/null +++ b/modules/hosts.nix @@ -0,0 +1,75 @@ +{ + lib, + fleetLib, + ... +}: let + inherit (fleetLib.modules) mkFleetGeneratorDefault; + inherit (fleetLib.types) mkHostsType mkDataType; + inherit (lib.options) mkOption; + inherit (lib.types) str listOf attrsOf submodule; +in { + options = { + data = mkOption { + type = mkDataType { + options = { + version = mkOption { + type = str; + internal = true; + }; + hosts = mkOption { + type = attrsOf (submodule { + options.encryptionKey = mkOption { + type = str; + description = "Rage SSH encryption key for secrets."; + }; + }); + }; + }; + }; + description = '' + Configuration provided from outside. + Usually used to persist fleet data between runs. + ''; + }; + hosts = mkOption { + type = mkHostsType ({config, ...}: { + options = { + system = mkOption { + description = "Type of the system."; + 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."; + type = listOf str; + }; + network = mkOption { + type = submodule { + options = { + internalIps = mkOption { + description = "Internal ips"; + type = listOf str; + default = []; + }; + externalIps = mkOption { + description = "External ips"; + type = listOf str; + default = []; + }; + }; + }; + description = "Network definition of host"; + }; + }; + config = { + nixos.networking.hostName = mkFleetGeneratorDefault config._module.args.name; + tags = ["all"]; + }; + _file = ./meta.nix; + }); + default = {}; + description = "Configurations of individual hosts"; + }; + }; + _file = ./meta.nix; +} --- /dev/null +++ b/modules/meta.nix @@ -0,0 +1,7 @@ +{lib, ...}: let + inherit (lib.modules) mkRemovedOptionModule; +in { + imports = [ + (mkRemovedOptionModule ["fleetModules"] "replaced with imports.") + ]; +} --- /dev/null +++ b/modules/module-list.nix @@ -0,0 +1,10 @@ +[ + ./assertions.nix + ./fleetLib.nix + ./hosts.nix + ./meta.nix + ./nixos.nix + ./nixpkgs.nix + ./secrets.nix + ./secrets-data.nix +] --- /dev/null +++ b/modules/nixos.nix @@ -0,0 +1,62 @@ +{ + lib, + fleetLib, + config, + ... +}: let + inherit (lib.attrsets) mapAttrs; + inherit (lib.options) mkOption; + inherit (lib.types) deferredModule; + inherit (lib.modules) mkRemovedOptionModule; + inherit (fleetLib.options) mkHostsOption; + + _file = ./nixos.nix; +in { + options = { + nixos = mkOption { + description = '' + Nixos configuration for all hosts. + ''; + type = deferredModule; + }; + hosts = mkHostsOption (hostArgs: { + inherit _file; + options = { + nixos = mkOption { + description = '' + Nixos configuration for the current host. + ''; + type = deferredModule; + apply = module: + config.nixpkgs.buildUsing.lib.nixosSystem { + inherit (hostArgs.config) system; + modules = [ + (module // {key = "attr<host.nixos>";}) + (config.nixos // {key = "attr<fleet.nixos>";}) + ]; + specialArgs = { + inherit fleetLib; + }; + }; + }; + }; + config = { + # imports = [ + # (mkRemovedOptionModule ["nixosModules"] "replaced with hosts.*.nixos.imports.") + # ]; + nixos = { + config._module.args = { + nixosHosts = mapAttrs (_: value: value.nixos.config) config.hosts; + hosts = config.hosts; + host = hostArgs.config; + }; + }; + }; + }); + }; + imports = [ + (mkRemovedOptionModule ["nixosModules"] "replaced with nixos.imports.") + ]; + config.nixos.imports = + import ./nixos/module-list.nix; +} --- /dev/null +++ b/modules/nixos/meta.nix @@ -0,0 +1,24 @@ +{ + lib, + pkgs, + ... +}: let + inherit (lib.options) mkOption; + inherit (lib.modules) mkRemovedOptionModule; +in { + options = { + # TODO: Give a real name. + # Previously it was nixpkgs.resolvedPkgs, which was erroreously merged with nixpkgs override attribute. + _resolvedPkgs = mkOption { + type = lib.types.pkgs // {description = "nixpkgs.pkgs";}; + description = "Value of pkgs"; + }; + }; + imports = [ + (mkRemovedOptionModule ["tags"] "tags are now defined at the host level, not the nixos system level for fast filtering without evaluating unnecessary hosts.") + (mkRemovedOptionModule ["network"] "network is now defined at the host level, not the nixos system level") + ]; + config = { + _resolvedPkgs = pkgs; + }; +} --- /dev/null +++ b/modules/nixos/module-list.nix @@ -0,0 +1,6 @@ +[ + ./meta.nix + ./secrets.nix + ./rollback.nix + ./nix-sign.nix +] --- /dev/null +++ b/modules/nixos/nix-sign.nix @@ -0,0 +1,21 @@ +# Required for nix copy in build_systems.rs +{ + lib, + config, + ... +}: let + inherit (lib.modules) mkIf; + hasPersistentHostname = config.networking.hostName != ""; +in { + # https://github.com/NixOS/nix/issues/3023 + systemd.services.generate-nix-cache-key = mkIf hasPersistentHostname { + wantedBy = ["multi-user.target"]; + serviceConfig.Type = "oneshot"; + 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 + ''; + }; + nix.settings.secret-key-files = mkIf hasPersistentHostname "/etc/nix/private-key"; +} --- /dev/null +++ b/modules/nixos/rollback.nix @@ -0,0 +1,48 @@ +# Tied to build_systems.rs +{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/ + # whatever user uses. boot.json also might help here. + + systemd.services.rollback-watchdog = { + description = "Rollback watchdog"; + script = '' + set -eux + if [ -f /etc/fleet_rollback_marker ]; then + echo "found the rollback marker, switching to older generation" + target=$(cat /etc/fleet_rollback_marker) + echo "rolling back profile" + nix profile rollback --profile /nix/var/nix/profiles/system --to "$target" + echo "executing activation script" + "/nix/var/nix/profiles/system-$target-link/bin/switch-to-configuration" switch || true + echo "removing rollback marker" + rm -f /etc/fleet_rollback_marker + else + echo "rollback marker was removed, upgrade is succeeded" + fi + ''; + path = [ + # Should have nix-command support + config.nix.package + ]; + serviceConfig.Type = "exec"; + unitConfig = { + X-StopOnRemoval = false; + X-RestartIfChanged = false; + X-StopIfChanged = false; + }; + }; + + systemd.timers.rollback-watchdog = { + description = "Timer for rollback watchdog"; + wantedBy = ["timers.target"]; + timerConfig = { + OnActiveSec = "3min"; + RemainAfterElapse = false; + }; + unitConfig = { + ConditionPathExists = "/etc/fleet_rollback_marker"; + }; + }; +} --- /dev/null +++ b/modules/nixos/secrets.nix @@ -0,0 +1,155 @@ +{ + lib, + fleetLib, + config, + pkgs, + ... +}: let + inherit (builtins) hashString; + inherit (lib.stringsWithDeps) stringAfter; + inherit (lib.options) mkOption; + inherit (lib.lists) optional; + inherit (lib.attrsets) mapAttrs; + inherit (lib.modules) mkIf; + inherit (lib.types) submodule str attrsOf nullOr unspecified lazyAttrsOf; + inherit (fleetLib.strings) decodeRawSecret; + + sysConfig = config; + 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 = { + shared = mkOption { + description = "Is this secret owned by this machine, or propagated from shared secrets"; + default = false; + }; + + generator = mkOption { + type = nullOr unspecified; + 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; + }; + }; + }); + processPart = part: { + inherit (part) raw path stablePath; + }; + processSecret = secret: + { + inherit (secret) group mode owner; + } + // (mapAttrs (_: processPart) (removeAttrs secret [ + "shared" + "generator" + "mode" + "group" + "owner" + ])); + secretsFile = pkgs.writeTextFile { + name = "secrets.json"; + text = + builtins.toJSON (mapAttrs (_: processSecret) + config.secrets); + }; + useSysusers = (config.systemd ? sysusers && config.systemd.sysusers.enable) || (config ? userborn && config.userborn.enable); +in { + options = { + secrets = mkOption { + type = attrsOf secretType; + default = {}; + description = "Host-local secrets"; + }; + }; + config = { + environment.systemPackages = [pkgs.fleet-install-secrets]; + + systemd.services.fleet-install-secrets = mkIf useSysusers { + wantedBy = ["sysinit.target"]; + after = ["systemd-sysusers.service"]; + restartTriggers = [ + secretsFile + ]; + aliases = [ + "sops-install-secrets" + "agenix-install-secrets" + ]; + + unitConfig.DefaultDependencies = false; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}"; + }; + }; + system.activationScripts.decryptSecrets = + mkIf (!useSysusers) + ( + stringAfter ( + [ + # secrets are owned by user/group, thus we need to refer to those + "users" + "groups" + "specialfs" + ] + # nixos-impermanence compatibility: secrets are encrypted by host-key, + # 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} + '' + ); + }; +} --- /dev/null +++ b/modules/nixpkgs.nix @@ -0,0 +1,55 @@ +{ + lib, + fleetLib, + config, + ... +}: let + inherit (lib.options) mkOption; + inherit (lib.types) path; + inherit (lib.modules) mkRemovedOptionModule; + inherit (fleetLib.options) mkHostsOption; + inherit (fleetLib.types) listOfOverlay; + + _file = ./nixpkgs.lib; +in { + options = { + nixpkgs = { + buildUsing = mkOption { + description = '' + Default nixpkgs to use for building the systems. + ''; + type = path; + }; + overlays = mkOption { + description = '' + Package overlays to apply for all the hosts, gets propagated into + `hosts.*.nixosModules.nixpkgs.overlays`. + ''; + type = listOfOverlay; + }; + }; + hosts = mkHostsOption { + inherit _file; + options.nixpkgs.buildUsing = mkOption { + description = '' + Nixpkgs to use for building the system. + + Note that this option is defined at the host level, not the nixosModules level, + nixosModules will be evaluated using this flake input. + ''; + type = path; + default = config.nixpkgs.buildUsing; + }; + # imports = [ + # (mkRemovedOptionModule ["nixpkgs" "overlays"] "this option needs to be specified at nixosModules level") + # ]; + config.nixos = { + inherit _file; + nixpkgs.overlays = config.nixpkgs.overlays; + imports = [ + (mkRemovedOptionModule ["nixpkgs" "buildUsing"] "this option should be specified at the host level, not the nixosModules level") + ]; + }; + }; + }; +} --- /dev/null +++ b/modules/secrets-data.nix @@ -0,0 +1,105 @@ +{ + lib, + fleetLib, + config, + ... +}: let + inherit (fleetLib.options) mkDataOption; + inherit (lib.options) mkOption; + inherit (lib.types) lazyAttrsOf nullOr listOf str attrsOf submodule bool; + inherit (lib.attrsets) mapAttrsToList mapAttrs catAttrs filterAttrs genAttrs; + inherit (lib.lists) sort unique concatLists; + inherit (lib.strings) toJSON; + + secretDataValue = { + options = { + raw = mkOption { + type = nullOr str; + description = "Encrypted + encoded secret data"; + default = null; + }; + }; + }; + + sharedSecretData = { + freeformType = attrsOf (submodule secretDataValue); + options = { + createdAt = mkOption { + type = str; + description = "When this secret was (re)generated"; + default = null; + }; + expiresAt = mkOption { + type = nullOr str; + description = "On which date this secret will expire, someone should regenerate this secret before it expires."; + 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. + + Imported from fleet.nix + ''; + default = []; + }; + }; + }; + + hostSecretData = { + freeformType = attrsOf (submodule secretDataValue); + options = { + createdAt = mkOption { + type = str; + description = "When this secret was (re)generated"; + default = null; + }; + expiresAt = mkOption { + type = nullOr str; + description = "On which date this secret will expire, someone should regenerate this secret before it expires."; + default = null; + }; + shared = mkOption { + type = bool; + description = "On which date this secret will expire, someone should regenerate this secret before it expires."; + default = false; + }; + }; + }; +in { + options.data = mkDataOption ({config, ...}: { + options = { + sharedSecrets = mkOption { + type = attrsOf (submodule sharedSecretData); + default = {}; + description = "Stored shared secret data."; + }; + hostSecrets = mkOption { + type = attrsOf (attrsOf (submodule hostSecretData)); + default = {}; + description = "Host 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 = { + 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; + sharedSecrets = + mapAttrs (_: _: {}) config.data.sharedSecrets; + }; +} --- /dev/null +++ b/modules/secrets.nix @@ -0,0 +1,134 @@ +{lib, config, ...}: let + inherit (lib.options) mkOption; + inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule; + inherit (lib.strings) concatStringsSep; + inherit (lib.attrsets) mapAttrs; + + sharedSecret = {config, ...}: { + options = { + 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) + + Secrets would be decrypted and stored to /run/secrets/$\{name} on owners + ''; + 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. + + You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs. + ''; + }; + regenerateOnOwnerRemoved = mkOption { + default = config.regenerateOnOwnerAdded; + type = bool; + description = '' + Should this secret be removed on owner removal, or it may be just reencrypted + + 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. + ''; + }; + generator = mkOption { + type = nullOr unspecified; + description = "Derivation to evaluate for secret generation"; + default = null; + }; + }; + }; +in { + options = { + sharedSecrets = mkOption { + type = attrsOf (submodule sharedSecret); + default = {}; + description = "Shared secrets"; + }; + }; + config = { + hosts = mapAttrs (_: secretMap: { + nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt"]) 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 + + 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 + + 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 = { + 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; + # }); + }; + }) + ]; + }; +} --- a/nixos/assertions.nix +++ /dev/null @@ -1,24 +0,0 @@ -# Similar module exists for fleet, however it also defines assertions and warnings, -# which are already defined for nixos. -{ - lib, - config, - ... -}: let - inherit (lib.options) mkOption; - inherit (lib.lists) map filter; - inherit (lib.types) listOf str; -in { - options = { - errors = mkOption { - type = listOf str; - internal = true; - description = '' - Similar to warnings, however build will fail if any error exists. - ''; - }; - }; - config.errors = - map (v: v.message) - (filter (v: !v.assertion) config.assertions); -} --- a/nixos/meta.nix +++ /dev/null @@ -1,42 +0,0 @@ -{ - lib, - pkgs, - ... -}: let - inherit (lib.options) mkOption; - inherit (lib.types) listOf str submodule; - inherit (lib.modules) mkRemovedOptionModule; -in { - options = { - # TODO: Give a real name. - # Previously it was nixpkgs.resolvedPkgs, which was erroreously merged with nixpkgs override attribute. - _resolvedPkgs = mkOption { - type = lib.types.pkgs // {description = "nixpkgs.pkgs";}; - description = "Value of pkgs"; - }; - network = mkOption { - type = submodule { - options = { - internalIps = mkOption { - type = listOf str; - description = "Internal ips"; - default = []; - }; - externalIps = mkOption { - type = listOf str; - description = "External ips"; - default = []; - }; - }; - }; - description = "Network definition of host"; - }; - }; - imports = [ - (mkRemovedOptionModule ["tags"] "tags are now defined at the host level, not the nixos system level for fast filtering without evaluating unnecessary hosts.") - ]; - config = { - network = {}; - _resolvedPkgs = pkgs; - }; -} --- a/nixos/modules/module-list.nix +++ /dev/null @@ -1,7 +0,0 @@ -[ - ../assertions.nix - ../meta.nix - ../secrets.nix - ../rollback.nix - ../nix-sign.nix -] --- a/nixos/nix-sign.nix +++ /dev/null @@ -1,19 +0,0 @@ -# Required for nix copy in build_systems.rs -{lib, config, ...}: -let - inherit (lib.modules) mkIf; - hasPersistentHostname = config.networking.hostName != ""; -in -{ - # https://github.com/NixOS/nix/issues/3023 - systemd.services.generate-nix-cache-key = mkIf hasPersistentHostname { - wantedBy = ["multi-user.target"]; - serviceConfig.Type = "oneshot"; - 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 - ''; - }; - nix.settings.secret-key-files = mkIf hasPersistentHostname "/etc/nix/private-key"; -} --- a/nixos/rollback.nix +++ /dev/null @@ -1,48 +0,0 @@ -# Tied to build_systems.rs -{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/ - # whatever user uses. boot.json also might help here. - - systemd.services.rollback-watchdog = { - description = "Rollback watchdog"; - script = '' - set -eux - if [ -f /etc/fleet_rollback_marker ]; then - echo "found the rollback marker, switching to older generation" - target=$(cat /etc/fleet_rollback_marker) - echo "rolling back profile" - nix profile rollback --profile /nix/var/nix/profiles/system --to "$target" - echo "executing activation script" - "/nix/var/nix/profiles/system-$target-link/bin/switch-to-configuration" switch || true - echo "removing rollback marker" - rm -f /etc/fleet_rollback_marker - else - echo "rollback marker was removed, upgrade is succeeded" - fi - ''; - path = [ - # Should have nix-command support - config.nix.package - ]; - serviceConfig.Type = "exec"; - unitConfig = { - X-StopOnRemoval = false; - X-RestartIfChanged = false; - X-StopIfChanged = false; - }; - }; - - systemd.timers.rollback-watchdog = { - description = "Timer for rollback watchdog"; - wantedBy = ["timers.target"]; - timerConfig = { - OnActiveSec = "3min"; - RemainAfterElapse = false; - }; - unitConfig = { - ConditionPathExists = "/etc/fleet_rollback_marker"; - }; - }; -} --- a/nixos/secrets.nix +++ /dev/null @@ -1,168 +0,0 @@ -{ - lib, - config, - pkgs, - ... -}: let - inherit (lib.strings) hasPrefix removePrefix; - inherit (lib.stringsWithDeps) stringAfter; - inherit (lib.options) mkOption; - inherit (lib.lists) optional; - inherit (lib.attrsets) mapAttrs; - inherit (lib.modules) mkOptionDefault mkIf; - inherit (lib.types) submodule str attrsOf nullOr unspecified lazyAttrsOf; - plaintextPrefix = "<PLAINTEXT>"; - plaintextNewlinePrefix = "<PLAINTEXT-NL>"; - - sysConfig = config; - secretPartType = secretName: - submodule ({config, ...}: { - options = { - raw = mkOption { - description = "Secret in fleet-specific undocumented format, do not use. Import from fleet.nix"; - internal = true; - }; - 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 = let - partName = config._module.args.name; - in { - hash = mkOptionDefault (builtins.hashString "sha1" config.raw); - data = mkOptionDefault ( - if hasPrefix plaintextPrefix config.raw - then removePrefix plaintextPrefix config.raw - else if hasPrefix plaintextNewlinePrefix config.raw - then removePrefix plaintextNewlinePrefix config.raw - else throw "secret.part.data attribute only works for public plaintext secret parts, got ${config.raw}" - ); - path = mkOptionDefault "/run/secrets/${secretName}/${config.hash}-${partName}"; - stablePath = mkOptionDefault "/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; - }; - expectedOwners = mkOption { - type = nullOr unspecified; - default = null; - internal = true; - }; - - generator = mkOption { - type = nullOr unspecified; - 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; - }; - }; - }); - processPart = part: { - inherit (part) raw path stablePath; - }; - processSecret = secret: - { - inherit (secret) group mode owner; - } - // (mapAttrs (_: processPart) (removeAttrs secret [ - "shared" - "generator" - "mode" - "group" - "owner" - - # FIXME: Some of those removed attributes shouldn't be here, but there is some error in passing shared secrets from fleet to nixos. - "expectedOwners" - ])); - secretsFile = pkgs.writeTextFile { - name = "secrets.json"; - text = - builtins.toJSON (mapAttrs (_: processSecret) - config.secrets); - }; - useSysusers = (config.systemd ? sysusers && config.systemd.sysusers.enable) || (config ? userborn && config.userborn.enable); -in { - options = { - secrets = mkOption { - type = attrsOf secretType; - default = {}; - description = "Host-local secrets"; - }; - }; - config = { - environment.systemPackages = [pkgs.fleet-install-secrets]; - - systemd.services.fleet-install-secrets = mkIf useSysusers { - wantedBy = ["sysinit.target"]; - after = ["systemd-sysusers.service"]; - restartTriggers = [ - secretsFile - ]; - aliases = [ - "sops-install-secrets" - "agenix-install-secrets" - ]; - - unitConfig.DefaultDependencies = false; - - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = "${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}"; - }; - }; - system.activationScripts.decryptSecrets = - mkIf (!useSysusers) - ( - stringAfter ( - [ - # secrets are owned by user/group, thus we need to refer to those - "users" - "groups" - "specialfs" - ] - # nixos-impermanence compatibility: secrets are encrypted by host-key, - # 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} - '' - ); - }; -}