From c4c39450b1f6620c839f3969807fccc1797f08d6 Mon Sep 17 00:00:00 2001 From: mlatus Date: Mon, 13 Mar 2023 23:56:19 +0800 Subject: [PATCH] add sops.templates --- modules/sops/default.nix | 1 + modules/sops/templates/default.nix | 108 +++++++++++++++++++++++++++++ modules/sops/templates/subs.py | 17 +++++ 3 files changed, 126 insertions(+) create mode 100644 modules/sops/templates/default.nix create mode 100644 modules/sops/templates/subs.py diff --git a/modules/sops/default.nix b/modules/sops/default.nix index d17fa9a..3245444 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -294,6 +294,7 @@ in { }; }; imports = [ + ./templates (mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ]) (mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ]) ]; diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix new file mode 100644 index 0000000..0a504b1 --- /dev/null +++ b/modules/sops/templates/default.nix @@ -0,0 +1,108 @@ +{ config, pkgs, lib, options, ... }: +with lib; +with lib.types; +with builtins; +let + cfg = config.sops; + secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; + users = config.users.users; + substitute = pkgs.writers.writePython3 "substitute" { } + (replaceStrings [ "@subst@" ] [ "${subst-pairs}" ] (readFile ./subs.py)); + subst-pairs = pkgs.writeText "pairs" (concatMapStringsSep "\n" (name: + "${toString config.sops.placeholder.${name}} ${ + config.sops.secrets.${name}.path + }") (attrNames config.sops.secrets)); + coercibleToString = mkOptionType { + name = "coercibleToString"; + description = "value that can be coerced to string"; + check = strings.isCoercibleToString; + merge = mergeEqualOption; + }; + templateType = submodule ({ config, ... }: { + options = { + name = mkOption { + type = str; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets-rendered + ''; + }; + path = mkOption { + type = str; + default = "/run/secrets-rendered/${config.name}"; + }; + content = mkOption { + type = lines; + default = ""; + description = '' + Content of the file + ''; + }; + mode = mkOption { + type = str; + default = "0400"; + description = '' + Permissions mode of the in octal. + ''; + }; + owner = mkOption { + type = str; + default = "root"; + description = '' + User of the file. + ''; + }; + group = mkOption { + type = str; + default = users.${config.owner}.group; + description = '' + Group of the file. + ''; + }; + file = mkOption { + type = types.path; + default = pkgs.writeText config.name config.content; + visible = false; + readOnly = true; + }; + }; + }); +in { + options.sops = { + templates = mkOption { + type = attrsOf templateType; + default = { }; + }; + placeholder = mkOption { + type = attrsOf coercibleToString; + default = { }; + visible = false; + }; + substituteCmd = mkOption { + type = types.path; + default = substitute; + }; + }; + + config = optionalAttrs (options ? sops.secrets) + (mkIf (config.sops.templates != { }) { + sops.placeholder = mapAttrs + (name: _: mkDefault "") + config.sops.secrets; + + system.activationScripts.renderSecrets = mkIf (cfg.templates != { }) + (stringAfter ([ "setupSecrets" ] + ++ optional (secretsForUsers != { }) "setupSecretsForUsers") '' + echo Setting up sops templates... + ${concatMapStringsSep "\n" (name: + let tpl = config.sops.templates.${name}; + in '' + mkdir -p "${dirOf tpl.path}" + ${config.sops.substituteCmd} ${tpl.file} > ${tpl.path} + chmod "${tpl.mode}" "${tpl.path}" + chown "${tpl.owner}" "${tpl.path}" + chgrp "${tpl.group}" "${tpl.path}" + '') (attrNames config.sops.templates)} + ''); + }); +} diff --git a/modules/sops/templates/subs.py b/modules/sops/templates/subs.py new file mode 100644 index 0000000..62737a1 --- /dev/null +++ b/modules/sops/templates/subs.py @@ -0,0 +1,17 @@ +from sys import argv + +target = argv[1] +subst = "@subst@" + +with open(target) as f: + content = f.read() + +with open(subst) as f: + subst_pairs = f.read().splitlines() + +for pair in subst_pairs: + placeholder, path = pair.split() + with open(path) as f: + content = content.replace(placeholder, f.read()) + +print(content)