From 747a45729f5fc89c068729d9dcc9a3e502b51594 Mon Sep 17 00:00:00 2001 From: Moritz Johner Date: Tue, 13 Aug 2019 22:53:18 +0200 Subject: [PATCH] feat: restrict iam roles per namespace add option to restrict the range of assumed roles by specifying an regular expression on a namespace annotation Signed-off-by: Moritz Johner --- lib/poller.js | 40 +++++++++++++++++++++++++++++++++++++++- lib/poller.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/lib/poller.js b/lib/poller.js index 9f9c716f..569f6598 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -12,6 +12,8 @@ * object, this is the property name of the value to use. */ +const annotationPermittedKey = 'iam.amazonaws.com/permitted' + /** Poller class. */ class Poller { /** @@ -86,9 +88,15 @@ class Poller { async _upsertKubernetesSecret () { const secretDescriptor = this._secretDescriptor const secretName = secretDescriptor.name - const secretManifest = await this._createSecretManifest() const kubeNamespace = this._kubeClient.api.v1.namespaces(this._namespace) + // check if namespace is allowed to fetch this secret + const ns = await kubeNamespace.get() + const verdict = await this._isPermitted(ns.body, secretDescriptor) + if (!verdict.allowed) { + throw (new Error(`not allowed to fetch secret: ${secretDescriptor.name}: ${verdict.reason}`)) + } + const secretManifest = await this._createSecretManifest() this._logger.info(`upserting secret ${secretName} in ${this._namespace}`) try { return await kubeNamespace.secrets.post({ body: secretManifest }) @@ -98,6 +106,36 @@ class Poller { } } + /** + * checks if the supplied namespace is allowed to sync the given secret + * + * @param {Object} namespace namespace object + * @param {Object} descriptor secret descriptor + */ + async _isPermitted (namespace, descriptor) { + const role = descriptor.roleArn + let allowed = true + let reason = '' + + if (!namespace.metadata.annotations) { + return { + allowed, reason + } + } + // an empty annotation value allows access to all roles + const re = new RegExp(namespace.metadata.annotations[annotationPermittedKey]) + + if (!re.test(role)) { + allowed = false + reason = `namspace does not allow to assume role ${role}` + } + + return { + allowed, + reason + } + } + /** * Checks if secret exists, if not trigger a poll */ diff --git a/lib/poller.test.js b/lib/poller.test.js index 672bb98e..4ae1edb3 100644 --- a/lib/poller.test.js +++ b/lib/poller.test.js @@ -178,6 +178,7 @@ describe('Poller', () => { describe('_upsertKubernetesSecret', () => { let kubeNamespaceMock let poller + let fakeNamespace beforeEach(() => { poller = pollerFactory({ @@ -185,8 +186,16 @@ describe('Poller', () => { name: 'fakeSecretName', properties: ['fakePropertyName'] }) + fakeNamespace = { + body: { + metadata: { + annotations: {} + } + } + } kubeNamespaceMock = sinon.mock() kubeNamespaceMock.secrets = sinon.stub().returns(kubeNamespaceMock) + kubeNamespaceMock.get = sinon.stub().resolves(fakeNamespace) kubeClientMock.api = sinon.mock() kubeClientMock.api.v1 = sinon.mock() kubeClientMock.api.v1.namespaces = sinon.stub().returns(kubeNamespaceMock) @@ -228,6 +237,7 @@ describe('Poller', () => { conflictError.statusCode = 409 kubeNamespaceMock.secrets.post = sinon.stub().throws(conflictError) kubeNamespaceMock.put = sinon.stub().resolves() + kubeNamespaceMock.get = sinon.stub().resolves(fakeNamespace) await poller._upsertKubernetesSecret() @@ -248,6 +258,27 @@ describe('Poller', () => { })).to.equal(true) }) + it('does not permit update of secret', async () => { + fakeNamespace.body.metadata.annotations['iam.amazonaws.com/permitted'] = '^$' + poller = pollerFactory({ + backendType: 'fakeBackendType', + name: 'fakeSecretName', + roleArn: 'arn:aws:iam::123456789012:role/test-role', + properties: ['fakePropertyName'] + }) + kubeNamespaceMock.get = sinon.stub().resolves(fakeNamespace) + + let error + try { + await poller._upsertKubernetesSecret() + } catch (err) { + error = err + } + + expect(error).to.not.equal(undefined) + expect(error.message).equals('not allowed to fetch secret: fakeSecretName: namspace does not allow to assume role arn:aws:iam::123456789012:role/test-role') + }) + it('fails storing secret', async () => { const internalErrorServer = new Error('Internal Error Server') internalErrorServer.statusCode = 500