From a7639c68d3c041b7b5ead98fb90152d7123e516f Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 29 Apr 2022 10:55:03 +0200 Subject: [PATCH 1/2] decryptor: detect DockerConfigJsonKey as JSON out This ensures the Secret field gets formatted back into JSON, instead of it being detected as binary output. Signed-off-by: Hidde Beydals --- controllers/kustomization_decryptor.go | 17 ++++- controllers/kustomization_decryptor_test.go | 83 ++++++++++++++++++++- 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index c4f0435b..9b3663ba 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -335,7 +335,7 @@ func (d *KustomizeDecryptor) DecryptResource(res *resource.Resource) (*resource. } if bytes.Contains(data, sopsFormatToMarkerBytes[formats.Yaml]) || bytes.Contains(data, sopsFormatToMarkerBytes[formats.Json]) { - outF := formats.FormatForPath(key) + outF := formatForPath(key) out, err := d.SopsDecryptWithFormat(data, formats.Yaml, outF) if err != nil { return nil, fmt.Errorf("failed to decrypt and format '%s/%s' Secret field '%s': %w", @@ -406,13 +406,13 @@ func (d *KustomizeDecryptor) decryptKustomizationEnvSources(visited map[string]s } else { filePath = key } - if err := visitRef(filePath, formats.FormatForPath(key)); err != nil { + if err := visitRef(filePath, formatForPath(key)); err != nil { return err } } for _, envFile := range gen.EnvSources { - format := formats.FormatForPath(envFile) - if formats.FormatForPath(envFile) == formats.Binary { + format := formatForPath(envFile) + if formatForPath(envFile) == formats.Binary { // Default to dotenv format = formats.Dotenv } @@ -731,3 +731,12 @@ func securePathErr(root string, err error) error { } return err } + +func formatForPath(path string) formats.Format { + switch { + case strings.HasSuffix(path, corev1.DockerConfigJsonKey): + return formats.Json + default: + return formats.FormatForPath(path) + } +} diff --git a/controllers/kustomization_decryptor_test.go b/controllers/kustomization_decryptor_test.go index dfde174f..deb29cc9 100644 --- a/controllers/kustomization_decryptor_test.go +++ b/controllers/kustomization_decryptor_test.go @@ -677,8 +677,8 @@ func TestKustomizeDecryptor_DecryptResource(t *testing.T) { "apiVersion": "v1", "kind": "Secret", "metadata": map[string]interface{}{ - "name": "secret", - "namespace": "test", + "name": name, + "namespace": namespace, }, "data": data, }) @@ -806,6 +806,59 @@ func TestKustomizeDecryptor_DecryptResource(t *testing.T) { g.Expect(got.GetDataMap()).To(HaveKeyWithValue("key.yaml", base64.StdEncoding.EncodeToString(plainData))) }) + t.Run("SOPS-encrypted Docker config Secret", func(t *testing.T) { + g := NewWithT(t) + + kus := kustomization.DeepCopy() + kus.Spec.Decryption = &kustomizev1.Decryption{ + Provider: DecryptionProviderSOPS, + } + + d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), *kus) + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(cleanup) + + ageID, err := extage.GenerateX25519Identity() + g.Expect(err).ToNot(HaveOccurred()) + d.ageIdentities = append(d.ageIdentities, ageID) + + plainData := []byte(`{ + "auths": { + "my-registry.example:5000": { + "username": "tiger", + "password": "pass1234", + "email": "tiger@acme.example", + "auth": "dGlnZXI6cGFzczEyMzQ=" + } + } +}`) + encData, err := d.sopsEncryptWithFormat(sops.Metadata{ + KeyGroups: []sops.KeyGroup{ + {&sopsage.MasterKey{Recipient: ageID.Recipient().String()}}, + }, + }, plainData, formats.Json, formats.Yaml) + g.Expect(err).ToNot(HaveOccurred()) + + secret := resourceFactory.FromMap(map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "secret", + "namespace": "test", + }, + "type": corev1.SecretTypeDockercfg, + "data": map[string]interface{}{ + corev1.DockerConfigJsonKey: base64.StdEncoding.EncodeToString(encData), + }, + }) + g.Expect(isSOPSEncryptedResource(secret)).To(BeFalse()) + + got, err := d.DecryptResource(secret) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(got).ToNot(BeNil()) + g.Expect(got.GetDataMap()).To(HaveKeyWithValue(corev1.DockerConfigJsonKey, base64.StdEncoding.EncodeToString(plainData))) + }) + t.Run("nil resource", func(t *testing.T) { g := NewWithT(t) @@ -1646,3 +1699,29 @@ func Test_secureAbsPath(t *testing.T) { }) } } + +func Test_formatForPath(t *testing.T) { + tests := []struct { + name string + path string + want formats.Format + }{ + { + name: "docker config", + path: corev1.DockerConfigJsonKey, + want: formats.Json, + }, + { + name: "fallback", + path: "foo.yaml", + want: formats.Yaml, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + g.Expect(formatForPath(tt.path)).To(Equal(tt.want)) + }) + } +} From 36df540a5dcad5c23c9f733a57f4e17b9fff380e Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 29 Apr 2022 11:26:34 +0200 Subject: [PATCH 2/2] decryptor: detect format of Secret data field This checks the base64 decoded bytes from a Secret field for any of the marker bytes, thereby allowing data to be encrypted into any format. Instead of the previous behavior which assumed it to either be YAML or JSON. Signed-off-by: Hidde Beydals --- controllers/kustomization_decryptor.go | 23 +++++++++++++---- controllers/kustomization_decryptor_test.go | 28 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index 9b3663ba..c19d948d 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -67,12 +67,16 @@ const ( // DecryptionAzureAuthFile is the name of the file containing the Azure // credentials. DecryptionAzureAuthFile = "sops.azure-kv" -) -var ( // maxEncryptedFileSize is the max allowed file size in bytes of an encrypted // file. maxEncryptedFileSize int64 = 5 << 20 + // unsupportedFormat is used to signal no sopsFormatToMarkerBytes format was + // detected by detectFormatFromMarkerBytes. + unsupportedFormat = formats.Format(-1) +) + +var ( // sopsFormatToString is the counterpart to // https://github.com/mozilla/sops/blob/v3.7.2/cmd/sops/formats/formats.go#L16 sopsFormatToString = map[formats.Format]string{ @@ -334,9 +338,9 @@ func (d *KustomizeDecryptor) DecryptResource(res *resource.Resource) (*resource. continue } - if bytes.Contains(data, sopsFormatToMarkerBytes[formats.Yaml]) || bytes.Contains(data, sopsFormatToMarkerBytes[formats.Json]) { + if inF := detectFormatFromMarkerBytes(data); inF != unsupportedFormat { outF := formatForPath(key) - out, err := d.SopsDecryptWithFormat(data, formats.Yaml, outF) + out, err := d.SopsDecryptWithFormat(data, inF, outF) if err != nil { return nil, fmt.Errorf("failed to decrypt and format '%s/%s' Secret field '%s': %w", res.GetNamespace(), res.GetName(), key, err) @@ -412,7 +416,7 @@ func (d *KustomizeDecryptor) decryptKustomizationEnvSources(visited map[string]s } for _, envFile := range gen.EnvSources { format := formatForPath(envFile) - if formatForPath(envFile) == formats.Binary { + if format == formats.Binary { // Default to dotenv format = formats.Dotenv } @@ -740,3 +744,12 @@ func formatForPath(path string) formats.Format { return formats.FormatForPath(path) } } + +func detectFormatFromMarkerBytes(b []byte) formats.Format { + for k, v := range sopsFormatToMarkerBytes { + if bytes.Contains(b, v) { + return k + } + } + return unsupportedFormat +} diff --git a/controllers/kustomization_decryptor_test.go b/controllers/kustomization_decryptor_test.go index deb29cc9..1a529c50 100644 --- a/controllers/kustomization_decryptor_test.go +++ b/controllers/kustomization_decryptor_test.go @@ -846,7 +846,7 @@ func TestKustomizeDecryptor_DecryptResource(t *testing.T) { "name": "secret", "namespace": "test", }, - "type": corev1.SecretTypeDockercfg, + "type": corev1.SecretTypeDockerConfigJson, "data": map[string]interface{}{ corev1.DockerConfigJsonKey: base64.StdEncoding.EncodeToString(encData), }, @@ -1725,3 +1725,29 @@ func Test_formatForPath(t *testing.T) { }) } } + +func Test_detectFormatFromMarkerBytes(t *testing.T) { + tests := []struct { + name string + b []byte + want formats.Format + }{ + { + name: "detects format", + b: bytes.Join([][]byte{[]byte("random other bytes"), sopsFormatToMarkerBytes[formats.Yaml], []byte("more random bytes")}, []byte(" ")), + want: formats.Yaml, + }, + { + name: "returns unsupported format", + b: []byte("no marker bytes present"), + want: unsupportedFormat, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := detectFormatFromMarkerBytes(tt.b); got != tt.want { + t.Errorf("detectFormatFromMarkerBytes() = %v, want %v", got, tt.want) + } + }) + } +}