1
0
Fork 0
mirror of https://github.com/Mic92/sops-nix.git synced 2024-12-14 11:57:52 +00:00

add tests + ssh key support

This commit is contained in:
Jörg Thalheim 2020-07-12 13:50:55 +01:00
parent bb21643ce1
commit b75e51c423
No known key found for this signature in database
GPG key ID: 003F2096411B5F92
29 changed files with 706 additions and 102 deletions

3
.gitignore vendored
View file

@ -16,4 +16,5 @@
# Dependency directories (remove the comment below to include it)
# vendor/
/pkgs/sops-install-secrets/sops-install-secrets
/pkgs/sops-install-secrets/sops-install-secrets
/pkgs/ssh-to-pgp/ssh-to-pgp

10
go.mod Normal file
View file

@ -0,0 +1,10 @@
module github.com/Mic92/sops-nix
go 1.14
require (
github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c
go.mozilla.org/sops/v3 v3.5.0
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0
)

View file

@ -208,8 +208,9 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@ -260,9 +261,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

View file

@ -78,6 +78,7 @@ let
# Does this need to be configurable?
secretsMountPoint = "/run/secrets.d";
symlinkPath = "/run/secrets";
inherit (cfg) gnupgHome sshKeyPaths;
});
in {
options.sops = {
@ -97,17 +98,38 @@ in {
};
gnupgHome = mkOption {
type = types.str;
default = "/root/.gnupg";
type = types.nullOr types.str;
default = null;
example = "/root/.gnupg";
description = ''
Path to gnupg database directory containing the key for decrypting sops file
Path to gnupg database directory containing the key for decrypting sops file.
'';
};
sshKeyPaths = mkOption {
type = types.listOf types.path;
default = if config.services.openssh.enable then
map (e: e.path) (lib.filter (e: e.type == "rsa") config.services.openssh.hostKeys)
else [];
description = ''
Path to ssh keys added as GPG keys during sops description.
This option must be explicitly unset if <literal>config.sops.sshKeyPaths</literal>.
'';
};
};
config = mkIf (cfg.secrets != {}) {
assertions = [{
assertion = cfg.gnupgHome != null -> cfg.sshKeyPaths == [];
message = "config.sops.gnupgHome and config.sops.sshKeyPaths are mutual exclusive";
} {
assertion = cfg.gnupgHome == null -> cfg.sshKeyPaths != [];
message = "Either config.sops.sshKeyPaths and config.sops.gnupgHome must be set";
}];
system.activationScripts.setup-secrets = stringAfter [ "users" "groups" ] ''
echo setting up secrets...
SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg GNUPGHOME=${cfg.gnupgHome} ${sops-install-secrets}/bin/sops-install-secrets ${manifest}
${optionalString (cfg.gnupgHome != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${manifest}
'';
};
}

View file

@ -72,4 +72,6 @@ gpg --export --armor
echo 'EOF'
rm "${GNUPGHOME}/key-template"
parent=$(dirname "$FINAL_GNUPGHOME")
mkdir -p "$parent"
mv "$GNUPGHOME" "$FINAL_GNUPGHOME"

View file

@ -5,10 +5,9 @@ buildGoModule {
hardeningDisable = [ "all" ];
src = ./.;
src = ../..;
vendorSha256 = "1ky7xzsx12d8m4kvqkayqzybkf3s0w21d6m8qlhvrm00fmyidkxj";
shellHook = ''
unset GOFLAGS
'';
subPackages = [ "pkgs/sops-install-secrets" ];
vendorSha256 = "sha256-O0z+oEffOOZa/bn2gV9onLVbPBHsNDH2yq1CZPi8w58=";
}

View file

@ -1,9 +0,0 @@
module github.com/Mic92/sops-install-secrets
go 1.14
require (
github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c
go.mozilla.org/sops/v3 v3.5.0
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
)

View file

@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
@ -10,6 +11,8 @@ import (
"strconv"
"strings"
"github.com/Mic92/sops-nix/pkgs/sshkeys"
"github.com/mozilla-services/yaml"
"go.mozilla.org/sops/v3/decrypt"
"golang.org/x/sys/unix"
@ -36,6 +39,8 @@ type manifest struct {
Secrets []secret `json:"secrets"`
SecretsMountPoint string `json:"secretsMountpoint"`
SymlinkPath string `json:"symlinkPath"`
SSHKeyPaths []string `json:"sshKeyPaths"`
GnupgHome string `json:"gnupgHome`
}
func readManifest(path string) (*manifest, error) {
@ -52,7 +57,26 @@ func readManifest(path string) (*manifest, error) {
return &m, nil
}
func prepareSymlinks(targetDir string, secrets []secret) error {
func symlinkSecret(targetFile string, secret *secret) error {
for {
currentLinkTarget, err := os.Readlink(secret.Path)
if os.IsNotExist(err) {
if err := os.Symlink(targetFile, secret.Path); err != nil {
return fmt.Errorf("Cannot create symlink '%s': %s", secret.Path, err)
}
return nil
} else if err != nil {
return fmt.Errorf("Cannot read symlink: '%s'", err)
} else if currentLinkTarget == targetFile {
return nil
}
if err := os.Remove(secret.Path); err != nil {
return fmt.Errorf("Cannot override %s", secret.Path)
}
}
}
func symlinkSecrets(targetDir string, secrets []secret) error {
for _, secret := range secrets {
targetFile := filepath.Join(targetDir, secret.Name)
if targetFile == secret.Path {
@ -62,21 +86,8 @@ func prepareSymlinks(targetDir string, secrets []secret) error {
if err := os.MkdirAll(parent, os.ModePerm); err != nil {
return fmt.Errorf("Cannot create parent directory of '%s': %s", secret.Path, err)
}
for {
currentLinkTarget, err := os.Readlink(secret.Path)
if os.IsNotExist(err) {
if err := os.Symlink(targetFile, secret.Path); err != nil {
return fmt.Errorf("Cannot create symlink '%s': %s", secret.Path, err)
}
return nil
} else if err != nil {
return fmt.Errorf("Cannot read symlink: '%s'", err)
} else if currentLinkTarget == targetFile {
return nil
}
if err := os.Remove(secret.Path); err != nil {
return fmt.Errorf("Cannot override %s", secret.Path)
}
if err := symlinkSecret(targetFile, &secret); err != nil {
return err
}
}
return nil
@ -131,7 +142,7 @@ func decryptSecrets(secrets []secret) error {
return nil
}
func prepareSecretFs(mountpoint string, keysGid int) error {
func mountSecretFs(mountpoint string, keysGid int) error {
if err := os.MkdirAll(mountpoint, 0750); err != nil {
return fmt.Errorf("Cannot create directory '%s': %s", mountpoint, err)
}
@ -247,6 +258,10 @@ func validateManifest(m *manifest) 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.")
}
for i := range m.Secrets {
if err := validateSecret(&m.Secrets[i]); err != nil {
return err
@ -288,13 +303,61 @@ func atomicSymlink(oldname, newname string) error {
return os.RemoveAll(d)
}
func importSSHKeys(keyPaths []string, gpgHome string) error {
secringPath := filepath.Join(gpgHome, "secring.gpg")
secring, err := os.Create(secringPath)
if err != nil {
return fmt.Errorf("Cannot create %s: %s", secringPath, err)
}
for _, path := range keyPaths {
sshKey, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("Cannot read ssh key '%s': %s", path, err)
}
gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey)
if err != nil {
return err
}
if err := gpgKey.SerializePrivate(secring, nil); err != nil {
return fmt.Errorf("Cannot write secring: %s", err)
}
}
return nil
}
type keyring struct {
path string
}
func (k *keyring) Remove() {
os.RemoveAll(k.path)
os.Unsetenv("GNUPGHOME")
}
func setupGPGKeyring(sshKeys []string, parentDir string) (*keyring, error) {
dir, err := ioutil.TempDir(parentDir, "gpg")
if err != nil {
return nil, fmt.Errorf("Cannot create gpg home in '%s': %s", parentDir, err)
}
k := keyring{dir}
if err := importSSHKeys(sshKeys, dir); err != nil {
os.RemoveAll(dir)
return nil, err
}
os.Setenv("GNUPGHOME", dir)
return &k, nil
}
func installSecrets(args []string) error {
if len(args) <= 1 {
return fmt.Errorf("USAGE: %s manifest.json", args)
}
manifest, err := readManifest(args[1])
if err != nil {
return fmt.Errorf("%s", err)
return err
}
if err := validateManifest(manifest); err != nil {
@ -306,16 +369,24 @@ func installSecrets(args []string) error {
return err
}
if err := mountSecretFs(manifest.SecretsMountPoint, keysGid); err != nil {
return fmt.Errorf("Failed to mount filesystem for secrets: %s", err)
}
if len(manifest.SSHKeyPaths) != 0 {
keyring, err := setupGPGKeyring(manifest.SSHKeyPaths, manifest.SecretsMountPoint)
if err != nil {
return fmt.Errorf("Error setting up gpg keyring: %s", err)
}
defer keyring.Remove()
} else if manifest.GnupgHome != "" {
os.Setenv("GNUPGHOME", manifest.GnupgHome)
}
if err := decryptSecrets(manifest.Secrets); err != nil {
return err
}
if err := prepareSecretFs(manifest.SecretsMountPoint, keysGid); err != nil {
return fmt.Errorf("Failed to mount filesystem for secrets: %s", err)
}
if err := prepareSymlinks(manifest.SymlinkPath, manifest.Secrets); err != nil {
return fmt.Errorf("Failed to prepare symlinks to secret store: %s", err)
}
secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGid)
if err != nil {
return fmt.Errorf("Failed to prepare new secrets directory: %s", err)
@ -323,6 +394,9 @@ func installSecrets(args []string) error {
if err := writeSecrets(*secretDir, manifest.Secrets); err != nil {
return fmt.Errorf("Cannot write secrets: %s", err)
}
if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets); err != nil {
return fmt.Errorf("Failed to prepare symlinks to secret store: %s", err)
}
if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil {
return fmt.Errorf("Cannot update secrets symlink: %s", err)
}

View file

@ -43,24 +43,48 @@ func writeManifest(t *testing.T, dir string, m *manifest) string {
return filename
}
func TestCliArgs(t *testing.T) {
func testAssetPath() string {
_, filename, _, _ := runtime.Caller(0)
var testSecrets = path.Join(path.Dir(filename), "test-secrets")
return path.Join(path.Dir(filename), "test-assets")
}
type testDir struct {
path, secretsPath, symlinkPath string
}
func (dir testDir) Remove() {
os.RemoveAll(dir.path)
}
func newTestDir(t *testing.T) testDir {
tempdir, err := ioutil.TempDir("", "symlinkDir")
ok(t, err)
defer os.RemoveAll(tempdir)
secretsPath := path.Join(tempdir, "secrets.d")
symlinkPath := path.Join(tempdir, "secrets")
gpgHome := path.Join(tempdir, "gpg-home")
return testDir{tempdir, path.Join(tempdir, "secrets.d"), path.Join(tempdir, "secrets")}
}
func testInstallSecret(t *testing.T, testdir testDir, m *manifest) {
path := writeManifest(t, testdir.path, m)
ok(t, installSecrets([]string{"sops-install-secrets", path}))
}
func testGPG(t *testing.T) {
assets := testAssetPath()
testdir := newTestDir(t)
defer testdir.Remove()
gpgHome := path.Join(testdir.path, "gpg-home")
gpgEnv := append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", gpgHome))
ok(t, os.Mkdir(gpgHome, os.FileMode(0700)))
os.Setenv("GNUPGHOME", gpgHome)
cmd := exec.Command("gpg", "--import", path.Join(testSecrets, "key.asc"))
cmd := exec.Command("gpg", "--import", path.Join(assets, "key.asc"))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = gpgEnv
ok(t, cmd.Run())
stopGpgCmd := exec.Command("gpgconf", "--kill", "gpg-agent")
stopGpgCmd.Stdout = os.Stdout
stopGpgCmd.Stderr = os.Stderr
stopGpgCmd.Env = gpgEnv
defer func() {
if err := stopGpgCmd.Run(); err != nil {
fmt.Printf("failed to stop gpg-agent: %s\n", err)
@ -73,43 +97,40 @@ func TestCliArgs(t *testing.T) {
Key: "test_key",
Owner: "nobody",
Group: "nogroup",
SourceFile: path.Join(testSecrets, "secrets.yaml"),
Path: path.Join(tempdir, "test-target"),
SopsFile: path.Join(assets, "secrets.yaml"),
Path: path.Join(testdir.path, "test-target"),
Mode: "0400",
RestartServices: []string{"affected-service"},
ReloadServices: make([]string, 0),
}
var jsonSecret secret
var jsonSecret, binarySecret secret
// should not create a symlink
jsonSecret = yamlSecret
jsonSecret.Name = "test2"
jsonSecret.Owner = "root"
jsonSecret.Format = "json"
jsonSecret.Group = "root"
jsonSecret.SourceFile = path.Join(testSecrets, "secrets.json")
jsonSecret.Path = path.Join(symlinkPath, "test2")
jsonSecret.SopsFile = path.Join(assets, "secrets.json")
jsonSecret.Path = path.Join(testdir.secretsPath, "test2")
jsonSecret.Mode = "0700"
var binarySecret secret
binarySecret = yamlSecret
binarySecret.Name = "test3"
binarySecret.Format = "binary"
binarySecret.SourceFile = path.Join(testSecrets, "secrets.bin")
binarySecret.Path = path.Join(symlinkPath, "test3")
binarySecret.SopsFile = path.Join(assets, "secrets.bin")
binarySecret.Path = path.Join(testdir.secretsPath, "test3")
manifest := manifest{
Secrets: []secret{yamlSecret, jsonSecret, binarySecret},
SecretsMountPoint: secretsPath,
SymlinkPath: symlinkPath,
SecretsMountPoint: testdir.secretsPath,
SymlinkPath: testdir.symlinkPath,
GnupgHome: gpgHome,
}
manifestPath := writeManifest(t, tempdir, &manifest)
testInstallSecret(t, testdir, &manifest)
err = installSecrets([]string{"sops-install-secrets", manifestPath})
ok(t, err)
_, err = os.Stat(manifest.SecretsMountPoint)
_, err := os.Stat(manifest.SecretsMountPoint)
ok(t, err)
_, err = os.Stat(manifest.SymlinkPath)
@ -150,13 +171,44 @@ func TestCliArgs(t *testing.T) {
content, err = ioutil.ReadFile(binarySecret.Path)
ok(t, err)
equals(t, "binary_value\n", string(content))
manifestPath = writeManifest(t, symlinkPath, &manifest)
testInstallSecret(t, testdir, &manifest)
err = installSecrets([]string{"sops-install-secrets", manifestPath})
target, err := os.Readlink(testdir.symlinkPath)
ok(t, err)
target, err := os.Readlink(symlinkPath)
equals(t, path.Join(secretsPath, "2"), target)
equals(t, path.Join(testdir.secretsPath, "2"), target)
}
func testSSHKey(t *testing.T) {
assets := testAssetPath()
testdir := newTestDir(t)
defer testdir.Remove()
s := secret{
Name: "test",
Key: "test_key",
Owner: "nobody",
Group: "nogroup",
SopsFile: path.Join(assets, "secrets.yaml"),
Path: path.Join(testdir.path, "test-target"),
Mode: "0400",
RestartServices: []string{"affected-service"},
ReloadServices: make([]string, 0),
}
m := manifest{
Secrets: []secret{s},
SecretsMountPoint: testdir.secretsPath,
SymlinkPath: testdir.symlinkPath,
SSHKeyPaths: []string{path.Join(assets, "ssh-key")},
}
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)
}

View file

@ -0,0 +1,47 @@
let
makeTest = import <nixpkgs/nixos/tests/make-test-python.nix>;
in {
ssh-keys = makeTest {
nodes.server = { ... }: {
imports = [ ../../modules/sops ];
services.openssh.enable = true;
services.openssh.hostKeys = [{
type = "rsa";
bits = 4096;
path = ./test-assets/ssh-key;
}];
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key = {};
};
testScript = ''
start_all()
server.succeed("cat /run/secrets/test_key | grep -q test_value")
'';
};
gpg-keys = makeTest {
nodes.server = { pkgs, lib, ... }: {
imports = [ ../../modules/sops ];
sops.gnupgHome = "/run/gpghome";
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key = {};
# must run before sops
system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] ''
cp -r ${./test-assets/gnupghome} /run/gpghome
chmod -R 700 /run/gpghome
'';
# Useful for debugging
#environment.systemPackages = [ pkgs.gnupg pkgs.sops ];
#environment.variables = {
# GNUPGHOME = "/run/gpghome";
# SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg";
# SOPSFILE = "${./test-assets/secrets.yaml}";
#};
};
testScript = ''
start_all()
server.succeed("cat /run/secrets/test_key | grep -q test_value")
'';
};
}

View file

@ -0,0 +1,4 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [ go delve utillinux gnupg ];
}

View file

@ -0,0 +1,43 @@
test_key: ENC[AES256_GCM,data:4cC2PTi7xVPZPA==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:ZaKi9m6ziFKNV+gx7XedTw==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
lastmodified: '2020-07-12T08:04:25Z'
mac: ENC[AES256_GCM,data:Sw+u03EAxagKQ9qd4Vwr5BRrnAdtPlUC660fpaVb62W481YVmcUo/CW+SBwdZhn1oSAqGDFE0exqWp4+FRhBPNnxcatI2kjnJ/m9INZhrjgTGVcSVC+pLfXYrmtqCxJCS1clREuQ89QG3inDQvgJ2M+A8S6qhlwPfXlIhuyHMI8=,iv:zpdbtNjicBx74MnYqLwMkY0atPFe7BDJI8o4VDhGlb0=,tag:yGnUUSrs+6uFZGuX33yVJg==,type:str]
pgp:
- 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
-----END PGP MESSAGE-----
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
-----END PGP MESSAGE-----
fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4
unencrypted_suffix: _unencrypted
version: 3.5.0

View file

@ -11,8 +11,9 @@ export GNUPGHOME=$(mktemp -d)
trap "gpgconf --kill gpg-agent && rm -rf $GNUPGHOME" EXIT
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
gpg --import "$DIR/key.asc"
gpg --import "$DIR/key.asc" "$DIR/ssh-key.asc"
gpg --fingerprint --list-keys
fpr=$(gpg --with-fingerprint --with-colons --show-key ./key.asc | awk -F: '$1 == "fpr" { print $10;}')
fpr=$(gpg --with-fingerprint --with-colons --show-key "$DIR/key.asc" "$DIR/ssh-key.asc" | awk -F: '$1 == "fpr" { print $10;}' | xargs | sed -e 's/ /,/g')
sops --pgp "$fpr" "$@"

View file

@ -0,0 +1,38 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

View file

@ -0,0 +1,42 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=WFkZ
-----END PGP PRIVATE KEY BLOCK-----

View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDtwQhY3uyYF0EtACthbkam7TA672VqWH6epfX/m6KqVTbqozHstCp6CRuBhGxTtkHQNbf38iQkwePPaqXQWMD1ahyKmgUg1LNek1At6X+Q4kOHTKyS8ygbavKy7MhGT8TE3fG/M1PZoz58tw7Coe0tLlQh+wPnIFEhBeR15ZxZAaJWbxYXnvijbXwkxDV/KJ9itElmH3SpkOyhUhZCThWsORNF9ipbZVwiVRwOjoOdHISZUrUvcamhxxt8TM8sKWzao2GXnQTiMvf8xMfyAtHLGBdPpiVcSym2FvjPqRSmKoVFDFwsNQMfcDv1eSH1/c5art14KyKwuJpkiJUPWwfdppdjHbBV26moE+4YvMi04Z7tw2RMi0x8tXaiYxwhNvyDVqBdSD56AjckTmHo5dZCjbcsV00gzDiVZlWgP8RCzr55BJlMVjKLM6YkzVZl+c5kJorsfL3NyohxC/hrIwItQoqJW9XtA/rSd9/1AdkrE+iPahsCQQ1ZhH0KiUr00Pk= joerg@turingmachine

View file

@ -1,25 +0,0 @@
test_key: ENC[AES256_GCM,data:bjCOFe3ngTjbBA==,iv:v5M/Ws9d4yBAMFZ13qJ6hkLPceT7a2wcxiJjVp8mvSQ=,tag:zAWPg4H7E2Eavq+iE3yXpg==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
lastmodified: '2020-07-05T19:01:53Z'
mac: ENC[AES256_GCM,data:CrYKSxkKZynq9wwD11GE280VIwv0FqFRNgm5EF588fTN4RwVs8s51DoN4Qx9kPoabYH/lwv+OdeVRq9mbYxtclhJh6ind04pmz5UZps/V5PgJDFd0ckLc9SCeqnEmvBFsp9WM+Tr6M/v9QgeamK+GGovw0+ePwgU1VADPnoyTAo=,iv:kgtewrlBa2N3qyhd7ipTZSxMQndvTy2p8PIM3lynrNY=,tag:aUzPq+u2fi5zziYXVfdaYQ==,type:str]
pgp:
- created_at: '2020-07-05T19:01:34Z'
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA/m6nevQP1fAAQgAnUb/ShUMr5vkDLG+LWGD8DT6etv3kPoJIhq7C0d7y/BX
0UHTcxR4coZ55jB6+juxGmh9e9WhawoxHToUs/edxJHxOlC19HunWv9EvTFkKYVf
MAFu+Gfx6cWgDPOrrtvFVZ3YUqPizoOUmP/Op5y/gxhKEYBtD1/5YuSGNnL1VYkv
ZEfgPYtAq5Jmx+jF2Smkb/T6cWFV79WUJTZbYRQs466qQHtZGU9Ms9amxQ73Xf/Y
KhysQCu1yuVxGzBuPVrT5jbZdgaHm5KlmSIGwk47m+PH4ienkhE76WTUCr4xx9R8
n8/+eVga9cYWbwHYoY84GNUi7fbx5LTD3x19LnVk9dJeAahWrgzT3azwJRPH7WdW
U8+La2v36cucQ/u7+KraVCofOuNXBsjZOJokte97DtKwqtPXRSbyL8owSA90O3Vh
++zsV3ufM+8TO+9LDAjMqmF0lpQuGexNPafdkuAGdQ==
=5GUN
-----END PGP MESSAGE-----
fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0
unencrypted_suffix: _unencrypted
version: 3.5.0

112
pkgs/ssh-to-pgp/main.go Normal file
View file

@ -0,0 +1,112 @@
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"syscall"
"github.com/Mic92/sops-nix/pkgs/sshkeys"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/ssh/terminal"
)
type options struct {
publicKey, privateKey, format, out string
}
func parseFlags(args []string) options {
var opts options
f := flag.NewFlagSet(args[0], flag.ExitOnError)
f.StringVar(&opts.publicKey, "pubkey", "", "Path to public key. Reads from standard input if equal to '-'")
f.StringVar(&opts.privateKey, "privkey", "", "Path to private key. Reads from standard input if equal to '-'")
f.StringVar(&opts.format, "format", "auto", "GPG format encoding (auto|binary|armor)")
f.StringVar(&opts.out, "o", "-", "Output path. Prints by default to standard output")
f.Parse(args[1:])
if opts.format == "auto" {
if opts.out == "-" && terminal.IsTerminal(syscall.Stdout) {
opts.format = "armor"
} else {
opts.format = "binary"
}
}
if opts.publicKey != "" && opts.privateKey != "" {
fmt.Fprintln(os.Stderr, "-pubkey and -privkey are mutual exclusive")
os.Exit(1)
}
if opts.publicKey == "" && opts.privateKey == "" {
fmt.Fprintln(os.Stderr, "Either -pubkey and -privkey must be specified")
os.Exit(1)
}
return opts
}
func convertKeys(args []string) error {
opts := parseFlags(args)
var err error
var sshKey []byte
keyPath := opts.privateKey
if opts.publicKey != "" {
keyPath = opts.publicKey
}
if keyPath == "-" {
sshKey, _ = ioutil.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("error reading stdin: %s", err)
}
} else {
sshKey, err = ioutil.ReadFile(keyPath)
if err != nil {
return fmt.Errorf("error reading %s: %s", opts.privateKey, err)
}
}
writer := io.WriteCloser(os.Stdout)
if opts.out != "-" {
writer, err = os.Create(opts.out)
if err != nil {
return fmt.Errorf("failed to create %s: %s", opts.out, err)
}
defer writer.Close()
}
if opts.format == "armor" {
keyType := openpgp.PrivateKeyType
if opts.publicKey != "" {
keyType = openpgp.PublicKeyType
}
writer, err = armor.Encode(writer, keyType, make(map[string]string))
if err != nil {
return fmt.Errorf("failed to encode armor writer")
}
defer writer.Close()
}
if opts.publicKey != "" {
gpgKey, err := sshkeys.SSHPublicKeyToPGP(sshKey)
if err != nil {
return err
}
gpgKey.Serialize(writer)
} else {
gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey)
if err != nil {
return err
}
gpgKey.SerializePrivate(writer, nil)
}
return err
}
func main() {
if err := convertKeys(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "%s: %s", os.Args[0], err)
os.Exit(1)
}
}

View file

@ -0,0 +1,47 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"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 TestCli(t *testing.T) {
_, filename, _, _ := runtime.Caller(0)
assets := path.Join(path.Dir(filename), "test-assets")
tempdir, err := ioutil.TempDir("", "testdir")
ok(t, err)
defer os.RemoveAll(tempdir)
out := path.Join(tempdir, "out")
pubKey := path.Join(assets, "id_rsa.pub")
privKey := path.Join(assets, "id_rsa")
cmds := [][]string{
{"ssh-to-pgp", "-pubkey", pubKey, "-o", out},
{"ssh-to-pgp", "-format=armor", "-pubkey", pubKey, "-o", out},
{"ssh-to-pgp", "-privkey", privKey, "-o", out},
{"ssh-to-pgp", "-format=armor", "-privkey", privKey, "-o", out},
}
for _, cmd := range cmds {
err = convertKeys(cmd)
ok(t, err)
cmd := exec.Command("gpg", "--with-fingerprint", "--show-key", out)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
ok(t, cmd.Run())
}
}

10
pkgs/ssh-to-pgp/shell.nix Normal file
View file

@ -0,0 +1,10 @@
with import <nixpkgs> {};
mkShell {
nativeBuildInputs = [
bashInteractive
go
delve
gnupg
];
hardeningDisable = [ "all" ];
}

View file

@ -0,0 +1,38 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA0nkukhKbrSDotpkIS3iBCZzAIp5PinFL9/B52i2pN55y5lLGCZ12
AeSmRsWsVbSI6+fSfE53ZsJ4mfsHNK6peg5In/QrE14AI8az4pJ2TUOyUG4FlK9KZOI8fy
t8yw+ov1wEvEFskjSZmOWiVskmxfyuvO5FDCeapdAV+E7dEYli+KSMM2WZS8x+K0cksuM9
ZG4rjmX/IbVUbZRqAxqhlYiUabsm0iq5l23r0SO2lo4ppdgVUzJLT3pAD8fjAN7f/BDP7i
cbVqe4NwbJ3h0HSiI0dFhCgCE05rgyxBLSF1xFG4AVtFo2w+tp0X7fwOv4slspDyZpCwOF
p0i0tN3GnlMQiqBLYWdcXwTwkTcO8W8rEBfAhyc/HADI2RoARpTop/BCg4ZpZmuhWfeHAA
eU6+Bt0dIeJMu+2z5Nv+r72bclPwBZwz9h3xmkQgzRfkO/n0fWJisHFv7wmtiLSBF4DJgY
0vspdKfuH1WmOkO2wk263es52+oExqO5w/So/whlAAAFiHeHPDl3hzw5AAAAB3NzaC1yc2
EAAAGBANJ5LpISm60g6LaZCEt4gQmcwCKeT4pxS/fwedotqTeecuZSxgmddgHkpkbFrFW0
iOvn0nxOd2bCeJn7BzSuqXoOSJ/0KxNeACPGs+KSdk1DslBuBZSvSmTiPH8rfMsPqL9cBL
xBbJI0mZjlolbJJsX8rrzuRQwnmqXQFfhO3RGJYvikjDNlmUvMfitHJLLjPWRuK45l/yG1
VG2UagMaoZWIlGm7JtIquZdt69EjtpaOKaXYFVMyS096QA/H4wDe3/wQz+4nG1anuDcGyd
4dB0oiNHRYQoAhNOa4MsQS0hdcRRuAFbRaNsPradF+38Dr+LJbKQ8maQsDhadItLTdxp5T
EIqgS2FnXF8E8JE3DvFvKxAXwIcnPxwAyNkaAEaU6KfwQoOGaWZroVn3hwAHlOvgbdHSHi
TLvts+Tb/q+9m3JT8AWcM/Yd8ZpEIM0X5Dv59H1iYrBxb+8JrYi0gReAyYGNL7KXSn7h9V
pjpDtsJNut3rOdvqBMajucP0qP8IZQAAAAMBAAEAAAGAU5/wX/tivTQBImPFRu83HdGZCW
grJE+Fppp2X7iark2XS2oB41obw/7MDfyGT3sul8SA/gDTMhH8hvmVUFpBXgyE0IDcCJLl
rVFKsbANrv9BvvEn6H6JKXI2JTTrHWc4Xee6ve2krKaXjIdYq/C6JhoSd2CYMI8fw9fcks
8KyOf0WeRPDDDG6rXyP1HCBA2Dm/6l8asW5pa8V9mLEXaoUth0V1oTv5dYLBFxi6QL7N/J
LmqfdnHaOFbTUzHRQMxMK1L04ETDhIUn6C6hnJfXetRInii5s2l8xCJjkd3bBzrWUk7Csz
3nxBqVPWIFoPrhU5W7lEJRngQMYRLS0HHq2puxhgcpxgr183uSBoXTggIUs1KnutDizZXs
Ioh5JhR5DQmlilGfRsh9pk+WjpbWT2RtR44ugkvcbBLYkU24KYAyzgjM88Zq3yoJeHfFea
2osxpSY02CDdY4YGNO1x0vSSkE4nGeiV4n+EK7HxaVY4IN+8Pr/6a2PB3J43OZ2heBAAAA
wET9bby2qLJJOE6bpYezwlCt7ip2UM07z8LL0Za/qeCyCmsMGGyO4GAnXZW52IRQbotCh9
gYoW3HqBZHWTt+ptEOkSywdOdaqw7Ib0HSgjkTw1IHIr/ij3eqFa83SosAbmtrwdEhiue9
Hm0McAN8d/9lpFxjE9DAfqvZNn5Gsnv6rqPmuqKXcdEClGVj6ciUAsTinDs++FkBczRIfj
uRBwqJonXRh/Ts9EKo/+AGDtB1Wx8iAu9mmttlgcqY9OT4sAAAAMEA+Y/MT94gR9pS7YVf
nZHPAZ8QP0Mz6czcGYjWkO9SzfA118ZR4ZLFWBGw7dqmmFOtUG+EoGvaolWqRoNO8++MpD
TlmOU+NsHIsafuhsGVju+v6cI3TFnIvhkPUEaWInMaPH8rW6zxT/Fh8+pRgDdluRhyNLE2
fB2AcNUHlvfHe4d1BiohRrMMb7GdeRInhhO7+NQWUVGwflkY1wUALczuTguTqmpxX3EEed
Tc1OO9uRfImDBEQR+9TbEbJgZtqlMhAAAAwQDX5zqzktidmM2iZVQoo6PuORO5vm8PvFT4
kckH6mW+dNz3NP3gtsxkJih01EJo77tjHAfoI0WVmiQekHxnDfMRWBVRYi1uZcpvC/Zy3r
CH3waJE8h4cwRlge7gDc50k4tp5DDdeSoj8ud8911pvOLlAewtu/IQz67lvlvMI4LyvDwz
BT6RBfsv3esiWqYFzX/1+mpFu/VhQ8rIERh22Y8AMLHCTcwAXXfYB5TUSTBRLBmtrb+Qy2
GlNV/y/LNbEMUAAAATam9lcmdAdHVyaW5nbWFjaGluZQ==
-----END OPENSSH PRIVATE KEY-----

View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDSeS6SEputIOi2mQhLeIEJnMAink+KcUv38HnaLak3nnLmUsYJnXYB5KZGxaxVtIjr59J8TndmwniZ+wc0rql6Dkif9CsTXgAjxrPiknZNQ7JQbgWUr0pk4jx/K3zLD6i/XAS8QWySNJmY5aJWySbF/K687kUMJ5ql0BX4Tt0RiWL4pIwzZZlLzH4rRySy4z1kbiuOZf8htVRtlGoDGqGViJRpuybSKrmXbevRI7aWjiml2BVTMktPekAPx+MA3t/8EM/uJxtWp7g3BsneHQdKIjR0WEKAITTmuDLEEtIXXEUbgBW0WjbD62nRft/A6/iyWykPJmkLA4WnSLS03caeUxCKoEthZ1xfBPCRNw7xbysQF8CHJz8cAMjZGgBGlOin8EKDhmlma6FZ94cAB5Tr4G3R0h4ky77bPk2/6vvZtyU/AFnDP2HfGaRCDNF+Q7+fR9YmKwcW/vCa2ItIEXgMmBjS+yl0p+4fVaY6Q7bCTbrd6znb6gTGo7nD9Kj/CGU= joerg@turingmachine

94
pkgs/sshkeys/convert.go Normal file
View file

@ -0,0 +1,94 @@
package sshkeys
import (
"crypto"
"crypto/rsa"
"fmt"
"reflect"
"time"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
"golang.org/x/crypto/ssh"
)
func parsePublicKey(publicKey []byte) (*rsa.PublicKey, error) {
key, _, _, _, err := ssh.ParseAuthorizedKey(publicKey)
if err != nil {
return nil, fmt.Errorf("failed to parse public ssh key: %s", err)
}
cryptoPublicKey, ok := key.(ssh.CryptoPublicKey)
if !ok {
return nil, fmt.Errorf("Unsupported public key algo: %s", key.Type())
}
rsaKey, ok := cryptoPublicKey.CryptoPublicKey().(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("Unsupported public key algo: %s", key.Type())
}
return rsaKey, nil
}
func SSHPublicKeyToPGP(sshPublicKey []byte) (*packet.PublicKey, error) {
rsaKey, err := parsePublicKey(sshPublicKey)
if err != nil {
return nil, err
}
return packet.NewRSAPublicKey(time.Unix(0, 0), rsaKey), nil
}
func parsePrivateKey(sshPrivateKey []byte) (*rsa.PrivateKey, error) {
privateKey, err := ssh.ParseRawPrivateKey(sshPrivateKey)
if err != nil {
panic(err)
}
rsaKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("Only RSA keys are supported right now, got: %s", reflect.TypeOf(privateKey))
}
return rsaKey, nil
}
func SSHPrivateKeyToPGP(sshPrivateKey []byte) (*openpgp.Entity, error) {
key, err := parsePrivateKey(sshPrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to parse private ssh key: %s", err)
}
// Let's make keys reproducible
timeNull := time.Unix(0, 0)
gpgKey := &openpgp.Entity{
PrimaryKey: packet.NewRSAPublicKey(timeNull, &key.PublicKey),
PrivateKey: packet.NewRSAPrivateKey(timeNull, key),
Identities: make(map[string]*openpgp.Identity),
}
uid := packet.NewUserId("root", "", "root@localhost")
isPrimaryID := true
gpgKey.Identities[uid.Id] = &openpgp.Identity{
Name: uid.Id,
UserId: uid,
SelfSignature: &packet.Signature{
CreationTime: timeNull,
SigType: packet.SigTypePositiveCert,
PubKeyAlgo: packet.PubKeyAlgoRSA,
Hash: crypto.SHA256,
IsPrimaryId: &isPrimaryID,
FlagsValid: true,
FlagSign: true,
FlagCertify: true,
FlagEncryptStorage: true,
FlagEncryptCommunications: true,
IssuerKeyId: &gpgKey.PrimaryKey.KeyId,
},
}
return gpgKey, nil
}