feat(secrets)!: Replace sops with agenix-rekey
sops-nix is tending to be fairly complex for our use-cases, which adds
difficulty to deploying, maintaining our wrapper module, keeping
".env.bin" files, etc.
agenix-rekey is a lot simpler.
notable in this commit is the `// { outputPath = ...; }` hack in
flake.nix. This is needed due to snowfall-lib otherwise butchering paths
such that agenix-rekey is unable to show us what secrets exist with
`agenix edit`, etc... companion to that is the lib.snowfall.fs stuff in
the secrets/default.nix file
Change-Id: Id3e79cfc7d37a7b7de7b8cc42f7392c4d8bd07c5
Reviewed-on: https://git.clicks.codes/c/Infra/NixFiles/+/801
Reviewed-by: Skyler Grey <minion@clicks.codes>
Tested-by: Skyler Grey <minion@clicks.codes>
diff --git a/modules/nixos/clicks/secrets/README.md b/modules/nixos/clicks/secrets/README.md
deleted file mode 100644
index 83f43b9..0000000
--- a/modules/nixos/clicks/secrets/README.md
+++ /dev/null
@@ -1,62 +0,0 @@
-<!--
-SPDX-FileCopyrightText: 2024 Clicks Codes
-
-SPDX-License-Identifier: GPL-3.0-only
--->
-
-# Clicks SOPS
-
-To create a secret you can do the following:
-
-```nix
-clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.json}" = {
- file = ./headscale.sops.json;
- group = "headscale";
- keys = [
- "oidc_client_secret"
- "database_password"
- "noise_private_key"
- "private_key"
- ];
- neededForUsers = false;
-};
-```
-The secret name is based on the secret file's hash.
-`file` is a path to the secrets file. It is required.
-`group` is the group the key should be owned by. We chose to use groups instead of users so that you can allow multiple
-different users to read the file. If you don't set it, we'll use `"root`.
-`keys` is a list of the keys of the secret file, assuming it's not a binary file. If it isn't a binary file, you are
-required to set this. If it is a binary file, you shouldn't specify this.
-`neededForUsers` requires the secret to be present before users are created on boot, it's identical to the sops option
-of the same name. Use it for user passwords. If you don't specify it, we'll use `false`.
-
----
-
-You can then refer to the different keys directly from the secret, no need to manually create individual files:
-
-```nix
-client_secret_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.json}".paths.oidc_client_secret;
-```
-
-If the secret file is a binary file, the path can be accessed via
-
-```nix
-private_key = config.clicks.secrets."${lib.clicks.secrets.name ./privatekey.bin}".path;
-```
-
----
-
-We recommend using `lib.clicks.secrets.name` with your path to name your secrets. This avoids you creating naming
-conflicts or having messy names. This is not a hard requirement for using the module outside of Clicks, but if you're
-contributing to Clicks infrastructure we will enforce this at review.
-
-This takes a path, and is guarenteed to be stable when passed the same file at the same path.
-
-```nix
-lib.clicks.secrets.name ./file.sops.json
-```
-
----
-
-In Clicks, secrets are only ever encrypted to a single host. You'll need to make the secrets within the
-`systems/<arch>/<hostname>` directory to let sops know what host to encrypt to.
diff --git a/modules/nixos/clicks/secrets/default.nix b/modules/nixos/clicks/secrets/default.nix
deleted file mode 100644
index 19f01b4..0000000
--- a/modules/nixos/clicks/secrets/default.nix
+++ /dev/null
@@ -1,138 +0,0 @@
-# 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;
- };
-}
diff --git a/modules/nixos/clicks/security/secrets/default.nix b/modules/nixos/clicks/security/secrets/default.nix
new file mode 100644
index 0000000..9a97f9d
--- /dev/null
+++ b/modules/nixos/clicks/security/secrets/default.nix
@@ -0,0 +1,32 @@
+# SPDX-FileCopyrightText: 2024 Auxolotl Infrastructure Contributors
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ config, lib, pkgs, inputs, ... }: let
+ cfg = config.clicks.security.secrets;
+in {
+ options.clicks.security.secrets.enable = lib.mkOption {
+ description = "Enable using agenix-rekey for secrets";
+ type = lib.types.bool;
+ default = true;
+ };
+
+ config = lib.mkIf cfg.enable {
+ age.rekey = {
+ masterIdentities = [
+ "${inputs.self}/secrets/keys/minion/collabora-yubikey.pub"
+ "${inputs.self}/secrets/keys/minion/tiny-yubikey.pub"
+ "${inputs.self}/secrets/keys/minion/iyubikey.pub"
+ ];
+ storageMode = "local";
+ generatedSecretsDir = lib.snowfall.fs.get-snowfall-file "secrets/generated/${config.networking.hostName}";
+ localStorageDir = lib.snowfall.fs.get-snowfall-file "secrets/rekeyed/${config.networking.hostName}";
+ };
+
+ age.identityPaths = lib.mkIf config.clicks.storage.impermanence.enable [
+ "/persist/data/etc/ssh/ssh_host_ed25519_key"
+ "/persist/data/etc/ssh/ssh_host_rsa_key"
+ ];
+ };
+}
diff --git a/modules/nixos/clicks/services/headscale/README.md b/modules/nixos/clicks/services/headscale/README.md
index 9e87c05..6c22a0f 100644
--- a/modules/nixos/clicks/services/headscale/README.md
+++ b/modules/nixos/clicks/services/headscale/README.md
@@ -45,7 +45,7 @@
issuer = "https://login.clicks.codes/realms/master";
allowed_groups = [ "/clicks" ];
client_id = "headscale";
- client_secret_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.oidc_client_secret;
+ client_secret_path = config.age.secrets."clicks.services.headscale.oidc.client_secret_path".path;
};
};
```
@@ -64,9 +64,9 @@
```nix
clicks.services.headscale = {
- database_password_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.database_password;
- noise_private_key_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.noise_private_key;
- private_key_path = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.a1d1.json}".paths.private_key;
+ database_password_path = config.age.secrets."clicks.services.headscale.database_password_path".path;
+ noise_private_key_path = config.age.secrets."clicks.services.headscale.noise_private_key_path".path;
+ private_key_path = config.age.secrets."clicks.services.headscale.private_key_path".path;
}
```
diff --git a/modules/nixos/clicks/services/postgres/README.md b/modules/nixos/clicks/services/postgres/README.md
index 3efd637..be29a14 100644
--- a/modules/nixos/clicks/services/postgres/README.md
+++ b/modules/nixos/clicks/services/postgres/README.md
@@ -8,12 +8,12 @@
You can create a database, user and credentials by using `clicks.services.postgres.databases.<name>`. You should set this to a file containing the password for your database user.
-We recommend using our secrets module to create this password file.
+We recommend using [agenix-rekey](https://github.com/oddlama/agenix-rekey) to create this password file
```nix
clicks.services.postgres = {
enable = true;
- databases.headscale = config.clicks.secrets."${lib.clicks.secrets.name ./headscale.sops.json}".paths.database_password;
+ databases.headscale = config.age.secrets."clicks.services.postgres.databases.headscale".path;
};
```