Skip to content

Commit

Permalink
Merge #325
Browse files Browse the repository at this point in the history
325: Support parsing multiple private keys in --recovery-private-key r=mkmik a=mkmik

When following the backup procedure outlined in our FAQ, the users tend to get one single YAML file
that contains a `v1.List` object containing all the known sealing keypairs.
This is actually a good thing, since kubectl makes backing up multiple secrets so easy.
However, if the users pass such a file to --recovery-unseal --recovery-private-key it won't work since
that command expects single secrets (either encoded as PEM or as json/yaml encoded v1.Secret objects).

This change implements parsing v1.List of v1.Secrets (the output of `kubectl --namespace kube-system get secret -lsealedsecrets.bitnami.com/sealed-secrets-key -o yaml`)

Closes #319

Co-authored-by: Marko Mikulicic <mkm@bitnami.com>
  • Loading branch information
bors[bot] and Marko Mikulicic authored Nov 29, 2019
2 parents 6003857 + 391453d commit 138ae73
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 17 deletions.
69 changes: 52 additions & 17 deletions cmd/kubeseal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ var (
sealingScope ssv1alpha1.SealingScope
reEncrypt bool // re-encrypt command
unseal = flag.Bool("recovery-unseal", false, "Decrypt a sealed secrets file obtained from stdin, using the private key passed with --recovery-private-key. Intended to be used in disaster recovery mode.")
privKeys = flag.StringSlice("recovery-private-key", nil, "Private key filename used by the --recovery-unseal command. Multiple files accepted either via comma separated list of by repetition of the flag. Either PEM encoded private keys or a backup of a json/yaml encoded k8s sealed-secret controller secret are accepted.")
privKeys = flag.StringSlice("recovery-private-key", nil, "Private key filename used by the --recovery-unseal command. Multiple files accepted either via comma separated list or by repetition of the flag. Either PEM encoded private keys or a backup of a json/yaml encoded k8s sealed-secret controller secret (and v1.List) are accepted. ")

// VERSION set from Makefile
VERSION = buildinfo.DefaultVersion
Expand Down Expand Up @@ -460,27 +460,60 @@ func parseFromFile(s string) (string, string) {
return c[0], c[1]
}

func readPrivKey(filename string) (*rsa.PrivateKey, error) {
func readPrivKeysFromFile(filename string) ([]*rsa.PrivateKey, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}

res, err := parsePrivKey(b)
if err == nil {
return res, nil
return []*rsa.PrivateKey{res}, nil
}
// try to parse it as json/yaml encoded secret
s, err := readSecret(scheme.Codecs.UniversalDecoder(), bytes.NewBuffer(b))
if err != nil {
return nil, err

var secrets []*v1.Secret

// try to parse it as json/yaml encoded v1.List of secrets
var lst v1.List
if err = runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), b, &lst); err == nil {
for _, r := range lst.Items {
s, err := readSecret(scheme.Codecs.UniversalDecoder(), bytes.NewBuffer(r.Raw))
if err != nil {
return nil, err
}
secrets = append(secrets, s)
}
} else {
// try to parse it as json/yaml encoded secret
s, err := readSecret(scheme.Codecs.UniversalDecoder(), bytes.NewBuffer(b))
if err != nil {
return nil, err
}
secrets = append(secrets, s)
}
tlsKey, ok := s.Data["tls.key"]
if !ok {
return nil, fmt.Errorf("secret must contain a 'tls.data' key")

var keys []*rsa.PrivateKey
for _, s := range secrets {
tlsKey, ok := s.Data["tls.key"]
if !ok {
return nil, fmt.Errorf("secret must contain a 'tls.data' key")
}
pk, err := parsePrivKey(tlsKey)
if err != nil {
return nil, err
}
keys = append(keys, pk)
}

return parsePrivKey(tlsKey)
return keys, nil
}

func readPrivKey(filename string) (*rsa.PrivateKey, error) {
pks, err := readPrivKeysFromFile(filename)
if err != nil {
return nil, err
}
return pks[0], nil
}

func parsePrivKey(b []byte) (*rsa.PrivateKey, error) {
Expand All @@ -499,16 +532,18 @@ func parsePrivKey(b []byte) (*rsa.PrivateKey, error) {
func readPrivKeys(filenames []string) (map[string]*rsa.PrivateKey, error) {
res := map[string]*rsa.PrivateKey{}
for _, filename := range filenames {
pk, err := readPrivKey(filename)
if err != nil {
return nil, err
}
fingerprint, err := crypto.PublicKeyFingerprint(&pk.PublicKey)
pks, err := readPrivKeysFromFile(filename)
if err != nil {
return nil, err
}
for _, pk := range pks {
fingerprint, err := crypto.PublicKeyFingerprint(&pk.PublicKey)
if err != nil {
return nil, err
}

res[fingerprint] = pk
res[fingerprint] = pk
}
}
return res, nil
}
Expand Down
57 changes: 57 additions & 0 deletions cmd/kubeseal/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,63 @@ func TestUnseal(t *testing.T) {
}
}

func TestUnsealList(t *testing.T) {
pubKey, privKeys := newTestKeyPair(t)
pkFile, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(pkFile.Name())

// encode a v1.List containing all the privKeys into one file.
prettyEnc, err := prettyEncoder(scheme.Codecs, runtime.ContentTypeJSON, v1.SchemeGroupVersion)
if err != nil {
t.Fatal(err)
}

var secrets [][]byte
for _, key := range privKeys {
b := pem.EncodeToMemory(&pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(key)})
buf, err := runtime.Encode(prettyEnc, &v1.Secret{Data: map[string][]byte{"tls.key": b}})
if err != nil {
t.Fatal(err)
}
secrets = append(secrets, buf)
}
lst := &v1.List{}
for _, s := range secrets {
lst.Items = append(lst.Items, runtime.RawExtension{Raw: s})
}
blst, err := runtime.Encode(prettyEnc, lst)
if err != nil {
t.Fatal(err)
}
if _, err := pkFile.Write(blst); err != nil {
t.Fatal(err)
}
pkFile.Close()

const (
secretItemKey = "foo"
secretItemValue = "secret1"
)
ss := mkTestSealedSecret(t, pubKey, secretItemKey, secretItemValue)

var buf bytes.Buffer
if err := unsealSealedSecret(&buf, bytes.NewBuffer(ss), scheme.Codecs, []string{pkFile.Name()}); err != nil {
t.Fatal(err)
}

secret, err := readSecret(scheme.Codecs.UniversalDecoder(), &buf)
if err != nil {
t.Fatal(err)
}

if got, want := string(secret.Data[secretItemKey]), secretItemValue; got != want {
t.Fatalf("got: %q, want: %q", got, want)
}
}

func TestMergeInto(t *testing.T) {
pubKey, privKeys := newTestKeyPair(t)

Expand Down

0 comments on commit 138ae73

Please sign in to comment.