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

Commit c4fdea6

Browse files
muenchhausenFlydiverny
authored andcommitted
feat: allow enforcing naming conventions for key names, limiting which keys can be fetched from backends (#230)
* feat(naming-convention): enforcing naming conventions for AWS Secrets Manager entries closes #178 * chore: fix typo namspace to namespace * chore(naming-convention): early return on isPermitted also improved wording for naming permitted annotation related #178 * docs(naming-convention): initial documentation related #178
1 parent 234e907 commit c4fdea6

File tree

8 files changed

+130
-7
lines changed

8 files changed

+130
-7
lines changed

README.md

+26
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,32 @@ data:
184184
password: MTIzNA==
185185
```
186186

187+
## Enforcing naming conventions for backend keys
188+
189+
by default an `ExternalSecret` may access arbitrary keys from the backend e.g.
190+
191+
```yml
192+
data:
193+
- key: /dev/cluster1/core-namespace/hello-service/password
194+
name: password
195+
```
196+
197+
An enforced naming convention helps to keep the structure tidy and limits the access according
198+
to your naming schema.
199+
200+
Configure the schema as regular expression in the namespace using an annotation.
201+
This allows `ExternalSecrets` in `core-namespace` just to access secrets that start with
202+
`/dev/cluster1/core-namespace/`:
203+
204+
```yaml
205+
kind: Namespace
206+
metadata:
207+
name: core-namespace
208+
annotations:
209+
# annotation key is configurable
210+
externalsecrets.kubernetes-client.io/permitted-key-name: "/dev/cluster1/core-namespace/.*"
211+
```
212+
187213
## Deprecations
188214

189215
A few properties has changed name overtime, we still maintain backwards compatbility with these but they will eventually be removed, and they are not validated using the CRD validation.

bin/daemon.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const {
2323
metricsPort,
2424
pollerIntervalMilliseconds,
2525
pollingDisabled,
26-
rolePermittedAnnotation
26+
rolePermittedAnnotation,
27+
namingPermittedAnnotation
2728
} = require('../config')
2829

2930
async function main () {
@@ -49,6 +50,7 @@ async function main () {
4950
metrics,
5051
pollerIntervalMilliseconds,
5152
rolePermittedAnnotation,
53+
namingPermittedAnnotation,
5254
customResourceManifest,
5355
pollingDisabled,
5456
logger

config/environment.js

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const logLevel = process.env.LOG_LEVEL || 'info'
2424
const pollingDisabled = 'DISABLE_POLLING' in process.env
2525

2626
const rolePermittedAnnotation = process.env.ROLE_PERMITTED_ANNOTATION || 'iam.amazonaws.com/permitted'
27+
const namingPermittedAnnotation = process.env.NAMING_PERMITTED_ANNOTATION || 'externalsecrets.kubernetes-client.io/permitted-key-name'
2728

2829
const metricsPort = process.env.METRICS_PORT || 3001
2930

@@ -33,6 +34,7 @@ module.exports = {
3334
pollerIntervalMilliseconds,
3435
metricsPort,
3536
rolePermittedAnnotation,
37+
namingPermittedAnnotation,
3638
pollingDisabled,
3739
logLevel
3840
}

e2e/tests/secrets-manager.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ describe('secretsmanager', async () => {
200200
.externalsecrets(`e2e-secretmanager-permitted-tls-${uuid}`)
201201
.get()
202202
expect(result).to.not.equal(undefined)
203-
expect(result.body.status.status).to.contain('namspace does not allow to assume role let-me-be-root')
203+
expect(result.body.status.status).to.contain('namespace does not allow to assume role let-me-be-root')
204204
})
205205
})
206206
})

e2e/tests/ssm.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ describe('ssm', async () => {
122122
.externalsecrets(`e2e-ssm-permitted-${uuid}`)
123123
.get()
124124
expect(result).to.not.equal(undefined)
125-
expect(result.body.status.status).to.contain('namspace does not allow to assume role let-me-be-root')
125+
expect(result.body.status.status).to.contain('namespace does not allow to assume role let-me-be-root')
126126
})
127127
})
128128
})

lib/poller-factory.js

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class PollerFactory {
1919
metrics,
2020
pollerIntervalMilliseconds,
2121
rolePermittedAnnotation,
22+
namingPermittedAnnotation,
2223
customResourceManifest,
2324
pollingDisabled,
2425
logger
@@ -30,6 +31,7 @@ class PollerFactory {
3031
this._pollerIntervalMilliseconds = pollerIntervalMilliseconds
3132
this._customResourceManifest = customResourceManifest
3233
this._rolePermittedAnnotation = rolePermittedAnnotation
34+
this._namingPermittedAnnotation = namingPermittedAnnotation
3335
this._pollingDisabled = pollingDisabled
3436
}
3537

@@ -46,6 +48,7 @@ class PollerFactory {
4648
metrics: this._metrics,
4749
customResourceManifest: this._customResourceManifest,
4850
rolePermittedAnnotation: this._rolePermittedAnnotation,
51+
namingPermittedAnnotation: this._namingPermittedAnnotation,
4952
pollingDisabled: this._pollingDisabled,
5053
externalSecret
5154
})

lib/poller.js

+33-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Poller {
3838
metrics,
3939
customResourceManifest,
4040
rolePermittedAnnotation,
41+
namingPermittedAnnotation,
4142
pollingDisabled,
4243
externalSecret
4344
}) {
@@ -49,6 +50,7 @@ class Poller {
4950
this._metrics = metrics
5051
this._pollingDisabled = pollingDisabled
5152
this._rolePermittedAnnotation = rolePermittedAnnotation
53+
this._namingPermittedAnnotation = namingPermittedAnnotation
5254
this._customResourceManifest = customResourceManifest
5355

5456
this._externalSecret = externalSecret
@@ -187,21 +189,49 @@ class Poller {
187189
* @param {Object} descriptor secret descriptor
188190
*/
189191
_isPermitted (namespace, descriptor) {
190-
const role = descriptor.roleArn
191192
let allowed = true
192193
let reason = ''
193194

194-
if (!namespace.metadata.annotations || !role) {
195+
if (!namespace.metadata.annotations) {
195196
return {
196197
allowed, reason
197198
}
198199
}
200+
201+
// 1. testing naming convention if defined in namespace
202+
203+
const externalData = descriptor.data || descriptor.properties
204+
const namingConvention = namespace.metadata.annotations[this._namingPermittedAnnotation]
205+
206+
if (namingConvention) {
207+
externalData.forEach((secretProperty, index) => {
208+
const reNaming = new RegExp(namingConvention)
209+
if (!reNaming.test(secretProperty.key)) {
210+
allowed = false
211+
reason = `key name does not match naming convention ${namingConvention}`
212+
return {
213+
allowed, reason
214+
}
215+
}
216+
})
217+
}
218+
219+
// 2. testing assume role if configured
220+
221+
const role = descriptor.roleArn
222+
223+
if (!role) {
224+
return {
225+
allowed, reason
226+
}
227+
}
228+
199229
// an empty annotation value allows access to all roles
200230
const re = new RegExp(namespace.metadata.annotations[this._rolePermittedAnnotation])
201231

202232
if (!re.test(role)) {
203233
allowed = false
204-
reason = `namspace does not allow to assume role ${role}`
234+
reason = `namespace does not allow to assume role ${role}`
205235
}
206236

207237
return {

lib/poller.test.js

+61-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('Poller', () => {
2626
})
2727

2828
const rolePermittedAnnotation = 'iam.amazonaws.com/permitted'
29+
const namingPermittedAnnotation = 'externalsecrets.kubernetes-client.io/permitted-key-name'
2930

3031
beforeEach(() => {
3132
backendMock = sinon.mock()
@@ -93,6 +94,7 @@ describe('Poller', () => {
9394
logger: loggerMock,
9495
externalSecret: fakeExternalSecret,
9596
rolePermittedAnnotation,
97+
namingPermittedAnnotation,
9698
customResourceManifest: fakeCustomResourceManifest
9799
})
98100
}
@@ -636,7 +638,7 @@ describe('Poller', () => {
636638
}
637639

638640
expect(error).to.not.equal(undefined)
639-
expect(error.message).equals('not allowed to fetch secret: fakeNamespace/fakeSecretName: namspace does not allow to assume role arn:aws:iam::123456789012:role/test-role')
641+
expect(error.message).equals('not allowed to fetch secret: fakeNamespace/fakeSecretName: namespace does not allow to assume role arn:aws:iam::123456789012:role/test-role')
640642
})
641643

642644
it('fails storing secret', async () => {
@@ -745,6 +747,64 @@ describe('Poller', () => {
745747
}
746748
]
747749

750+
for (let i = 0; i < testcases.length; i++) {
751+
const testcase = testcases[i]
752+
const verdict = poller._isPermitted(testcase.ns, testcase.descriptor)
753+
expect(verdict.allowed).to.equal(testcase.permitted)
754+
}
755+
})
756+
})
757+
describe('nameing conventions', () => {
758+
let poller
759+
beforeEach(() => {
760+
poller = pollerFactory()
761+
})
762+
it('should restrict access as defined in namespace naming convention ', () => {
763+
const testcases = [
764+
{
765+
// no annotations at all
766+
ns: { metadata: {} },
767+
descriptor: {},
768+
permitted: true
769+
},
770+
{
771+
// empty annotation
772+
ns: { metadata: { annotations: { [namingPermittedAnnotation]: '' } } },
773+
descriptor: {},
774+
permitted: true
775+
},
776+
{
777+
// test regex
778+
ns: { metadata: { annotations: { [namingPermittedAnnotation]: '.*' } } },
779+
descriptor: {
780+
data: [
781+
{ key: 'whatever', name: 'somethingelse' }
782+
]
783+
},
784+
permitted: true
785+
},
786+
{
787+
// test regex
788+
ns: { metadata: { annotations: { [namingPermittedAnnotation]: 'dev/team-a/.*' } } },
789+
descriptor: {
790+
data: [
791+
{ key: 'dev/team-a/secret', name: 'somethingelse' }
792+
]
793+
},
794+
permitted: true
795+
},
796+
{
797+
// test regex
798+
ns: { metadata: { annotations: { [namingPermittedAnnotation]: 'dev/team-a/.*' } } },
799+
descriptor: {
800+
data: [
801+
{ key: 'dev/team-b/secret', name: 'somethingelse' }
802+
]
803+
},
804+
permitted: false
805+
}
806+
]
807+
748808
for (let i = 0; i < testcases.length; i++) {
749809
const testcase = testcases[i]
750810
const verdict = poller._isPermitted(testcase.ns, testcase.descriptor)

0 commit comments

Comments
 (0)