git.delta.rocks / jrsonnet / refs/commits / 3bff084b3bc8

difftreelog

source

modules/nixos/secrets.nix7.3 KiBsourcehistory
1{2  lib,3  fleetLib,4  config,5  pkgs,6  ...7}:8let9  inherit (builtins) hashString elemAt length toJSON filter;10  inherit (lib.stringsWithDeps) stringAfter;11  inherit (lib.options) mkOption literalExpression;12  inherit (lib.lists) optional;13  inherit (lib.attrsets) mapAttrs mapAttrsToList;14  inherit (lib.modules) mkIf;15  inherit (lib.types)16    submodule17    str18    attrsOf19    nullOr20    unspecified21    lazyAttrsOf22    uniq23    functionTo24    package25    listOf26    ;27  inherit (fleetLib.strings) decodeRawSecret;2829  sysConfig = config;30  secretPartDataType = submodule {31    options = {32      raw = mkOption {33        type = str;34        internal = true;35        description = "Encoded & Encrypted secret part data, passed from fleet.nix";36      };37    };38  };39  secretDataType = submodule {40    freeformType = lazyAttrsOf secretPartDataType;41    options = {42      shared = mkOption {43        description = "Is this secret owned by this machine, or propagated from shared secrets";44        default = false;45      };46    };47  };48  secretPartType =49    secretName:50    submodule (51      { config, ... }:52      let53        partName = config._module.args.name;54      in55      {56        options = {57          hash = mkOption {58            type = str;59            description = "Hash of secret in encoded format";60          };61          path = mkOption {62            type = str;63            description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)";64          };65          stablePath = mkOption {66            type = str;67            description = "Path to secret part, stable path (users are expected to watch for file changes/re-read secret on demand)";68          };69          data = mkOption {70            type = str;71            description = "Secret public data (only available for plaintext)";72          };73        };74        config =75          let76            raw = sysConfig.data.secrets.${secretName}.${partName}.raw;77          in78          {79            hash = hashString "sha1" raw;80            data = decodeRawSecret raw;81            path = "/run/secrets/${secretName}/${config.hash}-${partName}";82            stablePath = "/run/secrets/${secretName}/${partName}";83          };84      }85    );86  secretType = submodule (87    {88      config,89      loc,90      options,91      ...92    }:93    let94      secretName =95        # Due to config definition for freeformType, we can't just use _module.args due to infinite recursion, instead96        # extract the secret name the ugly way...97        let98          saLoc = options._module.specialArgs.loc;99          comp = elemAt saLoc;100        in101        assert102          (length saLoc == 2 ||103          length saLoc == 4 &&104          comp 0 == "secrets" && comp 2 == "_module" && comp 3 == "specialArgs") ||105          throw "Unexpected module structure ${toJSON saLoc}";106        if length saLoc == 2 then "documentation generator stub" else comp 1;107    in108    {109      freeformType = lazyAttrsOf (secretPartType secretName);110      options = {111        generator = mkOption {112          type = uniq (nullOr (functionTo package));113          description = "Derivation to evaluate for secret generation";114          default = null;115        };116        mode = mkOption {117          type = str;118          description = "Secret mode";119          default = "0440";120        };121        owner = mkOption {122          type = str;123          description = "Owner of the secret";124          default = "root";125        };126        group = mkOption {127          type = str;128          description = "Group of the secret";129          default = sysConfig.users.users.${config.owner}.group;130          defaultText = literalExpression "config.users.users.$${owner}.group";131        };132        expectedGenerationData = mkOption {133          type = unspecified;134          description = "Data that gets embedded into secret part";135          default = null;136        };137        expectedPrivateParts = mkOption {138          type = listOf str;139          default = [ ];140          description = "List of parts that are expected to be encrypted";141        };142        expectedPublicParts = mkOption {143          type = listOf str;144          default = [ ];145          description = "List of parts that are expected to be public";146        };147      };148      config = mapAttrs (_: _: { }) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) [ "shared" ]);149    }150  );151  processPart = secretName: partName: part: {152    inherit (part) path stablePath;153    raw = config.data.secrets.${secretName}.${partName}.raw;154  };155  processSecret =156    secretName: secret:157    {158      inherit (secret) group mode owner;159    }160    // (mapAttrs (processPart secretName) (161      removeAttrs secret [162        "shared"163        "generator"164        "mode"165        "group"166        "owner"167        "expectedGenerationData"168        "expectedPrivateParts"169        "expectedPublicParts"170      ]171    ));172  secretsData = (mapAttrs (processSecret) config.secrets);173  secretsFile = pkgs.writeTextFile {174    name = "secrets.json";175    text = toJSON secretsData;176  };177  useSysusers =178    (config.systemd ? sysusers && config.systemd.sysusers.enable)179    || (config ? userborn && config.userborn.enable);180in181{182  options = {183    data.secrets = mkOption {184      type = attrsOf secretDataType;185      default = { };186      description = "Host-local secret data";187    };188    secrets = mkOption {189      type = attrsOf secretType;190      default = { };191      description = "Host-local secrets";192    };193    system.secretsData = mkOption {194      type = unspecified;195      default = {};196      description = "secrets.json contents";197    };198  };199  config = {200    system = {inherit secretsData;};201    environment.systemPackages = [ pkgs.fleet-install-secrets ];202203    warnings = filter (v: v!=null) (mapAttrsToList (204      name: secret:205      if206        secret.expectedPrivateParts == [ ]207        && secret.expectedPublicParts == [ ]208        && !(config.data.secrets.${name} or { shared = false; }).shared209      then210        "Secret ${name} has no expected parts defined, this is deprecated for better visibility"211      else212        null213    ) config.secrets);214215    systemd.services.fleet-install-secrets = mkIf useSysusers {216      wantedBy = [ "sysinit.target" ];217      after = [ "systemd-sysusers.service" ];218      restartTriggers = [219        secretsFile220      ];221      aliases = [222        "sops-install-secrets"223        "agenix-install-secrets"224      ];225226      unitConfig.DefaultDependencies = false;227228      serviceConfig = {229        Type = "oneshot";230        RemainAfterExit = true;231        ExecStart = "${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}";232      };233    };234    system.activationScripts.decryptSecrets = mkIf (!useSysusers) (235      stringAfter236        (237          [238            # secrets are owned by user/group, thus we need to refer to those239            "users"240            "groups"241            "specialfs"242          ]243          # nixos-impermanence compatibility: secrets are encrypted by host-key,244          # but with impermanence we expect that the host-key is installed by245          # persist-file activation script.246          ++ (optional (config.system.activationScripts ? "persist-files") "persist-files")247        )248        ''249          1>&2 echo "setting up secrets"250          ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}251        ''252    );253  };254}