diff --git a/README.md b/README.md index b9e7ffa..4e9d24a 100644 --- a/README.md +++ b/README.md @@ -144,10 +144,20 @@ $ ssh-keygen -p -N "" -f /tmp/id_rsa $ nix-shell -p gnupg -p ssh-to-pgp --run "ssh-to-pgp -private-key -i /tmp/id_rsa | gpg --import --quiet" ``` -The hex string printed here is your GPG fingerprint that can be exported to `SOPS_PGP_FP`. +The hex string printed here is your GPG fingerprint that can written to your [`.sops.yaml`](https://github.com/mozilla/sops#using-sops-yaml-conf-to-select-kms-pgp-for-new-files) in the root of your configuration directory or repository. -```console -$ export SOPS_PGP_FP=2504791468b153b8a3963cc97ba53d1919c5dfd4 +```yaml +# This example uses yaml anchors which allows to name keys +# and re-use for multiple keys in a flexible way. +# Also see https://github.com/Mic92/dotfiles/blob/master/nixos/.sops.yaml +# for a more complex example +keys: + - &admin 2504791468b153b8a3963cc97ba53d1919c5dfd4 +creation_rules: + - path_regex: secrets/[^/]+\.yaml$ + key_groups: + - pgp: + - *admin ``` If you have generated a GnuPG key directly you can get your fingerprint like this: @@ -179,32 +189,36 @@ $ nix-shell -p ssh-to-pgp --run "ssh-to-pgp -i /etc/ssh/ssh_host_rsa_key -o serv 0fd60c8c3b664aceb1796ce02b318df330331003 ``` -Also the hex string here is the fingerprint of your server's gpg key that can be exported -append to `SOPS_PGP_FP`: +Also the hex string here is the fingerprint of your server's gpg key that can be exported append to `.sops.yaml`: -```console -$ export SOPS_PGP_FP=${SOPS_PGP_FP}:2504791468b153b8a3963cc97ba53d1919c5dfd4 +```yaml +keys: + - &admin 2504791468b153b8a3963cc97ba53d1919c5dfd4 + - &server 0fd60c8c3b664aceb1796ce02b318df330331003 +creation_rules: + - path_regex: secrets/[^/]+\.yaml$ + key_groups: + - pgp: + - *admin + - *server ``` If you prefer having a separate GnuPG key, see [Use with GnuPG instead of ssh keys](#use-with-gnupg-instead-of-ssh-keys). ### 4. Create a sops file -To create a sops file you need to set export `SOPS_PGP_FP` to include both the fingerprint -of your personal gpg key (and your colleagues) and your servers: +To create a sops file you need write a `.sops.yaml` as described above and +import your personal gpg key (and your colleagues) and your servers into your +gpg key chain. -```console -$ export SOPS_PGP_FP="2504791468b153b8a3963cc97ba53d1919c5dfd4,2504791468b153b8a3963cc97ba53d1919c5dfd4" -``` - -sops-nix automates that with a hook for nix-shell and also takes care of importing all keys, allowing -public keys to be stored in git: +sops-nix automates importing gpg keys with a hook for nix-shell allowing public +keys to be shared via version control (i.e. git): ```nix # shell.nix with import {}; mkShell { - # imports all files ending in .asc/.gpg and sets $SOPS_PGP_FP. + # imports all files ending in .asc/.gpg sopsPGPKeyDirs = [ "./keys/hosts" "./keys/users" @@ -214,8 +228,23 @@ mkShell { # "./keys/users/mic92.asc" # "./keys/hosts/server01.asc" #]; + + # This hook can also import gpg keys into its own seperate + # gpg keyring instead of using the default one. This allows + # to isolate otherwise unrelated server keys from the user gpg keychain. + # By uncommenting the following lines, it will set GNUPGHOME + # to .git/gnupg. + # Storing it inside .git prevents accedentially commiting private keys. + # After setting this option you will also need to import your own + # private key into keyring, i.e. using a a command like this + # (replacing 0000000000000000000000000000000000000000 with your fingerprint) + # $ (unset GNUPGHOME; gpg --armor --export-secret-key 0000000000000000000000000000000000000000) | gpg --import + #sopsCreateGPGHome = true; + # To use a different directory for gpg dirs set sopsGPGHome + #sopsGPGHome = "${toString ./.}/../gnupg"; + nativeBuildInputs = [ - (pkgs.callPackage {}).sops-pgp-hook + (pkgs.callPackage {}).sops-import-keys-hook ]; } ``` diff --git a/default.nix b/default.nix index 9127ae5..bdecfff 100644 --- a/default.nix +++ b/default.nix @@ -6,7 +6,11 @@ }; in rec { sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key {}; - sops-pgp-hook = pkgs.callPackage ./pkgs/sops-pgp-hook { }; + sops-pgp-hook = pkgs.lib.warn '' + sops-pgp-hook is deprecated, use sops-import-keys-hook instead. + Also see https://github.com/Mic92/sops-nix/issues/98 + '' pkgs.callPackage ./pkgs/sops-pgp-hook { }; + sops-import-keys-hook = pkgs.callPackage ./pkgs/sops-import-keys-hook { }; inherit sops-install-secrets; # backwards compatibility diff --git a/flake.nix b/flake.nix index b931857..0ce8e61 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ let localPkgs = import ./default.nix { pkgs = final; }; in { - inherit (localPkgs) sops-install-secrets sops-init-gpg-key sops-pgp-hook; + inherit (localPkgs) sops-install-secrets sops-init-gpg-key sops-pgp-hook sops-import-keys-hook; # backward compatibility inherit (prev) ssh-to-pgp; }; diff --git a/pkgs/sops-import-keys-hook/default.nix b/pkgs/sops-import-keys-hook/default.nix new file mode 100644 index 0000000..66b94aa --- /dev/null +++ b/pkgs/sops-import-keys-hook/default.nix @@ -0,0 +1,8 @@ +{ stdenv, makeSetupHook, gnupg, sops, nix }: + +(makeSetupHook { + substitutions = { + gpg = "${gnupg}/bin/gpg"; + }; + deps = [ sops gnupg ]; +} ./sops-import-keys-hook.bash) diff --git a/pkgs/sops-import-keys-hook/hook_test.go b/pkgs/sops-import-keys-hook/hook_test.go new file mode 100644 index 0000000..0cfdce0 --- /dev/null +++ b/pkgs/sops-import-keys-hook/hook_test.go @@ -0,0 +1,76 @@ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "testing" +) + +// ok fails the test if an err is not nil. +func ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +func TestShellHook(t *testing.T) { + assets := os.Getenv("TEST_ASSETS") + if assets == "" { + _, filename, _, _ := runtime.Caller(0) + assets = path.Join(path.Dir(filename), "test-assets") + } + tempdir, err := ioutil.TempDir("", "testdir") + ok(t, err) + cmd := exec.Command("cp", "-vra", assets+"/.", tempdir) + fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + ok(t, cmd.Run()) + + defer os.RemoveAll(tempdir) + + cmd = exec.Command("nix-shell", path.Join(assets, "shell.nix"), "--run", "gpg --list-keys") + var stdoutBuf, stderrBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + cmd.Dir = tempdir + fmt.Println(tempdir) + err = cmd.Run() + stdout := stdoutBuf.String() + stderr := stderrBuf.String() + fmt.Printf("$ %s\nstdout: \n%s\nstderr: \n%s\n", strings.Join(cmd.Args, " "), stdout, stderr) + ok(t, err) + + expectedKeys := []string{ + "C6DA56E69A7C756564A8AFEB4A6B05B714D13EFD", + "4EC40F8E04A945339F7F7C0032C5225271038E3F", + "7FB89715AADA920D65D25E63F9BA9DEBD03F57C0", + "E3B7464FBE89F5378ED4BC60FC925B42FC8B773D", + } + for _, key := range expectedKeys { + if !strings.Contains(stdout, key) { + t.Fatalf("'%v' not in '%v'", key, stdout) + } + } + + // it should ignore subkeys from ./keys/key-with-subkeys.asc + subkey := "94F174F588090494E73D0835A79B1680BC4D9A54" + if strings.Contains(stdout, subkey) { + t.Fatalf("subkey found in %s", stdout) + } + + expectedStderr := "./non-existing-key.gpg does not exists" + if !strings.Contains(stderr, expectedStderr) { + t.Fatalf("'%v' not in '%v'", expectedStderr, stdout) + } + +} diff --git a/pkgs/sops-import-keys-hook/sops-import-keys-hook.bash b/pkgs/sops-import-keys-hook/sops-import-keys-hook.bash new file mode 100644 index 0000000..8a2e9e0 --- /dev/null +++ b/pkgs/sops-import-keys-hook/sops-import-keys-hook.bash @@ -0,0 +1,33 @@ +_sopsAddKey() { + @gpg@ --quiet --import "$key" + local fpr + # only add the first fingerprint, this way we ignore subkeys + fpr=$(@gpg@ --with-fingerprint --with-colons --show-key "$key" \ + | awk -F: '$1 == "fpr" { print $10; exit }') +} + +sopsImportKeysHook() { + local key dir + if [ -n "${sopsCreateGPGHome}" ]; then + export GNUPGHOME=${sopsGPGHome:-$(pwd)/.git/gnupg} + mkdir -m 700 -p $GNUPGHOME + fi + for key in ${sopsPGPKeys-}; do + if [[ -f "$key" ]]; then + _sopsAddKey "$key" + else + echo "$key does not exists" >&2 + fi + done + for dir in ${sopsPGPKeyDirs-}; do + while IFS= read -r -d '' key; do + _sopsAddKey "$key" + done < <(find -L "$dir" -type f \( -name '*.gpg' -o -name '*.asc' \) -print0) + done +} + +if [ -z "${shellHook-}" ]; then + shellHook=sopsImportKeysHook +else + shellHook="sopsImportKeysHook;${shellHook}" +fi diff --git a/pkgs/sops-import-keys-hook/test-assets/existing-key.gpg b/pkgs/sops-import-keys-hook/test-assets/existing-key.gpg new file mode 100644 index 0000000..eba3738 Binary files /dev/null and b/pkgs/sops-import-keys-hook/test-assets/existing-key.gpg differ diff --git a/pkgs/sops-import-keys-hook/test-assets/keys/key-with-subkeys.asc b/pkgs/sops-import-keys-hook/test-assets/keys/key-with-subkeys.asc new file mode 100644 index 0000000..71f5405 --- /dev/null +++ b/pkgs/sops-import-keys-hook/test-assets/keys/key-with-subkeys.asc @@ -0,0 +1,61 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +W1YR5NNzw2GqjhH3nhj/SnUwXg== +=jshU +-----END PGP PUBLIC KEY BLOCK----- diff --git a/pkgs/sops-import-keys-hook/test-assets/keys/key.asc b/pkgs/sops-import-keys-hook/test-assets/keys/key.asc new file mode 120000 index 0000000..34bc240 --- /dev/null +++ b/pkgs/sops-import-keys-hook/test-assets/keys/key.asc @@ -0,0 +1 @@ +../../../sops-install-secrets/test-assets/key.asc \ No newline at end of file diff --git a/pkgs/sops-import-keys-hook/test-assets/keys/key.gpg b/pkgs/sops-import-keys-hook/test-assets/keys/key.gpg new file mode 100644 index 0000000..c168d74 Binary files /dev/null and b/pkgs/sops-import-keys-hook/test-assets/keys/key.gpg differ diff --git a/pkgs/sops-import-keys-hook/test-assets/shell.nix b/pkgs/sops-import-keys-hook/test-assets/shell.nix new file mode 100644 index 0000000..88c4b25 --- /dev/null +++ b/pkgs/sops-import-keys-hook/test-assets/shell.nix @@ -0,0 +1,15 @@ +# shell.nix +with import {}; +mkShell { + sopsPGPKeyDirs = [ + "./keys" + ]; + sopsPGPKeys = [ + "./existing-key.gpg" + "./non-existing-key.gpg" + ]; + sopsCreateGPGHome = "1"; + nativeBuildInputs = [ + (pkgs.callPackage ../../.. {}).sops-import-keys-hook + ]; +}