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

Commit af50ca6

Browse files
authored
feat(multitenancy): scope KES access using ExternalSecret spec.controllerId and INSTANCE_ID env (#701)
* feat(Multitenancy): scope KES access using ExternalSecret config
1 parent 456c9e2 commit af50ca6

File tree

5 files changed

+109
-0
lines changed

5 files changed

+109
-0
lines changed

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,35 @@ env:
403403
WATCHED_NAMESPACES: "default,qa,dev"
404404
```
405405

406+
### Using ExternalSecret config
407+
408+
ExternalSecret manifest allows scoping the access of kubernetes-external-secrets controller.
409+
This allows to deploy multi kubernetes-external-secrets instances at the same cluster
410+
and each instance can access a set of ExternalSecrets.
411+
412+
To enable this option, set the env var in the controller side:
413+
```yaml
414+
env:
415+
INSTANCE_ID: "dev-team-instance"
416+
```
417+
418+
And in ExternalSecret side:
419+
```yaml
420+
apiVersion: kubernetes-client.io/v1
421+
kind: ExternalSecret
422+
metadata:
423+
name: foo
424+
spec:
425+
controllerId: 'dev-team-instance'
426+
[...]
427+
```
428+
429+
**Please note**
430+
431+
Scoping access by ExternalSecret config provides only a logical separation and it doesn't cover the security aspects.
432+
i.e it assumes that the security side is managed by another component like Kubernetes Network policies
433+
or Open Policy Agent.
434+
406435
## Deprecations
407436

408437
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.

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

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ spec:
4242
any existing fields. into generated secret, can be used to
4343
set for example annotations or type on the generated secret
4444
type: object
45+
controllerId:
46+
description: The ID of controller instance that manages this ExternalSecret.
47+
This is needed in case there is more than a KES controller instances within the cluster.
48+
type: string
4549
backendType:
4650
type: string
4751
enum:

config/environment.js

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ if (environment === 'development') {
1616
require('dotenv').config()
1717
}
1818

19+
// The name of this KES instance which used to scope access of ExternalSecrets.
20+
// This is needed in case there is more than a KES controller instances within the cluster.
21+
const instanceId = process.env.INSTANCE_ID || ''
22+
1923
const vaultEndpoint = process.env.VAULT_ADDR || 'http://127.0.0.1:8200'
2024
// Grab the vault namespace from the environment
2125
const vaultNamespace = process.env.VAULT_NAMESPACE || null
@@ -52,6 +56,7 @@ watchedNamespaces = watchedNamespaces
5256
.filter(namespace => namespace)
5357

5458
module.exports = {
59+
instanceId,
5560
vaultEndpoint,
5661
vaultNamespace,
5762
vaultTokenRenewThreshold,

lib/daemon.js

+13
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ class Daemon {
1111
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
1212
*/
1313
constructor ({
14+
instanceId,
1415
externalSecretEvents,
1516
logger,
1617
pollerFactory
1718
}) {
19+
this._instanceId = instanceId
1820
this._externalSecretEvents = externalSecretEvents
1921
this._logger = logger
2022
this._pollerFactory = pollerFactory
@@ -62,6 +64,17 @@ class Daemon {
6264
*/
6365
async start () {
6466
for await (const event of this._externalSecretEvents) {
67+
// Check if the externalSecret should be managed by this instance.
68+
if (event.object.spec) {
69+
const externalSecretMetadata = event.object.metadata
70+
const externalSecretController = event.object.spec.controllerId
71+
if ((this._instanceId || externalSecretController) && this._instanceId !== externalSecretController) {
72+
this._logger.debug('the secret %s/%s is not managed by this instance but by %s',
73+
externalSecretMetadata.namespace, externalSecretMetadata.name, externalSecretController)
74+
continue
75+
}
76+
}
77+
6578
const descriptor = event.object ? this._createPollerDescriptor(event.object) : null
6679

6780
switch (event.type) {

lib/daemon.test.js

+58
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('Daemon', () => {
1515
beforeEach(() => {
1616
loggerMock = sinon.mock()
1717
loggerMock.info = sinon.stub()
18+
loggerMock.warn = sinon.stub()
1819
loggerMock.debug = sinon.stub()
1920

2021
pollerMock = sinon.mock()
@@ -106,4 +107,61 @@ describe('Daemon', () => {
106107
expect(daemon._addPoller.called).to.equal(true)
107108
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
108109
})
110+
111+
it('manage externalsecrets with unmatched controller id and instance id', async () => {
112+
const fakeExternalSecretEvents = (async function * () {
113+
yield {
114+
type: 'ADDED',
115+
object: {
116+
metadata: {
117+
name: 'foo',
118+
namespace: 'foo',
119+
uid: 'test-id'
120+
},
121+
spec: {
122+
controllerId: 'instance01'
123+
}
124+
}
125+
}
126+
}())
127+
128+
daemon._instanceId = 'instance01'
129+
daemon._externalSecretEvents = fakeExternalSecretEvents
130+
daemon._addPoller = sinon.mock()
131+
daemon._removePoller = sinon.mock()
132+
133+
await daemon.start()
134+
daemon.stop()
135+
136+
expect(daemon._addPoller.called).to.equal(true)
137+
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
138+
})
139+
140+
it('do not manage externalsecrets with unmatched controller id and instance id', async () => {
141+
const fakeExternalSecretEvents = (async function * () {
142+
yield {
143+
type: 'ADDED',
144+
object: {
145+
metadata: {
146+
name: 'foo',
147+
namespace: 'foo',
148+
uid: 'test-id'
149+
},
150+
spec: {
151+
controllerId: 'instance01'
152+
}
153+
}
154+
}
155+
}())
156+
157+
daemon._instanceId = 'instance02'
158+
daemon._externalSecretEvents = fakeExternalSecretEvents
159+
daemon._addPoller = sinon.mock()
160+
daemon._removePoller = sinon.mock()
161+
162+
await daemon.start()
163+
daemon.stop()
164+
165+
expect(daemon._addPoller.called).to.equal(false)
166+
})
109167
})

0 commit comments

Comments
 (0)