feat: add sops
We've previously used SOPS for our secrets management and liked it. We
did, however, find the configuration was a bit annoying to do. In aid of
this, we've made a SOPS module that we find a little easier to make the
sort of configurations we want without creating so much mess.
We haven't set up scalpel/equivalent yet - we intend to avoid it if at
all possible. It isn't necessarily out-of-scope but it isn't included in
our current SOPS plans.
Change-Id: I35b9c7e94c12a4f1360833026efe06803d59626e
Reviewed-on: https://git.clicks.codes/c/Infra/NixFiles/+/725
Reviewed-by: Samuel Shuert <coded@clicks.codes>
Tested-by: Samuel Shuert <coded@clicks.codes>
diff --git a/modules/nixos/clicks/secrets/default.nix b/modules/nixos/clicks/secrets/default.nix
new file mode 100644
index 0000000..19f01b4
--- /dev/null
+++ b/modules/nixos/clicks/secrets/default.nix
@@ -0,0 +1,138 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{
+ lib,
+ pkgs,
+ config,
+ ...
+}:
+let
+ cfg = config.clicks.security.sops;
+
+ guessFormat =
+ extension:
+ if extension == "json" then
+ "json"
+ else if extension == "yaml" || extension == "yml" then
+ "yaml"
+ else if extension == "env" then
+ "dotenv"
+ else if extension == "ini" then
+ "ini"
+ else
+ "binary";
+
+ getExtension =
+ filePath:
+ let
+ pathParts = builtins.split ''\.'' (builtins.toString filePath);
+ numPathParts = builtins.length pathParts;
+ in
+ builtins.elemAt pathParts (numPathParts - 1);
+in
+{
+ options.clicks.secrets =
+ let
+ generateNonBinarySopsPaths =
+ file: keys:
+ lib.lists.forEach keys (key: {
+ name = key;
+ value = config.sops.secrets."${lib.clicks.secrets.name file}:${key}".path;
+ });
+ in
+ lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.submodule (
+ { ... }@submodule:
+ {
+ options = {
+ file = lib.mkOption {
+ type = lib.types.pathInStore;
+ description = "The store path to your secrets file";
+ };
+ group = lib.mkOption {
+ type = lib.types.str;
+ description = "The user the secret should be owned by.";
+ default = "root";
+ };
+ keys = lib.mkOption {
+ type = lib.types.nullOr (lib.types.listOf lib.types.str);
+ description = "List of keys to pull from the structured data.";
+ default = null;
+ };
+ neededForUsers = lib.mkEnableOption "This secret is needed for users";
+ paths = lib.mkOption {
+ type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+ description = "Automatically populated with the SOPS paths to your keys, null if you are using binary secrets";
+ default =
+ if guessFormat (getExtension submodule.config.file) != "binary" then
+ builtins.listToAttrs (generateNonBinarySopsPaths submodule.config.file submodule.config.keys)
+ else
+ null;
+ };
+ path = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ description = "Populated automatically with the SOPS path of the secret, null if you are using non binary secrets";
+ default =
+ if guessFormat (getExtension submodule.config.file) == "binary" then
+ config.sops.secrets.${lib.clicks.secrets.name submodule.config.file}.path
+ else
+ null;
+ };
+ };
+ }
+ )
+ );
+ description = "";
+ default = { };
+ };
+
+ config =
+ let
+ generateBinarySopsSecret = secret: {
+ name = lib.clicks.secrets.name secret.value.file;
+ value = {
+ mode = "0400";
+ owner = config.users.users.root.name;
+ group = config.users.groups.${secret.value.group}.name;
+ sopsFile = secret.value.file;
+ format = guessFormat (getExtension secret.value.file);
+ inherit (secret.value) neededForUsers;
+ };
+ };
+
+ generateNonBinarySopsSecrets =
+ secret:
+ lib.lists.forEach secret.value.keys (key: {
+ name = "${lib.clicks.secrets.name secret.value.file}:${key}";
+ value = {
+ mode = "0040";
+ owner = config.users.users.root.name;
+ group = config.users.groups.${secret.value.group}.name;
+ sopsFile = secret.value.file;
+ format = guessFormat (getExtension secret.value.file);
+ inherit (secret.value) neededForUsers;
+ inherit key;
+ };
+ });
+
+ secretsAsList = lib.attrsets.attrsToList config.clicks.secrets;
+
+ secretsAsSops = lib.pipe secretsAsList [
+ (map (
+ secret:
+ if guessFormat (getExtension secret.value.file) == "binary" then
+ generateBinarySopsSecret secret
+ else
+ generateNonBinarySopsSecrets secret
+ ))
+ lib.flatten
+ builtins.listToAttrs
+ ];
+ in
+ {
+ sops.secrets = secretsAsSops;
+ };
+}