From f5a2ba217ba4778cec6602e1d647e2f5113fdf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janne=20He=C3=9F?= Date: Fri, 27 Aug 2021 00:49:58 +0200 Subject: [PATCH] Add age support --- modules/sops/default.nix | 37 +++++++++- pkgs/sops-install-secrets/main.go | 17 ++++- pkgs/sops-install-secrets/main_test.go | 34 +++++++++ pkgs/sops-install-secrets/nixos-test.nix | 20 +++++ .../test-assets/age-keys.txt | 3 + .../test-assets/secrets.yaml | 73 +++++++++++-------- 6 files changed, 145 insertions(+), 39 deletions(-) create mode 100644 pkgs/sops-install-secrets/test-assets/age-keys.txt diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 409b54f..be2f835 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -85,7 +85,7 @@ let # Does this need to be configurable? secretsMountPoint = "/run/secrets.d"; symlinkPath = "/run/secrets"; - inherit (cfg) gnupgHome sshKeyPaths; + inherit (cfg) gnupgHome sshKeyPaths ageKeyFile; }); checkedManifest = let @@ -130,6 +130,25 @@ in { ''; }; + ageKeyFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/lib/sops-nix/key.txt"; + description = '' + Path to age key file used for sops decryption. + ''; + }; + + generateAgeKey = mkOption { + type = types.bool; + default = false; + description = '' + Whether or not to generate the age key. If this + option is set to false, the key must already be + present at the specified location. + ''; + }; + gnupgHome = mkOption { type = types.nullOr types.str; default = null; @@ -152,8 +171,11 @@ in { }; config = mkIf (cfg.secrets != {}) { assertions = [{ - assertion = (cfg.gnupgHome == null) != (cfg.sshKeyPaths == []); + assertion = cfg.ageKeyFile == null -> (cfg.gnupgHome == null) != (cfg.sshKeyPaths == []); message = "Exactly one of sops.gnupgHome and sops.sshKeyPaths must be set"; + } { + assertion = (cfg.sshKeyPaths != [] || cfg.gnupgHome != null) != (cfg.ageKeyFile != null); + message = "sops.ageKeyFile is mutually exclusive with sops.gnupgHome and sops.sshKeyPaths"; }] ++ optionals cfg.validateSopsFiles ( concatLists (mapAttrsToList (name: secret: [{ assertion = builtins.pathExists secret.sopsFile; @@ -168,9 +190,18 @@ in { system.activationScripts.setup-secrets = let sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets; - in stringAfter [ "specialfs" "users" "groups" ] '' + in stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.generateAgeKey "generate-age-key") '' echo setting up secrets... ${optionalString (cfg.gnupgHome != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${checkedManifest} ''; + + system.activationScripts.generate-age-key = (mkIf cfg.generateAgeKey) (stringAfter [] '' + if [[ ! -f "${cfg.ageKeyFile}" ]]; then; + echo generating machine-specific age key... + mkdir -p $(dirname ${cfg.ageKeyFile}) + # age-keygen sets 0600 by default, no need to chmod. + ${pkgs.age}/bin/age-keygen -o ${cfg.ageKeyFile} + fi + ''); }; } diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index fe22505..7c0d7bc 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -5,7 +5,6 @@ package main import ( "encoding/hex" "encoding/json" - "errors" "flag" "fmt" "io/ioutil" @@ -47,6 +46,7 @@ type manifest struct { SymlinkPath string `json:"symlinkPath"` SSHKeyPaths []string `json:"sshKeyPaths"` GnupgHome string `json:"gnupgHome"` + AgeKeyFile string `json:"ageKeyFile"` } type secretFile struct { @@ -437,10 +437,17 @@ func (app *appContext) validateManifest() error { if m.SymlinkPath == "" { m.SymlinkPath = "/run/secrets" } - if len(m.SSHKeyPaths) > 0 && m.GnupgHome != "" { - return errors.New("gnupgHome and sshKeyPaths were specified in the manifest. " + - "Both options are mutual exclusive.") + if m.GnupgHome != "" { + errorFmt := "gnupgHome and %s were specified in the manifest. " + + "Both options are mutually exclusive." + if len(m.SSHKeyPaths) > 0 { + return fmt.Errorf(errorFmt, "sshKeyPaths") + } + if m.AgeKeyFile != "" { + return fmt.Errorf(errorFmt, "ageKeyFile") + } } + for i := range m.Secrets { if err := app.validateSecret(&m.Secrets[i]); err != nil { return err @@ -604,6 +611,8 @@ func installSecrets(args []string) error { defer keyring.Remove() } else if manifest.GnupgHome != "" { os.Setenv("GNUPGHOME", manifest.GnupgHome) + } else if manifest.AgeKeyFile != "" { + os.Setenv("SOPS_AGE_KEY_FILE", manifest.AgeKeyFile) } if err := decryptSecrets(manifest.Secrets); err != nil { diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index a5eb85e..9046d4f 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -219,10 +219,44 @@ func testSSHKey(t *testing.T) { testInstallSecret(t, testdir, &m) } +func testAge(t *testing.T) { + assets := testAssetPath() + + testdir := newTestDir(t) + defer testdir.Remove() + + target := path.Join(testdir.path, "existing-target") + file, err := os.Create(target) + ok(t, err) + file.Close() + + s := secret{ + Name: "test", + Key: "test_key", + Owner: "nobody", + Group: "nogroup", + SopsFile: path.Join(assets, "secrets.yaml"), + Path: target, + Mode: "0400", + RestartServices: []string{"affected-service"}, + ReloadServices: make([]string, 0), + } + + m := manifest{ + Secrets: []secret{s}, + SecretsMountPoint: testdir.secretsPath, + SymlinkPath: testdir.symlinkPath, + AgeKeyFile: path.Join(assets, "age-keys.txt"), + } + + testInstallSecret(t, testdir, &m) +} + func TestAll(t *testing.T) { // we can't test in parallel because we rely on GNUPGHOME environment variable testGPG(t) testSSHKey(t) + testAge(t) } func TestValidateManifest(t *testing.T) { diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index f98e182..a12539a 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -23,6 +23,26 @@ inherit (pkgs) system; }; + age-keys = makeTest { + name = "sops-age-keys"; + machine = { + imports = [ ../../modules/sops ]; + sops = { + ageKeyFile = ./test-assets/age-keys.txt; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets.test_key = {}; + }; + }; + + testScript = '' + start_all() + machine.succeed("cat /run/secrets/test_key | grep -q test_value") + ''; + } { + inherit pkgs; + inherit (pkgs) system; + }; + pgp-keys = makeTest { name = "sops-pgp-keys"; nodes.server = { pkgs, lib, config, ... }: { diff --git a/pkgs/sops-install-secrets/test-assets/age-keys.txt b/pkgs/sops-install-secrets/test-assets/age-keys.txt new file mode 100644 index 0000000..c56eb4f --- /dev/null +++ b/pkgs/sops-install-secrets/test-assets/age-keys.txt @@ -0,0 +1,3 @@ +# created: 2020-07-18T03:16:47-07:00 +# public key: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw +AGE-SECRET-KEY-1NJT5YCS2LWU4V4QAJQ6R4JNU7LXPDX602DZ9NUFANVU5GDTGUWCQ5T59M6 diff --git a/pkgs/sops-install-secrets/test-assets/secrets.yaml b/pkgs/sops-install-secrets/test-assets/secrets.yaml index 018b08e..7f36ea1 100644 --- a/pkgs/sops-install-secrets/test-assets/secrets.yaml +++ b/pkgs/sops-install-secrets/test-assets/secrets.yaml @@ -1,47 +1,56 @@ -test_key: ENC[AES256_GCM,data:4cC2PTi7xVPZPA==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:ZaKi9m6ziFKNV+gx7XedTw==,type:str] +test_key: ENC[AES256_GCM,data:6aaSGYcvIY1+lQ==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:/FSMgXuX8TnbxRxpcuwGEA==,type:str] a_list: -- ENC[AES256_GCM,data:5K0=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:Op0+iEYzV+gfYGveN3VKKg==,type:str] -- ENC[AES256_GCM,data:9dM=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:HvbERoLZcUOjEd4AwLVNEg==,type:str] + - ENC[AES256_GCM,data:IBI=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:7A4l/SgzgxK9sqi+/15A6w==,type:str] + - ENC[AES256_GCM,data:tLE=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:cmhMaddcY2bhydWrPDWNlQ==,type:str] sops: kms: [] gcp_kms: [] azure_kv: [] hc_vault: [] - lastmodified: '2021-01-27T06:12:22Z' - mac: ENC[AES256_GCM,data:/lwT78drEKdCoWW9TPU2H/IWlq/9uEmJocrTvftKTD1Au9e/7AMCUWGWMPGJKMg9R0FWV2pn3tgwli5YXRrIe4L9tIkeM5vJvz85IeQIc+vviby7PM8VtbO1ArisHh95cVwZuASR3KSbumnxURjayZ61J9Jiz0viBeuEmCP50u4=,iv:FX6XDUqetDaRTtLLfMaJAkPZmiZx59wnuDRm0SvmTJM=,tag:HnQ4dHsCCNim2v8WPXyLdw==,type:str] + age: + - recipient: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuY2RtdnhrMEtOeEUvRmFr + SU5uRm1xaU12R3F5TnJvbzI5YlNYMDFycGxvCnRBQVNwY2cxMDVicUNZWlpFdStp + ZTYxcUJkNEFHVkxTbGdrdkIxN3ZMNkUKLS0tIHd2K2ZhaFVoTmlhNDBKeXZyVHBh + ZXFnRy9hUTFNQm9rWVA3YnJXaWV0S2cKBpGxzhth36fab8KDKFBoweQO9L0juys4 + cMjz2X/hXVMLvDeCLVBTZTj3K/lXAo4v2qMUGZsR2Idpw3FOPxfSGg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2021-08-26T22:38:49Z" + mac: ENC[AES256_GCM,data:1TNMF0HIfIOetCF4F268N5k2DFQ28JBYOdjPxfOuw03udJ1eKcTZrlBAGGFEgMdu1FuW+ZC+gLHW/b0GpfZWAtkiLuWP9SUof01rrFPYse6LvqWvlY1mLK6unbMveGeD9My3QTkZmt962I9tEHQW+ph35j80egGnN1f6stiJdlY=,iv:KEa/Q6q+B0F2Dv+/Km+2WeztYij0Of1pCSygGXM2fG4=,tag:TzuiTyYwTor36eGPKvTcwQ==,type:str] pgp: - - created_at: '2020-07-12T08:03:51Z' - enc: | + - created_at: "2020-07-12T08:03:51Z" + enc: | -----BEGIN PGP MESSAGE----- - hQEMA/m6nevQP1fAAQgAnGLEGUnWgYTLGGZN7sEETu14MJjr4TY7JDhs8VOrA9ng - x9ivSF1dPw6arewal70OgxMLnS55HY6L81vRCbu8d0rdfXkdzO6oeMAo79/udKc8 - F5CDSkdrBImK5xpEQ0stpt1zmmKMAPGuPSbSc48YEt4OYUdpoIO+PesYUUgGerL7 - 4UvXtbhYjIhZL7Sgst/coOUNUWjpQgVRSeza6qcuVfS3gVBvxNCed51n+Cr3/rnD - 5PXjeWQ8aayLDDV32CjGf+s3+LG0gvJ7A6eq0THjsvgkY4qGHNSMobkN7npFI4lX - KZG68Scu5d11IY2N3y0ijRQQxYlPPPpukWvw3CK8htJeAZltLDHzh0bpISpc7gqU - xzATRG/LmJk7yS9Vej/B0NGmkVlbB+lFnLZG3lVmDiaQUsgePp2Ho1/j6ysit3/C - i87IM8B71y1brldk2mcr1oAoNSVE4dg+/C2HrDRKxA== - =Qyuq + hQEMA/m6nevQP1fAAQf/Zih3JJl/L/ApqkwdhIv7iUQsbebXV+WRwlslwOz6by/t + Sf6CH3b5FHLaBQtPEUh6QNLpQSqm52Rs4MoJxML0Tan8eu6Q313lS09XP5mQP1yc + d1FN4Tsg8V/xaGMGAtJHM/bj+6vfimGtnnQvNZ7N7PW2U+fxpq51NmqIXMmG+jOw + 2Tw+04oNoK6lrG89y0dzJrTvP9ph0wM+hwOgBthfhr0X6/UF7vS9fgEod+HFEqGn + MfAW4CiijXDvFYI5unwseeUE8IosOaq2VTW57eBSteLWIyJqmLUxvVSoMP0emitN + 4T0DkzTxywq5KPUMIEISEZrxYEskatqlTdQBmcjEAdJcAQl/u1OGCn7YRIJbXjMP + 8/UvlFVfaT3L19sgEg8rSVpo3GiwlwbP0KTcb4jVMu0mHhd70VciNKojtDVlZg9n + 0V8Z2LwPHY0+IzcTJu5IS7sO/fuyYJohWQ4ZV7Y= + =1olg -----END PGP MESSAGE----- - fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0 - - created_at: '2020-07-12T08:03:51Z' - enc: | + fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0 + - created_at: "2020-07-12T08:03:51Z" + enc: | -----BEGIN PGP MESSAGE----- - hQGMA3ulPRkZxd/UAQv/d1ExCl7gVIe0GeH8bc0xl8gt4hvR2ujH0BmmHpQ7SDLa - tS7lH3ennlLlN+owN3DkCKqm/BNEQ7Wy5y0oTvmQSo2BdSMYOvHrLKoPK3OsDLPH - vtXUKZ+NLKSV2xCSiqrcu4GRki8klLgDNEgna8T21erCXZM8W/ehcOd7gLSUiatd - wQTGEhNAA0RO04kf0NoPcNrPak7e0bGbwEc88RdM2QyxSXi6SVrKVHvYVPE3pfFB - VkUcWZU2edLUEKIAFRAtmb9s3DSjECMMuoVXR6lrPx4c6OMRKvxtlZh0uOOcRpoG - M5KL8O4ZXNxUVNRzPisgqe0hkh2AsnMLIQeWrVLfOLXUdDjr9x42vga/P8WU7AzT - /Lxmlu9cY0KKsXpv7yXibZfFJ/K/4Mo9WsNk6tGAmiSUg1hr/NJPha3PDHyX0ER7 - 6dPFeMrvs4VngC0I27v6rlEjU+HiRv3/uLnn04z9DRQEk6a+YpLZTpHvAbzfb1Dg - EBpKCQePm2rIzywLE8d60lABXSQKG4/LSH6WgxfRFibORME2z5DD2G7ibiDbVg8j - YbrH0E0KeRxYJMJ28FVDlLgoUL+JbNr70uHkdvz8S0hqCYwo+K4KqUOIsiPc6iIL - tA== - =XoKf + hQGMA3ulPRkZxd/UAQv+LSFG7a6XqqW7GYdYLb1H0bv3NkxJeBYGohsMrEC4AXnA + RdhJ2B55WqiNyDmVmylalUKAPFpD7RJ4c6HDZcRLx6doy5wgA+oFvKCe8FcYy5YS + abSUhNoT0TwWlDQ4Bc4Q/QSu9kMCica56Bc10tz5Pvb5JEYApoLfL0kTO7mBI+w4 + hKovAxlp9aoT9cky/7WFhmFn1+mYz//ejDKPuyVSYhM/2eIosDE9maPootLQMrIX + cqnsCDCOgMDwIOXt5/W4Cab799Pop4SQcFDSDa4FU4kN12vmNwJu7HPxpR+W80bL + GOUu35gVyykig9XCaTA/UCc85URtOMYmXPBsdTVoMVAgfxkji3ovJCEFBcI7wYgd + XudLQw5tJ9Pwz+5EeNw771ISaTBO7bWazSq+q3HrKW9kKLFJV4kVt7zLr+yZ1kNu + jEjTFmaKYl3p9AYh6yxb6n9PuTosk1ZJE8gv8ME+b4z21BVxfZsDfYWVhT9thZz5 + q2LHw+E/fmf0EXz9tviS0k4BimrDWSnYNyuDqTImB+v7baY0rEoYhQY2ZLAbUsew + wg+KC2RjkapaZ7CFbCMs7xaWzZ5p/nfWOtsb1GvmvCUZcHK5MYeVM5S+Xf0GnYg= + =7cg6 -----END PGP MESSAGE----- - fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4 + fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4 unencrypted_suffix: _unencrypted version: 3.6.1