Skip to content
This repository was archived by the owner on Jul 26, 2022. It is now read-only.

Commit e138a28

Browse files
authored
feat: add general support for isBinary for all backends (#585)
* feat: add general support for isBinary for all backends - Add support for `isBinary` in `KVBackend` + tests - Remove specific implementation of `isBinary` from Azure Key Vault & GCP Secrets Manager backends - Update description for `isBinary` field in the CRD to remove Azure-specific details - Update docs * chore: add test for isBinary explicitly set to false
1 parent 1f550f3 commit e138a28

8 files changed

+130
-35
lines changed

README.md

+27-17
Original file line numberDiff line numberDiff line change
@@ -443,23 +443,6 @@ spec:
443443
name: password
444444
```
445445

446-
Due to the way Azure handles binary files, you need to explicitly let the ExternalSecret know that the secret is binary.
447-
You can do that with the `isBinary` field on the key. This is necessary for certificates and other secret binary files.
448-
449-
```yml
450-
apiVersion: kubernetes-client.io/v1
451-
kind: ExternalSecret
452-
metadata:
453-
name: hello-keyvault-service
454-
spec:
455-
backendType: azureKeyVault
456-
keyVaultName: hello-world
457-
data:
458-
- key: hello-service/credentials
459-
name: password
460-
isBinary: true
461-
```
462-
463446
### Alibaba Cloud KMS Secret Manager
464447

465448
kubernetes-external-secrets supports fetching secrets from [Alibaba Cloud KMS Secret Manager](https://www.alibabacloud.com/help/doc-detail/152001.htm)
@@ -623,6 +606,33 @@ To retrieve an individual secret's content, use the following where "mysecret" i
623606

624607
The secrets will persist even if the helm installation is removed, although they will no longer sync to Google Secret Manager.
625608

609+
## Binary Secrets
610+
Most backends do not treat binary secrets any differently than text secrets. Since you typically store a binary secret as a base64-encoded string in the backend, you need to explicitly let the ExternalSecret know that the secret is binary, otherwise it will be encoded in base64 again.
611+
You can do that with the `isBinary` field on the key. This is necessary for certificates and other secret binary files.
612+
613+
```yml
614+
apiVersion: kubernetes-client.io/v1
615+
kind: ExternalSecret
616+
metadata:
617+
name: hello-service
618+
spec:
619+
backendType: anySupportedBackend
620+
# ...
621+
data:
622+
- key: hello-service/archives/secrets_zip
623+
name: secrets.zip
624+
isBinary: true # Default: false
625+
# also works with `property`
626+
- key: hello-service/certificates
627+
name: cert.p12
628+
property: cert.p12
629+
isBinary: true
630+
```
631+
632+
AWS Secrets Manager is a notable exception to this. If you create/update a secret using [SecretBinary](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html#API_CreateSecret_RequestSyntax) parameter of the API, then AWS API will return the secret data as `SecretBinary` in the response and ExternalSecret will handle it accordingly. In that case, you do not need to use the `isBinary` field.
633+
634+
Note that `SecretBinary` parameter is not available when using the AWS Secrets Manager console. For any binary secrets (represented by a base64-encoded strings) created/updated via the AWS console, or stored in key-value pairs instead of text strings, you can just use the `isBinary` field explicitly as above.
635+
626636
## Metrics
627637

628638
kubernetes-external-secrets exposes the following metrics over a prometheus endpoint:

charts/kubernetes-external-secrets/crds/kubernetes-client.io_externalsecrets_crd.yaml

+4-3
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ spec:
8383
description: Property to extract if secret in backend is a JSON object
8484
isBinary:
8585
description: >-
86-
You must set this to true if configuring an item for a binary file stored in Azure KeyVault.
87-
Azure automatically base64 encodes binary files and setting this to true ensures External Secrets
88-
does not base64 encode the base64 encoded binary files.
86+
Whether the backend secret shall be treated as binary data
87+
represented by a base64-encoded string. You must set this to true
88+
for any base64-encoded binary data in the backend - to ensure it
89+
is not encoded in base64 again. Default is false.
8990
type: boolean
9091
required:
9192
- name

examples/hello-service-external-secret-gcp.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ spec:
1515
property: value
1616
# Version of the secret (default: 'latest')
1717
version: 1
18-
# If the secret is encoded in base64 then decodes it (default: false)
18+
# If the secret is already encoded in base64, then sends it unchanged (default: false)
1919
isBinary: false

examples/hello-service-external-secret-vault.yml

+4
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ spec:
1111
- name: password
1212
key: secret/data/hello-service/password
1313
property: password
14+
- name: cert.p12
15+
key: secret/data/hello-service/certificates
16+
property: cert.p12
17+
isBinary: true # defaults to false

lib/backends/azure-keyvault-backend.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,13 @@ class AzureKeyVaultBackend extends KVBackend {
2626
* Get secret property value from Azure Key Vault.
2727
* @param {string} key - Key used to store secret property value in Azure Key Vault.
2828
* @param {string} specOptions.keyVaultName - Name of the azure key vault
29-
* @param {string} keyOptions.isBinary - Does the secret contain a binary? Set to "true" to handle as binary. Does not work with "property"
3029
* @returns {Promise} Promise object representing secret property value.
3130
*/
3231

33-
async _get ({ key, keyOptions, specOptions: { keyVaultName } }) {
32+
async _get ({ key, specOptions: { keyVaultName } }) {
3433
const client = this._keyvaultClient({ keyVaultName })
3534
this._logger.info(`fetching secret ${key} from Azure KeyVault ${keyVaultName}`)
3635
const secret = await client.getSecret(key)
37-
// Handle binary files, since the Azure client does not
38-
if (keyOptions && keyOptions.isBinary) {
39-
return Buffer.from(secret.value, 'base64')
40-
}
4136
return secret.value
4237
}
4338
}

lib/backends/gcp-secrets-manager-backend.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,7 @@ class GCPSecretsManagerBackend extends KVBackend {
4444
const [version] = await this._client.accessSecretVersion({
4545
name: 'projects/' + projectId + '/secrets/' + key + '/versions/' + secretVersion
4646
})
47-
const secret = version.payload.data.toString('utf8')
48-
// Handle binary files - this is useful when you've stored a base64 encoded string
49-
if (keyOptions && keyOptions.isBinary) {
50-
return Buffer.from(secret, 'base64')
51-
}
52-
53-
return secret
47+
return version.payload.data.toString('utf8')
5448
}
5549
}
5650

lib/backends/kv-backend.js

+13
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class KVBackend extends AbstractBackend {
2020
* @param {string} data[].name - Kubernetes Secret property name.
2121
* @param {string} data[].property - If the backend secret is an
2222
* object, this is the property name of the value to use.
23+
* @param {string} data[].isBinary - If the backend secret shall be treated
24+
* as binary data represented by a base64-encoded string. Defaults to false.
2325
* @param {Object} specOptions - Options set on spec level.
2426
* @returns {Promise} Promise object representing secret property values.
2527
*/
@@ -28,6 +30,7 @@ class KVBackend extends AbstractBackend {
2830
const { name, property = null, key, ...keyOptions } = dataItem
2931
const plainOrObjValue = await this._get({ key, keyOptions, specOptions })
3032
const shouldParseValue = 'property' in dataItem
33+
const isBinary = 'isBinary' in dataItem && dataItem.isBinary === true
3134

3235
let value = plainOrObjValue
3336
if (shouldParseValue) {
@@ -48,6 +51,16 @@ class KVBackend extends AbstractBackend {
4851
value = parsedValue[property]
4952
}
5053

54+
if (isBinary) {
55+
// value in the backend is binary data which is already encoded in base64.
56+
if (typeof value === 'string') {
57+
// Skip this step if the value from the backend is not a string (e.g., AWS
58+
// SecretsManager will already return a `Buffer` with base64 encoding if the
59+
// secret contains `SecretBinary` instead of `SecretString`).
60+
value = Buffer.from(value, 'base64')
61+
}
62+
}
63+
5164
return { [name]: value }
5265
}))
5366
}

lib/backends/kv-backend.test.js

+79-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,42 @@ describe('kv-backend', () => {
103103
expect(secretPropertyValues).deep.equals([{ fakePropertyName1: 'fakePropertyValue1' }, { fakePropertyName2: 'fakePropertyValue2' }])
104104
})
105105

106+
it('handles binary values', async () => {
107+
kvBackend._get.onCall(0).resolves('YmluYXJ5Cg==') // base64 of binary
108+
kvBackend._get.onCall(1).resolves('stringPropertyValue1')
109+
kvBackend._get.onCall(2).resolves('{"stringPropertyKey2": "stringPropertyValue2", "binaryPropertyKey2": "YmluYXJ5Cg=="}')
110+
kvBackend._get.onCall(3).resolves('{"stringPropertyKey2": "stringPropertyValue2", "binaryPropertyKey2": "YmluYXJ5Cg=="}')
111+
112+
const secretPropertyValues = await kvBackend._fetchDataValues({
113+
data: [{
114+
key: 'binaryPropertyKey1',
115+
name: 'binaryPropertyName1',
116+
isBinary: true
117+
}, {
118+
key: 'stringPropertyKey1',
119+
name: 'stringPropertyName1'
120+
// isBinary: false
121+
}, {
122+
key: 'jsonProperties',
123+
name: 'stringPropertyName2',
124+
// isBinary: false,
125+
property: 'stringPropertyKey2'
126+
}, {
127+
key: 'jsonProperties',
128+
name: 'binaryPropertyName2',
129+
isBinary: true,
130+
property: 'binaryPropertyKey2'
131+
}]
132+
})
133+
134+
expect(secretPropertyValues).deep.equals(
135+
[{ binaryPropertyName1: Buffer.from('YmluYXJ5Cg==', 'base64') }, // base64 of binary (unchanged)
136+
{ stringPropertyName1: 'stringPropertyValue1' },
137+
{ stringPropertyName2: 'stringPropertyValue2' },
138+
{ binaryPropertyName2: Buffer.from('YmluYXJ5Cg==', 'base64') } // base64 of binary (unchanged)
139+
])
140+
})
141+
106142
it('fetches secret property values using the specified role', async () => {
107143
kvBackend._get.onFirstCall().resolves('fakePropertyValue1')
108144
kvBackend._get.onSecondCall().resolves('fakePropertyValue2')
@@ -422,8 +458,11 @@ describe('kv-backend', () => {
422458
})
423459

424460
describe('base64 encoding', () => {
425-
it('handles json objects', async () => {
461+
beforeEach(() => {
426462
kvBackend._get = sinon.stub()
463+
})
464+
465+
it('handles json objects', async () => {
427466
kvBackend._get
428467
.resolves(JSON.stringify({
429468
textProperty: 'text',
@@ -446,5 +485,44 @@ describe('kv-backend', () => {
446485
jsonProperty: 'eyJzb21lS2V5Ijp7Im15VGV4dCI6InRleHQifX0=' // base 64 value of: {"someKey":{"myText":"text"}}
447486
})
448487
})
488+
489+
it('handles different types of binary data returned by backends', async () => {
490+
kvBackend._get.onCall(0).resolves(Buffer.from('YmluYXJ5Cg==', 'base64')) // base64 of binary as a Buffer
491+
kvBackend._get.onCall(1).resolves(Buffer.from('YmluYXJ5Cg==', 'base64')) // base64 of binary as a Buffer
492+
kvBackend._get.onCall(2).resolves('YmluYXJ5Cg==') // base64 of binary as String
493+
kvBackend._get.onCall(3).resolves('test')
494+
// e.g. AWS Secrets Manager will return `SecretBinary` as a Buffer and `SecretString` as a String
495+
496+
const manifestData = await kvBackend.getSecretManifestData({
497+
spec: {
498+
data: [{
499+
key: 'binaryPropertyKey1',
500+
name: 'binaryPropertyName1',
501+
isBinary: true
502+
}, {
503+
key: 'binaryPropertyKey2',
504+
name: 'binaryPropertyName2'
505+
// isBinary: false, but will have no impact if the backend returns a Buffer
506+
}, {
507+
key: 'stringPropertyKey3',
508+
name: 'stringPropertyName3'
509+
// isBinary: false,
510+
// must be set to true to ensure base64-encoded string in the backend
511+
// is not encoded in base64 again
512+
}, {
513+
key: 'stringPropertyKey4',
514+
name: 'stringPropertyName4',
515+
isBinary: false // explicitly set false
516+
}]
517+
}
518+
})
519+
520+
expect(manifestData).deep.equals({
521+
binaryPropertyName1: 'YmluYXJ5Cg==', // base64 of binary (unchanged)
522+
binaryPropertyName2: 'YmluYXJ5Cg==', // base64 of binary (unchanged)
523+
stringPropertyName3: 'WW1sdVlYSjVDZz09', // base64 of base64 of binary
524+
stringPropertyName4: 'dGVzdA==' // base64 of test
525+
})
526+
})
449527
})
450528
})

0 commit comments

Comments
 (0)