Skip to content

Commit 933ef1f

Browse files
authored
Merge pull request #1286 from vlasov-y/main
Added decryption of Kustomize patches and refactor SOPS tests
2 parents 29080cb + 49770ea commit 933ef1f

39 files changed

+346
-287
lines changed

docs/spec/v1/kustomizations.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ For more information, see [remote clusters/Cluster-API](#remote-clusterscluster-
725725
### Decryption
726726

727727
`.spec.decryption` is an optional field to specify the configuration to decrypt
728-
Secrets that are a part of the Kustomization.
728+
Secrets, ConfigMaps and patches that are a part of the Kustomization.
729729

730730
Since Secrets are either plain text or `base64` encoded, it's unsafe to store
731731
them in plain text in a public or private Git repository. In order to store
@@ -734,9 +734,11 @@ encrypt your Kubernetes Secret data with [age](https://age-encryption.org/v1/)
734734
and/or [OpenPGP](https://www.openpgp.org) keys, or with provider implementations
735735
like Azure Key Vault, GCP KMS or Hashicorp Vault.
736736

737-
**Note:** You should encrypt only the `data/stringData` section of the Kubernetes
738-
Secret, encrypting the `metadata`, `kind` or `apiVersion` fields is not supported.
739-
An easy way to do this is by appending `--encrypted-regex '^(data|stringData)$'`
737+
Also, you may want to encrypt some parts of resources as well. In order to do that,
738+
you may encrypt patches as well.
739+
740+
**Note:** You must leave `metadata`, `kind` or `apiVersion` in plain text.
741+
An easy way to do this is to limit encrypted keys by appending `--encrypted-regex '^(data|stringData)$'`
740742
to your `sops --encrypt` command.
741743

742744
It has two fields:

internal/controller/kustomization_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,8 @@ func (r *KustomizationReconciler) build(ctx context.Context,
599599
}
600600

601601
// Decrypt Kustomize EnvSources files before build
602-
if err = dec.DecryptEnvSources(dirPath); err != nil {
603-
return nil, fmt.Errorf("error decrypting env sources: %w", err)
602+
if err = dec.DecryptSources(dirPath); err != nil {
603+
return nil, fmt.Errorf("error decrypting sources: %w", err)
604604
}
605605

606606
m, err := generator.SecureBuild(workDir, dirPath, !r.NoRemoteBases)

internal/controller/kustomization_decryptor_test.go

Lines changed: 36 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,18 @@ func TestKustomizationReconciler_Decryptor(t *testing.T) {
4343
g.Expect(err).NotTo(HaveOccurred(), "failed to create vault client")
4444

4545
// create a master key on the vault transit engine
46-
path, data := "sops/keys/firstkey", map[string]interface{}{"type": "rsa-4096"}
46+
path, data := "sops/keys/vault", map[string]interface{}{"type": "rsa-4096"}
4747
_, err = cli.Logical().Write(path, data)
4848
g.Expect(err).NotTo(HaveOccurred(), "failed to write key")
4949

5050
// encrypt the testdata vault secret
51-
cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/firstkey", "--encrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/secret.vault.yaml")
51+
cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/vault", "--encrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/algorithms/vault.yaml")
5252
err = cmd.Run()
5353
g.Expect(err).NotTo(HaveOccurred(), "failed to encrypt file")
5454

5555
// defer the testdata vault secret decryption, to leave a clean testdata vault secret
5656
defer func() {
57-
cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/firstkey", "--decrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/secret.vault.yaml")
57+
cmd := exec.Command("sops", "--hc-vault-transit", cli.Address()+"/v1/sops/keys/firstkey", "--decrypt", "--encrypted-regex", "^(data|stringData)$", "--in-place", "./testdata/sops/algorithms/vault.yaml")
5858
err = cmd.Run()
5959
}()
6060

@@ -70,36 +70,23 @@ func TestKustomizationReconciler_Decryptor(t *testing.T) {
7070
artifactChecksum, err := testServer.ArtifactFromDir("testdata/sops", artifactName)
7171
g.Expect(err).ToNot(HaveOccurred())
7272

73-
overlayArtifactName := "sops-" + randStringRunes(5)
74-
overlayChecksum, err := testServer.ArtifactFromDir("testdata/test-dotenv", overlayArtifactName)
75-
g.Expect(err).ToNot(HaveOccurred())
76-
7773
repositoryName := types.NamespacedName{
7874
Name: fmt.Sprintf("sops-%s", randStringRunes(5)),
7975
Namespace: id,
8076
}
8177

82-
overlayRepositoryName := types.NamespacedName{
83-
Name: fmt.Sprintf("sops-%s", randStringRunes(5)),
84-
Namespace: id,
85-
}
86-
8778
err = applyGitRepository(repositoryName, artifactName, "main/"+artifactChecksum)
8879
g.Expect(err).NotTo(HaveOccurred())
8980

90-
err = applyGitRepository(overlayRepositoryName, overlayArtifactName, "main/"+overlayChecksum)
91-
g.Expect(err).NotTo(HaveOccurred())
92-
93-
pgpKey, err := os.ReadFile("testdata/sops/pgp.asc")
81+
pgpKey, err := os.ReadFile("testdata/sops/keys/pgp.asc")
9482
g.Expect(err).ToNot(HaveOccurred())
95-
ageKey, err := os.ReadFile("testdata/sops/age.txt")
83+
ageKey, err := os.ReadFile("testdata/sops/keys/age.txt")
9684
g.Expect(err).ToNot(HaveOccurred())
9785

9886
sopsSecretKey := types.NamespacedName{
9987
Name: "sops-" + randStringRunes(5),
10088
Namespace: id,
10189
}
102-
10390
sopsSecret := &corev1.Secret{
10491
ObjectMeta: metav1.ObjectMeta{
10592
Name: sopsSecretKey.Name,
@@ -153,64 +140,40 @@ func TestKustomizationReconciler_Decryptor(t *testing.T) {
153140
return obj.Status.LastAppliedRevision == "main/"+artifactChecksum
154141
}, timeout, time.Second).Should(BeTrue())
155142

156-
overlayKustomizationName := fmt.Sprintf("sops-%s", randStringRunes(5))
157-
overlayKs := kustomization.DeepCopy()
158-
overlayKs.ResourceVersion = ""
159-
overlayKs.Name = overlayKustomizationName
160-
overlayKs.Spec.SourceRef.Name = overlayRepositoryName.Name
161-
overlayKs.Spec.SourceRef.Namespace = overlayRepositoryName.Namespace
162-
overlayKs.Spec.Path = "./testdata/test-dotenv/overlays"
163-
164-
g.Expect(k8sClient.Create(context.TODO(), overlayKs)).To(Succeed())
165-
166-
g.Eventually(func() bool {
167-
var obj kustomizev1.Kustomization
168-
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(overlayKs), &obj)
169-
return obj.Status.LastAppliedRevision == "main/"+overlayChecksum
170-
}, timeout, time.Second).Should(BeTrue())
171-
172143
t.Run("decrypts SOPS secrets", func(t *testing.T) {
173144
g := NewWithT(t)
174145

175-
var pgpSecret corev1.Secret
176-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-pgp", Namespace: id}, &pgpSecret)).To(Succeed())
177-
g.Expect(pgpSecret.Data["secret"]).To(Equal([]byte(`my-sops-pgp-secret`)))
178-
179-
var ageSecret corev1.Secret
180-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-age", Namespace: id}, &ageSecret)).To(Succeed())
181-
g.Expect(ageSecret.Data["secret"]).To(Equal([]byte(`my-sops-age-secret`)))
182-
183-
var daySecret corev1.Secret
184-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-day", Namespace: id}, &daySecret)).To(Succeed())
185-
g.Expect(string(daySecret.Data["secret"])).To(Equal("day=Tuesday\n"))
186-
187-
var yearSecret corev1.Secret
188-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-year", Namespace: id}, &yearSecret)).To(Succeed())
189-
g.Expect(string(yearSecret.Data["year"])).To(Equal("2017"))
190-
191-
var unencryptedSecret corev1.Secret
192-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "unencrypted-sops-year", Namespace: id}, &unencryptedSecret)).To(Succeed())
193-
g.Expect(string(unencryptedSecret.Data["year"])).To(Equal("2021"))
194-
195-
var year1Secret corev1.Secret
196-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-year1", Namespace: id}, &year1Secret)).To(Succeed())
197-
g.Expect(string(year1Secret.Data["year"])).To(Equal("year1"))
198-
199-
var year2Secret corev1.Secret
200-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-year2", Namespace: id}, &year2Secret)).To(Succeed())
201-
g.Expect(string(year2Secret.Data["year"])).To(Equal("year2"))
202-
203-
var year3Secret corev1.Secret
204-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-year3", Namespace: id}, &year3Secret)).To(Succeed())
205-
g.Expect(string(year3Secret.Data["year"])).To(Equal("year3"))
206-
207-
var encodedSecret corev1.Secret
208-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-month", Namespace: id}, &encodedSecret)).To(Succeed())
209-
g.Expect(string(encodedSecret.Data["month.yaml"])).To(Equal("month: May\n"))
210-
211-
var hcvaultSecret corev1.Secret
212-
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-hcvault", Namespace: id}, &hcvaultSecret)).To(Succeed())
213-
g.Expect(string(hcvaultSecret.Data["secret"])).To(Equal("my-sops-vault-secret\n"))
146+
secretNames := []string{
147+
"sops-algo-age",
148+
"sops-algo-pgp",
149+
"sops-algo-vault",
150+
"sops-component",
151+
"sops-envs-secret",
152+
"sops-files-secret",
153+
"sops-inside-secret",
154+
"sops-remote-secret",
155+
}
156+
for _, name := range secretNames {
157+
var secret corev1.Secret
158+
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: id}, &secret)).To(Succeed())
159+
g.Expect(string(secret.Data["key"])).To(Equal("value"), fmt.Sprintf("failed on secret %s", name))
160+
}
161+
162+
configMapNames := []string{
163+
"sops-envs-configmap",
164+
"sops-files-configmap",
165+
"sops-remote-configmap",
166+
}
167+
for _, name := range configMapNames {
168+
var configMap corev1.ConfigMap
169+
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: id}, &configMap)).To(Succeed())
170+
g.Expect(string(configMap.Data["key"])).To(Equal("value"), fmt.Sprintf("failed on configmap %s", name))
171+
}
172+
173+
var patchedSecret corev1.Secret
174+
g.Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: "sops-patches-secret", Namespace: id}, &patchedSecret)).To(Succeed())
175+
g.Expect(string(patchedSecret.Data["key"])).To(Equal("merge1"))
176+
g.Expect(string(patchedSecret.Data["merge2"])).To(Equal("merge2"))
214177
})
215178

216179
t.Run("does not emit change events for identical secrets", func(t *testing.T) {

internal/controller/kustomization_fuzzer_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ const vaultVersion = "1.13.2"
8080
const defaultBinVersion = "1.24"
8181

8282
//go:embed testdata/crd/*.yaml
83-
//go:embed testdata/sops/pgp.asc
84-
//go:embed testdata/sops/age.txt
83+
//go:embed testdata/sops/keys/pgp.asc
84+
//go:embed testdata/sops/keys/age.txt
8585
var testFiles embed.FS
8686

8787
// FuzzControllers implements a fuzzer that targets the Kustomize controller.
@@ -182,11 +182,11 @@ func Fuzz_Controllers(f *testing.F) {
182182
if err != nil {
183183
return err
184184
}
185-
pgpKey, err := testFiles.ReadFile("testdata/sops/pgp.asc")
185+
pgpKey, err := testFiles.ReadFile("testdata/sops/keys/pgp.asc")
186186
if err != nil {
187187
return err
188188
}
189-
ageKey, err := testFiles.ReadFile("testdata/sops/age.txt")
189+
ageKey, err := testFiles.ReadFile("testdata/sops/keys/age.txt")
190190
if err != nil {
191191
return err
192192
}
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1+
stores:
2+
json:
3+
indent: 2
4+
yaml:
5+
indent: 2
6+
17
# creation rules are evaluated sequentially, the first match wins
28
creation_rules:
3-
# files using age
4-
- path_regex: \.age.yaml$
5-
encrypted_regex: ^(data|stringData)$
6-
age: age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29
7-
- path_regex: month.yaml$
8-
pgp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1
9-
# fallback to PGP
10-
- encrypted_regex: ^(data|stringData)$
11-
pgp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1
9+
# Testing PGP
10+
- path_regex: (inside|pgp)\.yaml$
11+
encrypted_regex: &encrypted_regex ^(data|stringData)$
12+
pgp: &pgp 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1
13+
14+
- path_regex: json\.yaml$
15+
encrypted_regex: ".*"
16+
age: &age age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29
17+
18+
- path_regex: \.yaml$
19+
encrypted_regex: *encrypted_regex
20+
age: *age
21+
22+
- path_regex: \.(env|txt)$
23+
age: *age
24+
25+
# Fallback
26+
- key_groups:
27+
- age:
28+
- *age
29+
- pgp:
30+
- *pgp
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: age
5+
stringData:
6+
key: ENC[AES256_GCM,data:mHeXsmQ=,iv:vUMpILz3xchORqkzDFvgwENY7EqIHHGJdEF6C8xqbFE=,tag:IroV7hykADvD0IUaq6kikA==,type:str]
7+
sops:
8+
kms: []
9+
gcp_kms: []
10+
azure_kv: []
11+
hc_vault: []
12+
age:
13+
- recipient: age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29
14+
enc: |
15+
-----BEGIN AGE ENCRYPTED FILE-----
16+
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZeHVSdjJoY3ZSQjJzbk1q
17+
ZXFxMWJ5amkrN1VXeHI4QzQ5OHcwVGxDem1zCm8wQVEzNEUrOUhtRUFkVnFUY0tN
18+
aFgwaHNrWmVWY1RGWXI2YlpYbUhYMGMKLS0tIDBFSXo3cjRCMngvTXpldzhMRlVp
19+
TXk2d2ExSVZYNDVTV0xwVlZnQnpScG8KVpjffjtRTA7Z4Wf/l1VMLjcl16hOrRUv
20+
LKiZDcq+nqKDUI7owZ+xNs2w5SrQjEWVhDXRSeSSRiJrK/bCYKzRxA==
21+
-----END AGE ENCRYPTED FILE-----
22+
lastmodified: "2024-11-12T13:33:42Z"
23+
mac: ENC[AES256_GCM,data:vmrF+VgW3o8z4h/DOStCUNudz68yHEC8Mws+LPoKpM3Xc7GM0Z1CfX0TKwdLLjMuvyWa2Nx2NIxm0+MCbmR8+y2izn0hHPSWhNVCWSK+iW48M05vXhDCV0xNkqM7g0kLhQ3PiSrB69loQj8C590HIfEViEtyDCFUeynDgcC289Q=,iv:u5lhmtXMxyt+3Pw09wWvgBhmKLoOSpKNWUpu/LuCr3Y=,tag:Dg0HFdLgQltzPgnEmltAzQ==,type:str]
24+
pgp: []
25+
encrypted_regex: ^(data|stringData)$
26+
version: 3.9.0
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
namePrefix: algo-
4+
resources:
5+
- age.yaml
6+
- pgp.yaml
7+
- vault.yaml
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: pgp
5+
stringData:
6+
key: ENC[AES256_GCM,data:EJey73Q=,iv:QRdpZJ6WYi3fWpKwjl8ZiV+Wwq9qtYTpcMQ0j0OEa44=,tag:d1WlcRpwEJg1lk3X3ILDmA==,type:str]
7+
sops:
8+
kms: []
9+
gcp_kms: []
10+
azure_kv: []
11+
hc_vault: []
12+
age: []
13+
lastmodified: "2024-11-12T13:33:42Z"
14+
mac: ENC[AES256_GCM,data:25ERLClNe3o33jEo109QtmVH/qzl+e0pMRR1RDyQ4QHrVqYfMIvgUeYDHAIJ5WDwQaueON8nne1KIo+fcPYVBdHvTYvnZiicCUPA5/fpgbyts0u5CdUs31bltI/blnUlU8VbJfIk2Zjlj93erLw23sdzdo/0xsdDTrf3bYiS2CI=,iv:vxrgdyqIKRWGBA+dgrGbjGn7tkXEqbADayIxuzNwxp0=,tag:qWesJqClsLpZHY9UR7ptLQ==,type:str]
15+
pgp:
16+
- created_at: "2024-11-12T13:33:42Z"
17+
enc: |-
18+
-----BEGIN PGP MESSAGE-----
19+
20+
hQIMA90SOJihaAjLARAAqSf7bnqHB0/gfh8CmweYr5cfUpH8aYg7B5QhsnD6nOok
21+
x0UIPtaxtfEBvuDsM9M678Gj/hTEzMv0FmDYRt88NAXm1+63HHnz0/0O3xXQ/DR6
22+
+1uEZruuyC23nyzjc1fefaqgZ1YJAnj5WCvcWaF12bXbIdFQpRhpVcoMMqWhQizF
23+
5QJFXjU3cnzIVtvcpMDD63NTpk8+hSTYJr5ZFODSMbQr+EPHvKPMrIx3LLcihkkS
24+
eyxvfLalj556f/3QVgGuOX6VX8lPIaUyIcmXyUkGsooEirOyhiZg2sk/QB6TYIa6
25+
Nm62hmeeXP01wyY6tax7l3LpAuda6CJRVg+Je1OkIjiuPMIBzHgtfhGFks8vgeTP
26+
xsHXKLKXlJAQyS4ewOItm9n9jc9Xdnwfli4HrGbHNzq7lgEyAOyZZtOifl4KqFbM
27+
0c3kGiP3ezycRrQGudvbdIZqGfeD+gKrBv6cV49Wgt7Nb1WJUKLcPv4PNtSlYzSu
28+
lGDM63bO+QBAKObc6MOvLnVXbFXrErLMqrexN9XFdjvvsmQAVr2z5phZk5fEk7kw
29+
j8CqyTuy2Dm+ChJwNEeqIY3BNHkvvWMLx8Cr7ZY6bO1BvOdp01mBf+XD/apeBBUe
30+
v2DT36mCehKZh5BHDYH7hKCNw+4PN2hzZd02zKMNzmARqLzQeseaTXti3Hyze23S
31+
XAG1ddNzKXsgbTwLog5EN7DTIQKR+uCIgHuK0DclyWvTiUK7P6HGepTE7byJnnpl
32+
jHtAVs8t+cYHBtY+gKFsstRGbJgAe8QfIt12/XMu9jcA/r8m7xdyNS5P9VZj
33+
=gXAv
34+
-----END PGP MESSAGE-----
35+
fp: 35C1A64CD7FC0AB6EB66756B2445463C3234ECE1
36+
encrypted_regex: ^(data|stringData)$
37+
version: 3.9.0
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: vault
5+
stringData:
6+
key: value
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
key=ENC[AES256_GCM,data:HfbmmMU=,iv:nWWqqIzzutZJBzu5PbaTPBsqvszaz2/+58mYOK7hj9Q=,tag:b+VcateAccwdb7x2dmYDrQ==,type:str]
2+
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsc0Vyd25KTE1sYWM1akFH\nTUFBeHBmSmdGMnY3ZFJvazRZMUtPMFpscmhBCnVsL2Y0cUd1Nkx1Z0Q1OWpHOG0w\nNnhXSmxjbzR5NVE1NGpjR3d2SHN6SzgKLS0tIG5tdXpXK0U2SUlsQlcvY0ZvRWJB\nS2N6MS9QRVR4K2toMEg1eDR3a3ZtdzAKiliurqchsdfT4XbttES0ohnuTMNKlZy9\nefqbQO2lTLw8wUsNUunTpJBEAx9MFZ+LFHE/EZfHZqYlzxCPzfhufA==\n-----END AGE ENCRYPTED FILE-----\n
3+
sops_age__list_0__map_recipient=age1l44xcng8dqj32nlv6d930qvvrny05hglzcv9qpc7kxjc6902ma4qufys29
4+
sops_lastmodified=2024-11-12T13:33:42Z
5+
sops_mac=ENC[AES256_GCM,data:kPn8FhXF7UcPbkA7gjfjfYljawfT67SQBsYbnaAgtcFAtMWTryTHSDAASp2RZiClZiWnKgOgT8NeFUC+hUvjlz/Vj3pQxl6zY+3CmlrbBiqYUwd8ksXjps8UTqcioWKc7xULLqV5GMUHpoWnDWkkt0F6F10uCL78P0JoKmIeCXM=,iv:/G3GIGXriXuoS9OhfEazEYgVBbo+XvouTGYEi5XVYqQ=,tag:80P9IXhwJzoqJ43eK2W+4g==,type:str]
6+
sops_unencrypted_suffix=_unencrypted
7+
sops_version=3.9.0

0 commit comments

Comments
 (0)