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

Commit

Permalink
feat(multitenancy): scope KES access using ExternalSecret `spec.contr…
Browse files Browse the repository at this point in the history
…ollerId` and `INSTANCE_ID` env (#701)

* feat(Multitenancy): scope KES access using ExternalSecret config
  • Loading branch information
aabouzaid authored Apr 14, 2021
1 parent 456c9e2 commit af50ca6
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,35 @@ env:
WATCHED_NAMESPACES: "default,qa,dev"
```

### Using ExternalSecret config

ExternalSecret manifest allows scoping the access of kubernetes-external-secrets controller.
This allows to deploy multi kubernetes-external-secrets instances at the same cluster
and each instance can access a set of ExternalSecrets.

To enable this option, set the env var in the controller side:
```yaml
env:
INSTANCE_ID: "dev-team-instance"
```

And in ExternalSecret side:
```yaml
apiVersion: kubernetes-client.io/v1
kind: ExternalSecret
metadata:
name: foo
spec:
controllerId: 'dev-team-instance'
[...]
```

**Please note**

Scoping access by ExternalSecret config provides only a logical separation and it doesn't cover the security aspects.
i.e it assumes that the security side is managed by another component like Kubernetes Network policies
or Open Policy Agent.

## Deprecations

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ spec:
any existing fields. into generated secret, can be used to
set for example annotations or type on the generated secret
type: object
controllerId:
description: The ID of controller instance that manages this ExternalSecret.
This is needed in case there is more than a KES controller instances within the cluster.
type: string
backendType:
type: string
enum:
Expand Down
5 changes: 5 additions & 0 deletions config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ if (environment === 'development') {
require('dotenv').config()
}

// The name of this KES instance which used to scope access of ExternalSecrets.
// This is needed in case there is more than a KES controller instances within the cluster.
const instanceId = process.env.INSTANCE_ID || ''

const vaultEndpoint = process.env.VAULT_ADDR || 'http://127.0.0.1:8200'
// Grab the vault namespace from the environment
const vaultNamespace = process.env.VAULT_NAMESPACE || null
Expand Down Expand Up @@ -52,6 +56,7 @@ watchedNamespaces = watchedNamespaces
.filter(namespace => namespace)

module.exports = {
instanceId,
vaultEndpoint,
vaultNamespace,
vaultTokenRenewThreshold,
Expand Down
13 changes: 13 additions & 0 deletions lib/daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ class Daemon {
* @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties.
*/
constructor ({
instanceId,
externalSecretEvents,
logger,
pollerFactory
}) {
this._instanceId = instanceId
this._externalSecretEvents = externalSecretEvents
this._logger = logger
this._pollerFactory = pollerFactory
Expand Down Expand Up @@ -62,6 +64,17 @@ class Daemon {
*/
async start () {
for await (const event of this._externalSecretEvents) {
// Check if the externalSecret should be managed by this instance.
if (event.object.spec) {
const externalSecretMetadata = event.object.metadata
const externalSecretController = event.object.spec.controllerId
if ((this._instanceId || externalSecretController) && this._instanceId !== externalSecretController) {
this._logger.debug('the secret %s/%s is not managed by this instance but by %s',
externalSecretMetadata.namespace, externalSecretMetadata.name, externalSecretController)
continue
}
}

const descriptor = event.object ? this._createPollerDescriptor(event.object) : null

switch (event.type) {
Expand Down
58 changes: 58 additions & 0 deletions lib/daemon.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('Daemon', () => {
beforeEach(() => {
loggerMock = sinon.mock()
loggerMock.info = sinon.stub()
loggerMock.warn = sinon.stub()
loggerMock.debug = sinon.stub()

pollerMock = sinon.mock()
Expand Down Expand Up @@ -106,4 +107,61 @@ describe('Daemon', () => {
expect(daemon._addPoller.called).to.equal(true)
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
})

it('manage externalsecrets with unmatched controller id and instance id', async () => {
const fakeExternalSecretEvents = (async function * () {
yield {
type: 'ADDED',
object: {
metadata: {
name: 'foo',
namespace: 'foo',
uid: 'test-id'
},
spec: {
controllerId: 'instance01'
}
}
}
}())

daemon._instanceId = 'instance01'
daemon._externalSecretEvents = fakeExternalSecretEvents
daemon._addPoller = sinon.mock()
daemon._removePoller = sinon.mock()

await daemon.start()
daemon.stop()

expect(daemon._addPoller.called).to.equal(true)
expect(daemon._removePoller.calledWith('test-id')).to.equal(true)
})

it('do not manage externalsecrets with unmatched controller id and instance id', async () => {
const fakeExternalSecretEvents = (async function * () {
yield {
type: 'ADDED',
object: {
metadata: {
name: 'foo',
namespace: 'foo',
uid: 'test-id'
},
spec: {
controllerId: 'instance01'
}
}
}
}())

daemon._instanceId = 'instance02'
daemon._externalSecretEvents = fakeExternalSecretEvents
daemon._addPoller = sinon.mock()
daemon._removePoller = sinon.mock()

await daemon.start()
daemon.stop()

expect(daemon._addPoller.called).to.equal(false)
})
})

0 comments on commit af50ca6

Please sign in to comment.