Skip to content

Commit 6c2b8dd

Browse files
authored
Add new logic for looking up Strongbox Secrets (#112)
We now reverted to the original logic to always look up the Strongbox Secret and "fail open" (skip decrypting) when we do not find it. We are adding a safeguard, we now check `kustomize build` output for Strongbox headers in Secret data and will fail if found.
1 parent a69db60 commit 6c2b8dd

File tree

10 files changed

+191
-66
lines changed

10 files changed

+191
-66
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ resources:
4141
4242
### Strongbox envvars
4343
44-
Secret name containing Strongbox keyring/identity file MUST be `argocd-voodoobox-strongbox-keyring`.
44+
Secret name containing Strongbox keyring/identity file MUST be
45+
`argocd-voodoobox-strongbox-keyring`.
4546

4647
Key name for keyring MUST be `.strongbox_keyring`
4748

@@ -238,7 +239,6 @@ subjects:
238239
- kind: ServiceAccount
239240
name: argocd-repo-server
240241
namespace: sys-argocd
241-
242242
```
243243

244244
### Plugin Configuration
@@ -258,7 +258,6 @@ subjects:
258258
|-|-|-|
259259
| ARGOCD_APP_NAME | set by argocd | name of application |
260260
| ARGOCD_APP_NAMESPACE | set by argocd | application's destination namespace |
261-
| STRONGBOX_ENABLED | "true" | Enable Strongbox for decryption |
262261
| STRONGBOX_SECRET_NAMESPACE | | the name of a namespace where secret resource containing strongbox keyring is located, defaults to current |
263262
| GIT_SSH_CUSTOM_KEY_ENABLED | "false" | Enable Git SSH building using custom (non global) key |
264263
| GIT_SSH_SECRET_NAMESPACE | | the value should be the name of a namespace where secret resource containing ssh keys are located, defaults to current |

decrypt.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,18 @@ var (
2929
func ensureDecryption(ctx context.Context, cwd string, app applicationInfo) error {
3030
keyringData, identityData, err := secretData(ctx, app.destinationNamespace, app.keyringSecret)
3131
if err != nil {
32+
if errors.Is(err, errNotFound) {
33+
return nil
34+
}
3235
return err
3336
}
3437
if keyringData == nil && identityData == nil {
3538
return nil
3639
}
3740

38-
// create strongbox keyRing file
41+
// create Strongbox keyRing file
3942
if keyringData != nil {
40-
keyRingPath := filepath.Join(cwd, strongboxKeyRingFile)
43+
keyRingPath := filepath.Join(cwd, strongboxKeyringFilename)
4144
if err := os.WriteFile(keyRingPath, keyringData, 0644); err != nil {
4245
return err
4346
}
@@ -61,7 +64,7 @@ func ensureDecryption(ctx context.Context, cwd string, app applicationInfo) erro
6164
}
6265

6366
func secretData(ctx context.Context, destinationNamespace string, si secretInfo) ([]byte, []byte, error) {
64-
secret, err := getSecret(ctx, destinationNamespace, si)
67+
secret, err := secret(ctx, destinationNamespace, si)
6568
if err != nil {
6669
return nil, nil, err
6770
}

generate.go

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,22 @@ import (
99
"os/exec"
1010
"path/filepath"
1111
"strings"
12+
13+
"filippo.io/age/armor"
14+
"github.com/ghodss/yaml"
15+
v1 "k8s.io/api/core/v1"
1216
)
1317

14-
func ensureBuild(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile string, app applicationInfo) (string, error) {
18+
func ensureBuild(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile string, app applicationInfo) ([]byte, error) {
1519
// Even when there is no git SSH secret defined, we still override the
16-
// git ssh command (pointing the key to /dev/null) in order to avoid
17-
// using ssh keys in default system locations and to surface the error
18-
// if bases over ssh have been configured.
20+
// Git SSH command (pointing the key to /dev/null) in order to avoid
21+
// using SSH keys in default system locations and to surface the error
22+
// if bases over SSH have been configured.
1923
sshCmdEnv := `GIT_SSH_COMMAND=ssh -q -F none -o IdentitiesOnly=yes -o IdentityFile=/dev/null -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`
2024

2125
kFiles, err := findKustomizeFiles(cwd)
2226
if err != nil {
23-
return "", fmt.Errorf("unable to ge kustomize files paths err:%s", err)
27+
return nil, fmt.Errorf("unable to get Kustomize files paths err:%s", err)
2428
}
2529

2630
if len(kFiles) == 0 {
@@ -29,13 +33,13 @@ func ensureBuild(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile st
2933

3034
hasRemoteBase, err := hasSSHRemoteBaseURL(kFiles)
3135
if err != nil {
32-
return "", fmt.Errorf("unable to look for ssh protocol err:%s", err)
36+
return nil, fmt.Errorf("unable to look for SSH protocol err:%s", err)
3337
}
3438

3539
if hasRemoteBase {
3640
sshCmdEnv, err = setupGitSSH(ctx, cwd, globalKeyPath, globalKnownHostFile, app)
3741
if err != nil {
38-
return "", err
42+
return nil, err
3943
}
4044
}
4145

@@ -50,20 +54,25 @@ func ensureBuild(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile st
5054

5155
env = append(env, sshCmdEnv)
5256

53-
// setup git config if .strongbox_keyring exits
54-
if _, err = os.Stat(filepath.Join(cwd, strongboxKeyRingFile)); err == nil {
57+
// setup Git config if .strongbox_keyring or .strongbox_identity exits
58+
if fileExists(filepath.Join(cwd, strongboxKeyringFilename)) || fileExists(filepath.Join(cwd, strongboxIdentityFilename)) {
5559
// setup SB home for kustomize run
5660
env = append(env, fmt.Sprintf("STRONGBOX_HOME=%s", cwd))
5761

58-
// getup git config via `strongbox -git-config`
62+
// setup git config via `strongbox -git-config`
5963
if err := setupGitConfigForSB(ctx, cwd, env); err != nil {
60-
return "", fmt.Errorf("unable setup git config for strongbox err:%s", err)
64+
return nil, fmt.Errorf("unable setup git config for strongbox err:%s", err)
6165
}
6266
}
6367

6468
return runKustomizeBuild(ctx, cwd, env)
6569
}
6670

71+
func fileExists(filepath string) bool {
72+
_, err := os.Stat(filepath)
73+
return err == nil
74+
}
75+
6776
func findKustomizeFiles(cwd string) ([]string, error) {
6877
kFiles := []string{}
6978

@@ -95,7 +104,7 @@ func hasSSHRemoteBaseURL(kFiles []string) (bool, error) {
95104
return false, nil
96105
}
97106

98-
// setupGitConfigForSB will setup git filters to run strongbox
107+
// setupGitConfigForSB will setup git filters to run Strongbox
99108
func setupGitConfigForSB(ctx context.Context, cwd string, env []string) error {
100109
s := exec.CommandContext(ctx, "strongbox", "-git-config")
101110
s.Dir = cwd
@@ -109,10 +118,9 @@ func setupGitConfigForSB(ctx context.Context, cwd string, env []string) error {
109118
return nil
110119
}
111120

112-
// runKustomizeBuild will run `kustomize build` cmd and return generated yaml or error
113-
func runKustomizeBuild(ctx context.Context, cwd string, env []string) (string, error) {
121+
// runKustomizeBuild runs `kustomize build` and returns the generated YAML or an error.
122+
func runKustomizeBuild(ctx context.Context, cwd string, env []string) ([]byte, error) {
114123
k := exec.CommandContext(ctx, "kustomize", "build", ".")
115-
116124
k.Dir = cwd
117125
k.Env = env
118126

@@ -123,31 +131,62 @@ func runKustomizeBuild(ctx context.Context, cwd string, env []string) (string, e
123131
k.Stderr = &stderr
124132

125133
if err := k.Start(); err != nil {
126-
return "", fmt.Errorf("unable to start kustomize cmd err:%s", err)
134+
return nil, fmt.Errorf("unable to start kustomize cmd: err=%s", err)
127135
}
128136

129137
if err := k.Wait(); err != nil {
130-
return "", fmt.Errorf("error running kustomize err:%s", strings.TrimSpace(stderr.String()))
138+
return nil, fmt.Errorf("error running kustomize: err=%s", stderr.String())
139+
}
140+
141+
if err := checkSecrets(stdout.Bytes()); err != nil {
142+
return nil, err
131143
}
132144

133-
return stdout.String(), nil
145+
return stdout.Bytes(), nil
134146
}
135147

136-
func findAndReadYamlFiles(cwd string) (string, error) {
137-
var content string
148+
func findAndReadYamlFiles(cwd string) ([]byte, error) {
149+
var content []byte
138150
err := filepath.WalkDir(cwd, func(path string, info fs.DirEntry, err error) error {
139151
if filepath.Ext(path) == ".yaml" || filepath.Base(path) == ".yml" {
140152
data, err := os.ReadFile(path)
141153
if err != nil {
142154
return fmt.Errorf("unable to read file %s err:%s", path, err)
143155
}
144-
content += fmt.Sprintf("%s\n---\n", data)
156+
content = append(content, []byte(fmt.Sprintf("%s\n---\n", data))...)
145157
}
146158
return nil
147159
})
148160
if err != nil {
149-
return "", err
161+
return nil, err
150162
}
151163

152164
return content, nil
153165
}
166+
167+
func checkSecrets(yamlData []byte) error {
168+
// Split input YAML into multiple documents by "---"
169+
docs := bytes.Split(yamlData, []byte("\n---\n"))
170+
171+
for _, doc := range docs {
172+
if len(bytes.TrimSpace(doc)) == 0 {
173+
continue // Skip empty documents
174+
}
175+
176+
var secret v1.Secret
177+
if err := yaml.Unmarshal(doc, &secret); err != nil {
178+
// Unmarshaling will fail for Secret like object, like ConfigMap
179+
continue
180+
}
181+
182+
// Check if the decoded document is a Secret
183+
if secret.Kind == "Secret" {
184+
for key, val := range secret.Data {
185+
if bytes.HasPrefix(val, encryptedFilePrefix) || strings.HasPrefix(string(val), armor.Header) {
186+
return fmt.Errorf("found ciphertext in Secret: secret=%s key=%s", secret.Name, key)
187+
}
188+
}
189+
}
190+
}
191+
return nil
192+
}

generate_test.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package main
22

33
import "testing"
44

5-
func Test_hasSSHRemoteBaseURL(t *testing.T) {
5+
func TestHasSSHRemoteBaseURL(t *testing.T) {
66
type args struct {
77
cwd string
88
}
@@ -33,3 +33,89 @@ func Test_hasSSHRemoteBaseURL(t *testing.T) {
3333
})
3434
}
3535
}
36+
37+
func TestCiphertextSecrets(t *testing.T) {
38+
t.Run("Error on STRONGBOX header in Secret data", func(t *testing.T) {
39+
yamlData := `
40+
apiVersion: v1
41+
kind: ServiceAccount
42+
metadata:
43+
name: test
44+
---
45+
apiVersion: v1
46+
kind: Secret
47+
metadata:
48+
name: my-secret
49+
data:
50+
key1: |
51+
c2VjcmV0ZGF0YQ==
52+
key2: |
53+
IyBTVFJPTkdCT1ggRU5DUllQVEVEIFJFU09VUkNFIDsgU2VlIGh0dHBzOi8vZ2l0aHViLmNvbS91
54+
dy1sYWJzL3N0cm9uZ2JveApiZlY0ZWZnVjNwTVVJUmRwV0VzbDFCdnJTNUo0QXZHcnd1eWNpZ0Y4
55+
eXZtUWVGUGNMNktFZGxRbjROOEtzVDhWNHJiUm45TVlIWXFUCmtoQ1d2bEMxWjh2QXJGcVhRdkhz
56+
UGF4M2lRPT0K
57+
`
58+
err := checkSecrets([]byte(yamlData))
59+
if err == nil {
60+
t.Error("Expected error due to STRONGBOX header, but got nil")
61+
}
62+
})
63+
64+
t.Run("Success with no STRONGBOX header in Secret data", func(t *testing.T) {
65+
yamlData := `
66+
apiVersion: v1
67+
kind: ConfigMap
68+
metadata:
69+
name: test
70+
data:
71+
key: value
72+
key1: value1
73+
---
74+
apiVersion: v1
75+
kind: ServiceAccount
76+
metadata:
77+
name: test
78+
---
79+
apiVersion: v1
80+
kind: Secret
81+
metadata:
82+
name: my-secret
83+
data:
84+
key1: |
85+
c2VjcmV0ZGF0YQ==
86+
`
87+
err := checkSecrets([]byte(yamlData))
88+
if err != nil {
89+
t.Errorf("Expected success, but got error: %v", err)
90+
}
91+
})
92+
93+
t.Run("Error on AGE header in Secret data", func(t *testing.T) {
94+
yamlData := `
95+
apiVersion: v1
96+
kind: ServiceAccount
97+
metadata:
98+
name: test
99+
---
100+
apiVersion: v1
101+
kind: Secret
102+
metadata:
103+
name: my-secret
104+
data:
105+
key1: |
106+
c2VjcmV0ZGF0YQ==
107+
key2: |
108+
LS0tLS1CRUdJTiBBR0UgRU5DUllQVEVEIEZJTEUtLS0tLQpZV2RsTFdWdVkzSjVjSFJwYjI0dWIz
109+
Sm5MM1l4Q2kwK0lGZ3lOVFV4T1NCMVNHbHdXRkJMT0VwbVpHOWlUM1JTClMzQk5aREZPYm5Ndllr
110+
c3pkMVpwTUVsTldXSXpXRVEyWmtRMENqUnJPRTVuUVV3dlVrNWpZWFZTV1RaalNUVXoKUzBOdWRu
111+
RXpWWE5WVFhBeVpFcHZaMjl2V0ZwSVN6Z0tMUzB0SUROWVIweFpkM2ROVG5admF6QkRjM2RJWm1G
112+
SQpZMDQ1Ukc1WVlsWnJUMmREWVdZek1GRTVhVk5RYVRRS05DYmE3QzU1S01FWFp2MjU4bFU2WjFD
113+
M1c4UUF0WklGClJxZXFQSXZKYTljRTU0YUFDQT09Ci0tLS0tRU5EIEFHRSBFTkNSWVBURUQgRklM
114+
RS0tLS0tCg==
115+
`
116+
err := checkSecrets([]byte(yamlData))
117+
if err == nil {
118+
t.Error("Expected error due to AGE header, but got nil")
119+
}
120+
})
121+
}

git-ssh.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ var (
3535
func setupGitSSH(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile string, app applicationInfo) (string, error) {
3636
knownHostsFragment := `-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`
3737

38-
sshDir := filepath.Join(cwd, SSHDirName)
38+
sshDir := filepath.Join(cwd, ".ssh")
3939
if err := os.Mkdir(sshDir, 0700); err != nil {
4040
return "", fmt.Errorf("unable to create ssh config dir err:%s", err)
4141
}
@@ -49,7 +49,7 @@ func setupGitSSH(ctx context.Context, cwd, globalKeyPath, globalKnownHostFile st
4949

5050
// Using own SSH key
5151
if app.gitSSHSecret.name != "" {
52-
sec, err := getSecret(ctx, app.destinationNamespace, app.gitSSHSecret)
52+
sec, err := secret(ctx, app.destinationNamespace, app.gitSSHSecret)
5353
if err != nil {
5454
return "", err
5555
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ toolchain go1.22.2
66

77
require (
88
filippo.io/age v1.2.0
9+
github.com/ghodss/yaml v1.0.0
910
github.com/google/go-cmp v0.6.0
1011
github.com/hashicorp/go-hclog v1.6.3
1112
github.com/urfave/cli/v2 v2.27.5

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
1515
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
1616
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
1717
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
18+
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
19+
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
1820
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
1921
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
2022
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=

0 commit comments

Comments
 (0)