Skip to content

Commit cf97d7e

Browse files
committed
feat: Add plain text secret support for AWS Secrets Manager
AWS Secrets Manager natively supports plain text secrets, but the plugin was forcing JSON parsing which caused plain text secrets to fail. This change allows retrieving plain text secrets using the SecretString key, while maintaining full backward compatibility with existing JSON and binary secret handling. Changes: - Modified GetSecrets to gracefully handle non-JSON secret strings - Added SecretString key for plain text secret access - Added comprehensive test coverage for plain text scenarios - Updated documentation with usage examples Fixes #710 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
1 parent b046a7d commit cf97d7e

File tree

3 files changed

+142
-5
lines changed

3 files changed

+142
-5
lines changed

docs/backends.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,25 @@ stringData:
312312
type: Opaque
313313
```
314314

315+
###### Plain Text Secrets
316+
317+
AWS Secrets Manager can store plain text (non-JSON) secrets. To retrieve the entire secret value as a raw string (whether it's plain text or JSON), use `SecretString` as the key:
318+
319+
```yaml
320+
apiVersion: v1
321+
kind: Secret
322+
metadata:
323+
name: aws-example
324+
stringData:
325+
# Plain text secret
326+
sample-secret: <path:test-plaintext-secret#SecretString>
327+
# Or JSON secret as raw string
328+
json-as-string: <path:test-json-secret#SecretString>
329+
type: Opaque
330+
```
331+
332+
**Note**: Use `#SecretString` to retrieve the raw secret value as a single string. If the secret contains JSON and you want to access individual elements, use `#keyName` (e.g., `<path:test-secret#username>`).
333+
315334
###### Versioned secrets
316335

317336
```yaml
@@ -357,9 +376,11 @@ stringData:
357376
type: Opaque
358377
```
359378

360-
###### Retrieving of binary data
379+
###### Retrieving Binary and Plain Text Data
380+
381+
AWS Secrets Manager supports three types of secret values: JSON objects, plain text strings, and binary data.
361382

362-
Since there is no way to set a key for binary type in AWS Secret Manager, set the `<key>` part to `SecretBinary` to retrieve binary data:
383+
**For binary data**, use `SecretBinary` as the key:
363384

364385
```yaml
365386
apiVersion: v1
@@ -371,6 +392,31 @@ stringData:
371392
type: Opaque
372393
```
373394

395+
**For plain text (non-JSON) strings**, use `SecretString` as the key:
396+
397+
```yaml
398+
apiVersion: v1
399+
kind: Secret
400+
metadata:
401+
name: aws-example
402+
stringData:
403+
sample-secret: <path:arn:aws:secretsmanager:<REGION>:<ACCOUNT_NUMBER>:<SECRET_ID>#SecretString>
404+
type: Opaque
405+
```
406+
407+
**For JSON secrets**, specify the individual key you want to retrieve:
408+
409+
```yaml
410+
apiVersion: v1
411+
kind: Secret
412+
metadata:
413+
name: aws-example
414+
stringData:
415+
username: <path:arn:aws:secretsmanager:<REGION>:<ACCOUNT_NUMBER>:<SECRET_ID>#username>
416+
password: <path:arn:aws:secretsmanager:<REGION>:<ACCOUNT_NUMBER>:<SECRET_ID>#password>
417+
type: Opaque
418+
```
419+
374420
**NOTE**
375421
For cross account access there is the need to configure the correct permissions between accounts, please check:
376422
https://aws.amazon.com/premiumsupport/knowledge-center/secrets-manager-share-between-accounts

pkg/backends/awssecretsmanager.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,14 @@ func (a *AWSSecretsManager) GetSecrets(path string, version string, annotations
7777
if result.SecretString != nil {
7878
err := json.Unmarshal([]byte(*result.SecretString), &dat)
7979
if err != nil {
80-
return nil, err
80+
// If JSON unmarshal fails, treat as plain text
81+
utils.VerboseToStdErr("Get plain text value for %v", path)
82+
dat = make(map[string]interface{})
83+
dat["SecretString"] = *result.SecretString
84+
return dat, nil
8185
}
86+
// Always include SecretString to allow retrieving raw JSON value
87+
dat["SecretString"] = *result.SecretString
8288
} else if result.SecretBinary != nil {
8389
utils.VerboseToStdErr("Get binary value for %v", path)
8490
dat = make(map[string]interface{})

pkg/backends/awssecretsmanager_test.go

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ func (m *mockSecretsManagerClient) GetSecretValue(ctx context.Context, input *se
2727
}
2828
case "test-binary":
2929
data.SecretBinary = []byte("binary-data")
30+
case "test-plaintext":
31+
string := "This is a plain text secret, not JSON"
32+
data.SecretString = &string
33+
case "test-empty-plaintext":
34+
string := ""
35+
data.SecretString = &string
36+
case "test-invalid-json":
37+
string := "{incomplete json"
38+
data.SecretString = &string
39+
case "test-json-as-string":
40+
string := "{\"username\":\"admin\",\"password\":\"secret123\"}"
41+
data.SecretString = &string
3042
}
3143

3244
return data, nil
@@ -42,7 +54,8 @@ func TestAWSSecretManagerGetSecrets(t *testing.T) {
4254
}
4355

4456
expected := map[string]interface{}{
45-
"test-secret": "current-value",
57+
"test-secret": "current-value",
58+
"SecretString": "{\"test-secret\":\"current-value\"}",
4659
}
4760

4861
if !reflect.DeepEqual(expected, data) {
@@ -70,7 +83,8 @@ func TestAWSSecretManagerGetSecrets(t *testing.T) {
7083
}
7184

7285
expected := map[string]interface{}{
73-
"test-secret": "previous-value",
86+
"test-secret": "previous-value",
87+
"SecretString": "{\"test-secret\":\"previous-value\"}",
7488
}
7589

7690
if !reflect.DeepEqual(expected, data) {
@@ -92,6 +106,77 @@ func TestAWSSecretManagerGetSecrets(t *testing.T) {
92106
t.Errorf("expected: %v, got: %v.", expected, data)
93107
}
94108
})
109+
110+
t.Run("Get plain text secret", func(t *testing.T) {
111+
data, err := sm.GetSecrets("test-plaintext", "", map[string]string{})
112+
if err != nil {
113+
t.Fatalf("expected 0 errors but got: %s", err)
114+
}
115+
116+
expected := map[string]interface{}{
117+
"SecretString": "This is a plain text secret, not JSON",
118+
}
119+
120+
if !reflect.DeepEqual(expected, data) {
121+
t.Errorf("expected: %v, got: %v.", expected, data)
122+
}
123+
})
124+
125+
t.Run("Get individual plain text secret", func(t *testing.T) {
126+
secret, err := sm.GetIndividualSecret("test-plaintext", "SecretString", "", map[string]string{})
127+
if err != nil {
128+
t.Fatalf("expected 0 errors but got: %s", err)
129+
}
130+
131+
expected := "This is a plain text secret, not JSON"
132+
133+
if !reflect.DeepEqual(expected, secret) {
134+
t.Errorf("expected: %s, got: %s.", expected, secret)
135+
}
136+
})
137+
138+
t.Run("Get empty plain text secret", func(t *testing.T) {
139+
data, err := sm.GetSecrets("test-empty-plaintext", "", map[string]string{})
140+
if err != nil {
141+
t.Fatalf("expected 0 errors but got: %s", err)
142+
}
143+
144+
expected := map[string]interface{}{
145+
"SecretString": "",
146+
}
147+
148+
if !reflect.DeepEqual(expected, data) {
149+
t.Errorf("expected: %v, got: %v.", expected, data)
150+
}
151+
})
152+
153+
t.Run("Get invalid JSON as plain text", func(t *testing.T) {
154+
data, err := sm.GetSecrets("test-invalid-json", "", map[string]string{})
155+
if err != nil {
156+
t.Fatalf("expected 0 errors but got: %s", err)
157+
}
158+
159+
expected := map[string]interface{}{
160+
"SecretString": "{incomplete json",
161+
}
162+
163+
if !reflect.DeepEqual(expected, data) {
164+
t.Errorf("expected: %v, got: %v.", expected, data)
165+
}
166+
})
167+
168+
t.Run("Get valid JSON secret as raw string using SecretString key", func(t *testing.T) {
169+
secret, err := sm.GetIndividualSecret("test-json-as-string", "SecretString", "", map[string]string{})
170+
if err != nil {
171+
t.Fatalf("expected 0 errors but got: %s", err)
172+
}
173+
174+
expected := "{\"username\":\"admin\",\"password\":\"secret123\"}"
175+
176+
if !reflect.DeepEqual(expected, secret) {
177+
t.Errorf("expected: %s, got: %s.", expected, secret)
178+
}
179+
})
95180
}
96181

97182
func TestAWSSecretManagerEmptyIfNoSecret(t *testing.T) {

0 commit comments

Comments
 (0)