From 63bb9395fd03ac73a88d3b2a313a723b38c238dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 23 Aug 2023 11:58:12 +0200 Subject: [PATCH] automatically import secrets into nixos --- checks/flake-module.nix | 24 ++++++++++-- checks/lib/test-base.nix | 18 +++++++++ checks/schemas.nix | 34 ++++++++++++++++ checks/secrets/.clan-flake | 0 checks/secrets/clan-secrets | 6 +++ checks/secrets/default.nix | 16 ++++++++ checks/secrets/key.age | 1 + checks/secrets/sops/machines/machine/key.json | 4 ++ .../secrets/sops/secrets/foo/machines/machine | 1 + checks/secrets/sops/secrets/foo/secret | 20 ++++++++++ checks/secrets/sops/secrets/foo/users/admin | 1 + checks/secrets/sops/users/admin/key.json | 4 ++ flake.lock | 22 +++++++++++ flake.nix | 3 ++ nixosModules/flake-module.nix | 8 +++- nixosModules/secrets/default.nix | 39 +++++++++++++++++++ pkgs/clan-cli/clan_cli/secrets/sops.py | 2 +- 17 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 checks/lib/test-base.nix create mode 100644 checks/schemas.nix create mode 100644 checks/secrets/.clan-flake create mode 100755 checks/secrets/clan-secrets create mode 100644 checks/secrets/default.nix create mode 100644 checks/secrets/key.age create mode 100755 checks/secrets/sops/machines/machine/key.json create mode 120000 checks/secrets/sops/secrets/foo/machines/machine create mode 100644 checks/secrets/sops/secrets/foo/secret create mode 120000 checks/secrets/sops/secrets/foo/users/admin create mode 100755 checks/secrets/sops/users/admin/key.json create mode 100644 nixosModules/secrets/default.nix diff --git a/checks/flake-module.nix b/checks/flake-module.nix index b826c2a1..3f0fd164 100644 --- a/checks/flake-module.nix +++ b/checks/flake-module.nix @@ -1,5 +1,21 @@ -{ - imports = [ - ./schema.nix - ]; +{ self, ... }: { + perSystem = { pkgs, ... }: { + checks = + let + nixosTestArgs = { + # reference to nixpkgs for the current system + inherit pkgs; + # this gives us a reference to our flake but also all flake inputs + inherit self; + }; + nixosTests = { + # import our test + secrets = import ./secrets nixosTestArgs; + }; + schemaTests = pkgs.callPackages ./schemas.nix { + inherit self; + }; + in + nixosTests // schemaTests; + }; } diff --git a/checks/lib/test-base.nix b/checks/lib/test-base.nix new file mode 100644 index 00000000..454d0aca --- /dev/null +++ b/checks/lib/test-base.nix @@ -0,0 +1,18 @@ +test: +{ pkgs +, self +, ... +}: +let + inherit (pkgs) lib; + nixos-lib = import (pkgs.path + "/nixos/lib") { }; +in +(nixos-lib.runTest { + hostPkgs = pkgs; + # speed-up evaluation + defaults.documentation.enable = lib.mkDefault false; + # to accept external dependencies such as disko + node.specialArgs.self = self; + imports = [ test ]; +}).config.result + diff --git a/checks/schemas.nix b/checks/schemas.nix new file mode 100644 index 00000000..49391395 --- /dev/null +++ b/checks/schemas.nix @@ -0,0 +1,34 @@ +{ self, runCommand, check-jsonschema, pkgs, lib, ... }: +let + clanModules = self.clanModules; + + baseModule = { + imports = + (import (pkgs.path + "/nixos/modules/module-list.nix")) + ++ [{ + nixpkgs.hostPlatform = "x86_64-linux"; + }]; + }; + + optionsFromModule = module: + let + evaled = lib.evalModules { + modules = [ module baseModule ]; + }; + in + evaled.options.clan.networking; + + clanModuleSchemas = lib.mapAttrs (_: module: self.lib.jsonschema.parseOptions (optionsFromModule module)) clanModules; + + mkTest = name: schema: runCommand "schema-${name}" { } '' + ${check-jsonschema}/bin/check-jsonschema \ + --check-metaschema ${builtins.toFile "schema-${name}" (builtins.toJSON schema)} + touch $out + ''; +in +lib.mapAttrs' + (name: schema: { + name = "schema-${name}"; + value = mkTest name schema; + }) + clanModuleSchemas diff --git a/checks/secrets/.clan-flake b/checks/secrets/.clan-flake new file mode 100644 index 00000000..e69de29b diff --git a/checks/secrets/clan-secrets b/checks/secrets/clan-secrets new file mode 100755 index 00000000..79310e72 --- /dev/null +++ b/checks/secrets/clan-secrets @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux -o pipefail +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +export SOPS_AGE_KEY_FILE="${SCRIPT_DIR}/key.age" +nix run .# -- secrets "$@" diff --git a/checks/secrets/default.nix b/checks/secrets/default.nix new file mode 100644 index 00000000..af4f13b9 --- /dev/null +++ b/checks/secrets/default.nix @@ -0,0 +1,16 @@ +(import ../lib/test-base.nix) { + name = "secrets"; + + nodes.machine = { self, config, ... }: { + imports = [ + self.nixosModules.secrets + ]; + environment.etc."secret".source = config.sops.secrets.foo.path; + sops.age.keyFile = ./key.age; + clan.sops.sopsDirectory = ./sops; + networking.hostName = "machine"; + }; + testScript = '' + machine.succeed("cat /etc/secret >&2") + ''; +} diff --git a/checks/secrets/key.age b/checks/secrets/key.age new file mode 100644 index 00000000..1c9755ab --- /dev/null +++ b/checks/secrets/key.age @@ -0,0 +1 @@ +AGE-SECRET-KEY-1UCXEUJH6JXF8LFKWFHDM4N9AQE2CCGQZGXLUNV4TKR5KY0KC8FDQ2TY4NX diff --git a/checks/secrets/sops/machines/machine/key.json b/checks/secrets/sops/machines/machine/key.json new file mode 100755 index 00000000..75648379 --- /dev/null +++ b/checks/secrets/sops/machines/machine/key.json @@ -0,0 +1,4 @@ +{ + "publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00", + "type": "age" +} \ No newline at end of file diff --git a/checks/secrets/sops/secrets/foo/machines/machine b/checks/secrets/sops/secrets/foo/machines/machine new file mode 120000 index 00000000..4cef1e1f --- /dev/null +++ b/checks/secrets/sops/secrets/foo/machines/machine @@ -0,0 +1 @@ +../../../machines/machine \ No newline at end of file diff --git a/checks/secrets/sops/secrets/foo/secret b/checks/secrets/sops/secrets/foo/secret new file mode 100644 index 00000000..062aebd4 --- /dev/null +++ b/checks/secrets/sops/secrets/foo/secret @@ -0,0 +1,20 @@ +{ + "data": "ENC[AES256_GCM,data:bhxF,iv:iNs+IfSU/7EwssZ0GVTF2raxJkVlddfQEPGIBeUYAy8=,tag:JMOKTMW3/ic3UTj9eT9YFQ==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxS0g4TEt4S09LQnFKdCtk\nZTlUQWhNUHZmcmZqdGtuZkhhTkMzZDVaWWdNCi9vNnZQeklNaFBBU2x0ditlUDR0\nNGJlRmFFb09WSUFGdEh5TGViTWtacFEKLS0tIE1OMWdQMHhGeFBwSlVEamtHUkcy\ndzI1VHRkZ1o4SStpekVNZmpQSnRkeUkKYmPS9sR6U0NHxd55DjRk29LNFINysOl6\nEM2MTrntLxOHFWZ1QgNx34l4rYIIXx97ONvR0SRpxN0ECL9VonQeZg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2023-08-23T09:11:08Z", + "mac": "ENC[AES256_GCM,data:8z819mP4FJXE/ExWM1+/dhaXIXzCglhBuZwE6ikl/jNLUAnv3jYL9c9vPrPFl2by3wXSNzqB4AOiTKDQoxDx2SBQKxeWaUnOajD6hbzskoLqCCBfVx7qOHrk/BULcBvMSxBca4RnzXXoMFTwKs2A1fXqAPvSQd1X4gX6Xm9VXWM=,iv:3YxZX+gaEcRKDN0Kuf9y1oWL+sT/J5B/5CtCf4iur9Y=,tag:0dwyjpvjCqbm9vIrz6WSWQ==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.7.3" + } +} \ No newline at end of file diff --git a/checks/secrets/sops/secrets/foo/users/admin b/checks/secrets/sops/secrets/foo/users/admin new file mode 120000 index 00000000..9e21a993 --- /dev/null +++ b/checks/secrets/sops/secrets/foo/users/admin @@ -0,0 +1 @@ +../../../users/admin \ No newline at end of file diff --git a/checks/secrets/sops/users/admin/key.json b/checks/secrets/sops/users/admin/key.json new file mode 100755 index 00000000..75648379 --- /dev/null +++ b/checks/secrets/sops/users/admin/key.json @@ -0,0 +1,4 @@ +{ + "publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00", + "type": "age" +} \ No newline at end of file diff --git a/flake.lock b/flake.lock index 6244e47c..60ef6056 100644 --- a/flake.lock +++ b/flake.lock @@ -119,9 +119,31 @@ "floco": "floco", "nixos-generators": "nixos-generators", "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix", "treefmt-nix": "treefmt-nix" } }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "sops-nix" + ], + "nixpkgs-stable": [] + }, + "locked": { + "lastModified": 1692500916, + "narHash": "sha256-iKADqEOHmyi+LCJ5LzWcM2zH0DP3WHFETjX98blH0tE=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "4f0f113b7dbcb92edb9c901515fcab0b91c6def7", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index 736b29e5..8a577bc2 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,9 @@ floco.inputs.nixpkgs.follows = "nixpkgs"; disko.url = "github:nix-community/disko"; disko.inputs.nixpkgs.follows = "nixpkgs"; + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "sops-nix"; + sops-nix.inputs.nixpkgs-stable.follows = ""; nixos-generators.url = "github:nix-community/nixos-generators"; nixos-generators.inputs.nixpkgs.follows = "nixpkgs"; flake-parts.url = "github:hercules-ci/flake-parts"; diff --git a/nixosModules/flake-module.nix b/nixosModules/flake-module.nix index 4d79d7d0..15ab5758 100644 --- a/nixosModules/flake-module.nix +++ b/nixosModules/flake-module.nix @@ -1,6 +1,10 @@ -{ ... }: { +{ inputs, ... }: { flake.nixosModules = { hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ]; - installer.imports = [ ./installer.nix ]; + installer.imports = [ ./installer ]; + secrets.imports = [ + inputs.sops-nix.nixosModules.sops + ./secrets + ]; }; } diff --git a/nixosModules/secrets/default.nix b/nixosModules/secrets/default.nix new file mode 100644 index 00000000..eb2d6ae1 --- /dev/null +++ b/nixosModules/secrets/default.nix @@ -0,0 +1,39 @@ +{ lib, config, ... }: +let + encryptedForThisMachine = name: type: + let + symlink = config.clan.sops.sopsDirectory + "/secrets/${name}/machines/${config.clan.sops.machineName}"; + in + # WTF, nix bug, my symlink is in the nixos module detected as a directory also it works in the repl + type == "directory" && (builtins.readFileType symlink == "directory" || builtins.readFileType symlink == "symlink"); + secrets = lib.filterAttrs encryptedForThisMachine (builtins.readDir (config.clan.sops.sopsDirectory + "/secrets")); +in +{ + imports = [ + ]; + options = { + clan.sops = { + machineName = lib.mkOption { + type = lib.types.str; + default = config.networking.hostName; + description = '' + Machine used to lookup secrets in the sops directory. + ''; + }; + sopsDirectory = lib.mkOption { + type = lib.types.path; + description = '' + Sops toplevel directory that stores users, machines, groups and secrets. + ''; + }; + }; + }; + config = { + sops.secrets = builtins.mapAttrs + (name: _: { + sopsFile = config.clan.sops.sopsDirectory + "/secrets/${name}/secret"; + format = "binary"; + }) + secrets; + }; +} diff --git a/pkgs/clan-cli/clan_cli/secrets/sops.py b/pkgs/clan-cli/clan_cli/secrets/sops.py index bdd7161d..84db62a2 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops.py @@ -43,7 +43,7 @@ def get_user_name(user: str) -> str: """Ask the user for their name until a unique one is provided.""" while True: name = input( - f"Enter your user name for which your sops key will be stored in the repository [default: {user}]: " + f"Your key is not yet added to the repository. Enter your user name for which your sops key will be stored in the repository [default: {user}]: " ) if name: user = name