diff --git a/flake.nix b/flake.nix index 3c4b712d..6db4f549 100644 --- a/flake.nix +++ b/flake.nix @@ -240,6 +240,9 @@ machinesDir ? null, usersDir ? null, }: + let + utils = import ./lib { inherit usersDir rootDir machinesDir; }; + in { dirs = { lib = self + "/lib"; @@ -256,7 +259,11 @@ usersDir ; }; - utils = import ./lib { inherit usersDir rootDir machinesDir; }; + inherit utils; + }; + + modules = { + users = import ./modules/users.nix utils; }; }; }; diff --git a/modules/default.nix b/modules/default.nix index c9131a9f..05913ecf 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -6,5 +6,7 @@ ./folder-size-metrics ./shard-split ./random-alerts + ./host-info.nix + ./secrets.nix ]; } diff --git a/modules/host-info.nix b/modules/host-info.nix index c3a3ba47..a0d8b744 100644 --- a/modules/host-info.nix +++ b/modules/host-info.nix @@ -1,56 +1,71 @@ +{ withSystem, ... }: { - config, - lib, - ... -}: -{ - options.mcl.host-info = with lib; { - type = mkOption { - type = types.nullOr ( - types.enum [ - "desktop" - "server" - ] - ); - default = null; - example = [ "desktop" ]; - description = '' - Whether this host is a desktop or a server. - ''; - }; + flake.modules.nixos.mcl-host-info = + { + config, + lib, + ... + }: + { + options.mcl.host-info = with lib; { + type = mkOption { + type = types.nullOr ( + types.enum [ + "desktop" + "server" + "container" + ] + ); + default = null; + example = [ "desktop" ]; + description = '' + Whether this host is a desktop or a server. + ''; + }; - isVM = mkOption { - type = types.nullOr types.bool; - default = null; - example = [ "false" ]; - description = '' - Whether this configuration is a VM variant. - ''; - }; + isDebugVM = mkOption { + type = types.nullOr types.bool; + default = null; + example = [ "false" ]; + description = '' + Whether this configuration is a VM variant with extra debug + functionality. + ''; + }; + + configPath = mkOption { + type = types.nullOr types.path; + default = null; + example = [ "machines/server/solunska-server" ]; + description = '' + The configuration path for this host relative to the repo root. + ''; + }; - configPath = mkOption { - type = types.nullOr types.string; - default = null; - example = [ "machines/server/solunska-server" ]; - description = '' - The configuration path for this host relative to the repo root. - ''; + sshKey = mkOption { + type = types.nullOr types.str; + default = ""; + example = "ssh-ed25519 AAAAC3Nza"; + description = '' + The public ssh key for this host. + ''; + }; + }; + config = { + assertions = [ + { + assertion = config.mcl.host-info.type != null; + message = "mcl.host-info.type must be defined for every host"; + } + { + assertion = config.mcl.host-info.isDebugVM != null; + message = "mcl.host-info.isDebugVM must be defined for every host"; + } + { + assertion = config.mcl.host-info.configPath != null; + message = "mcl.host-info.configPath must be defined for every host"; + } + ]; + }; }; - }; - config = { - assertions = [ - { - assertion = config.mcl.host-info.type != null; - message = "mcl.host-info.type must be defined for every host"; - } - { - assertion = config.mcl.host-info.isVM != null; - message = "mcl.host-info.isVM must be defined for every host"; - } - { - assertion = config.mcl.host-info.configPath != null; - message = "mcl.host-info.configPath must be defined for every host"; - } - ]; - }; } diff --git a/modules/secrets.nix b/modules/secrets.nix new file mode 100644 index 00000000..ec4fa200 --- /dev/null +++ b/modules/secrets.nix @@ -0,0 +1,126 @@ +{ withSystem, inputs, ... }: +{ + flake.modules.nixos.mcl-secrets = + { + config, + options, + lib, + dirs, + ... + }: + let + eachServiceCfg = config.mcl.secrets.services; + isDebugVM = config.mcl.host-info.isDebugVM; + + sshKey = + if isDebugVM then + config.virtualisation.vmVariant.mcl.host-info.sshKey + else + config.mcl.host-info.sshKey; + + ageSecretOpts = builtins.head (builtins.head options.age.secrets.type.nestedTypes.elemType.getSubModules) + .imports; + + secretDir = + let + machineConfigPath = config.mcl.host-info.configPath; + machineSecretDir = machineConfigPath + "/secrets"; + vmConfig = dirs.modules + "/default-vm-config"; + vmSecretDir = vmConfig + "/secrets"; + in + if isDebugVM then vmSecretDir else machineSecretDir; + in + { + imports = [ + inputs.agenix.nixosModules.default + ]; + + options.mcl.secrets = with lib; { + services = mkOption { + type = types.attrsOf ( + types.submodule ( + { config, ... }: + let + serviceName = config._module.args.name; + in + { + options = { + encryptedSecretDir = mkOption { + type = types.path; + default = secretDir; + }; + secrets = mkOption { + default = { }; + type = types.attrsOf ( + types.submoduleWith { + modules = [ + ageSecretOpts + ( + { name, ... }: + let + secretName = name; + in + { + config = { + name = "${serviceName}/${secretName}"; + file = lib.mkDefault (config.encryptedSecretDir + "/${serviceName}/${secretName}.age"); + }; + } + ) + ]; + } + ); + }; + extraKeys = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "ssh-ed25519 AAAAC3Nza" + "ssh-ed25519 AAAACSNss" + ]; + description = "Extra keys which can decrypt the secrets."; + }; + nix-file = mkOption { + default = builtins.toFile "${serviceName}-secrets.nix" '' + let + hostKey = ["${sshKey}"]; + extraKeys = ["${concatStringsSep "\"\"" config.extraKeys}"]; + in { + ${concatMapStringsSep "\n" (n: "\"${n}.age\".publicKeys = hostKey ++ extraKeys;") ( + builtins.attrNames config.secrets + )} + } + ''; + type = types.path; + }; + }; + } + ) + ); + default = { }; + example = { + service1.secrets.secretA = { }; + service1.secrets.secretB = { }; + service2.secrets.secretC = { }; + cachix-deploy.secrets.token = { + path = "/etc/cachix-agent.token"; + }; + }; + description = mdDoc "Per-service attrset of encryptedSecretDir and secrets"; + }; + }; + + config = lib.mkIf (eachServiceCfg != { }) { + age.secrets = lib.pipe eachServiceCfg [ + (lib.mapAttrsToList ( + serviceName: service: + lib.mapAttrsToList ( + secretName: config: lib.nameValuePair "${serviceName}/${secretName}" config + ) service.secrets + )) + lib.concatLists + lib.listToAttrs + ]; + }; + }; +} diff --git a/modules/users.nix b/modules/users.nix index 43e5844c..7a89c9c5 100644 --- a/modules/users.nix +++ b/modules/users.nix @@ -1,8 +1,4 @@ -{ - usersDir, - rootDir, - machinesDir, -}: +utils: { config, lib, @@ -12,7 +8,6 @@ let cfg = config.users; enabled = cfg.includedUsers != [ ] || cfg.includedGroups != [ ]; - utils = import ../lib { inherit usersDir rootDir machinesDir; }; allUsers = utils.usersInfo; allGroups = let diff --git a/packages/default.nix b/packages/default.nix index b9981703..6972239b 100644 --- a/packages/default.nix +++ b/packages/default.nix @@ -61,6 +61,7 @@ } // optionalAttrs (system == "x86_64-linux" || system == "aarch64-darwin") { grafana-agent = import ./grafana-agent { inherit inputs'; }; + secret = import ./secret { inherit inputs' pkgs; }; } // optionalAttrs isLinux { folder-size-metrics = pkgs.callPackage ./folder-size-metrics { }; diff --git a/packages/secret/default.nix b/packages/secret/default.nix new file mode 100644 index 00000000..2a840e64 --- /dev/null +++ b/packages/secret/default.nix @@ -0,0 +1,113 @@ +{ + inputs', + pkgs, + ... +}: +let + agenix = inputs'.agenix.packages.agenix.override { ageBin = "${pkgs.rage}/bin/rage"; }; +in +pkgs.writeShellApplication { + name = "secret"; + text = '' + #!/usr/bin/env bash + set -euo pipefail + + machine="" + service="" + secret="" + vm=false + reEncrypt=false + reEncryptAll=false + export RULES="" + secretsFolder="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --machine=*) machine="''${1#*=}";; + --secrets-folder=*) secretsFolder="''${1#*=}";; + --service=*) service="''${1#*=}";; + --secret=*) secret="''${1#*=}";; + --vm) vm=true;; + -r) reEncrypt=true;; + --re-encrypt-all) reEncryptAll=true;; + --help) + echo -e "NAME\n\ + secret\n\n\ + SYNOPSIS\n\ + secret [OPTION]\n\n\ + EXAMPLE\n\ + secret --machine=mymachine --service=myservice --secret=mysecret\n\n\ + DESCRIPTION\n\ + Secret is the command made for nix repos to get rid of the secret.nix when\n\ + you are using agenix. Secret must be used with mcl-secrets and mcl-host-info\n\ + modules from nixos-modules repository to work properly.\n\n\ + OPTIONS\n\ + --secrets-folder - pecifies the location where secrets are saved.\n\ + By default, secrets are stored in /(folder of the machine)/secrets/service/\n\ + if this directory exists, unless otherwise specified. + --machine - Machine for which you want to create a secret.\n\ + --service - Service for which you want to create a secret.\n\ + --secret - Secret you want to encrypt.\n\ + --vm - Make secret for the vmVariant.\n\ + -r - Re-encrypt the secret." + exit 0 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac + shift + done + + if [[ "$reEncryptAll" == true && -z "$machine" ]]; then + echo "You must specify machine" + exit 1 + elif [[ "$reEncrypt" == true && (-z "$machine" || -z "$service") ]]; then + echo "You must specify machine and service" + exit 1 + elif [[ "$reEncrypt" == false && "$reEncryptAll" == false && (-z "$machine" || -z "$service" || -z "$secret") ]]; then + echo "You must specify machine, service, and secret" + exit 1 + fi + + machineFolder="$(nix eval ".#nixosConfigurations.$machine.config.mcl.host-info.configPath" | sed 's|^\([^/]*/\)\{4\}||; s|"||g')" + + if [ "$secretsFolder" == "" ]; then + secretsFolder="$machineFolder/secrets/$service" + fi + + if [[ "$vm" == true && "$reEncryptAll" == false ]]; then + RULES="$(nix eval --raw ".#nixosConfigurations.$machine.config.virtualisation.vmVariant.mcl.secrets.services.$service.nix-file")" + secretsFolder="./modules/default-vm-config/secrets/$service" + elif [ "$reEncryptAll" == false ]; then + RULES="$(nix eval --raw ".#nixosConfigurations.$machine.config.mcl.secrets.services.$service.nix-file")" + fi + + if [ "$reEncryptAll" == true ]; then + for s in $(nix eval ".#nixosConfigurations.$machine.config.mcl.secrets.services" --apply builtins.attrNames | tr -d '[]"'); do + service=$s + secretsFolder="$machineFolder/secrets/$service" + echo "Re-encripting secrets for service $s" + if [ "$vm" == true ]; then + RULES="$(nix eval --raw ".#nixosConfigurations.$machine.config.virtualisation.vmVariant.mcl.secrets.services.$service.nix-file")" + else + RULES="$(nix eval --raw ".#nixosConfigurations.$machine.config.mcl.secrets.services.$service.nix-file")" + fi + ( + cd "$secretsFolder" + "${agenix}/bin/agenix -r" + ) + done + else + ( + cd "$secretsFolder" + if [ "$reEncrypt" == true ]; then + "${agenix}/bin/agenix" -r + else + "${agenix}/bin/agenix" -e "$secret.age" + fi + ) + fi + ''; +}