Repository: godaddy/kubernetes-external-secrets
Pull request #166 (2019-09-27 00:59:56)

chore: fix a bugo in the release.sh script


Patch:
diff --git a/README.md b/README.md
index e55c5ef..a376997 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,18 @@ If not running on EKS you will have to use an IAM user (in lieu of a role).
 Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env vars in the session/pod.
 You can use envVarsFromSecret in the helm chart to create these env vars from existing k8s secrets
 
+Additionally, you can specify a `roleArn` which will be assumed before retrieving the secret.
+You can limit the range of roles which can be assumed by this particular *namespace* by using annotations on the namespace resource.
+The annotation value is evaluated as a regular expression and tries to match the `roleArn`.
+
+```yaml
+kind: Namespace
+metadata:
+  name: iam-example
+  annotations:
+    iam.amazonaws.com/permitted: "arn:aws:iam::123456789012:role/.*"
+```
+
 ### Add a secret
 
 Add your secret data to your backend. For example, AWS Secrets Manager:
@@ -109,6 +121,8 @@ metadata:
   name: hello-service
 secretDescriptor:
   backendType: secretsManager
+  # optional: specify role to assume when retrieving the data
+  roleArn: arn:aws:iam::123456789012:role/test-role
   data:
     - key: hello-service/password
       name: password
@@ -126,6 +140,44 @@ secretDescriptor:
       name: password
 ```
 
+The following IAM policy allows a user or role to access parameters matching `prod-*`.
+```json
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": "ssm:GetParameter",
+      "Resource": "arn:aws:ssm:us-west-2:123456789012:parameter/prod-*"
+    }
+  ]
+}
+```
+
+The IAM policy for Secrets Manager is similar ([see docs](https://docs.aws.amazon.com/mediaconnect/latest/ug/iam-policy-examples-asm-secrets.html)):
+
+```json
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": [
+        "secretsmanager:GetResourcePolicy",
+        "secretsmanager:GetSecretValue",
+        "secretsmanager:DescribeSecret",
+        "secretsmanager:ListSecretVersionIds"
+      ],
+      "Resource": [
+        "arn:aws:secretsmanager:us-west-2:111122223333:secret:aes128-1a2b3c",
+        "arn:aws:secretsmanager:us-west-2:111122223333:secret:aes192-4D5e6F",
+        "arn:aws:secretsmanager:us-west-2:111122223333:secret:aes256-7g8H9i"
+      ]
+    }
+  ]
+}
+```
+
 Save the file and run:
 
 ```sh
@@ -152,7 +204,7 @@ data:
 
 ## Backends
 
-kubernetes-external-secrets supports only AWS Secrets Manager.
+kubernetes-external-secrets supports both AWS Secrets Manager and AWS System Manager.
 
 ### AWS Secrets Manager
 
@@ -179,6 +231,8 @@ metadata:
   name: hello-service
 secretDescriptor:
   backendType: secretsManager
+  # optional: specify role to assume when retrieving the data
+  roleArn: arn:aws:iam::123456789012:role/test-role
   data:
     - key: hello-service/credentials
       name: password
diff --git a/config/aws-config.js b/config/aws-config.js
index 6190faa..fab17ce 100644
--- a/config/aws-config.js
+++ b/config/aws-config.js
@@ -1,13 +1,36 @@
 'use strict'
 
 /* eslint-disable no-process-env */
+const AWS = require('aws-sdk')
 
 const localstack = process.env.LOCALSTACK || 0
 
 const secretsManagerConfig = localstack ? { endpoint: 'http://localhost:4584', region: 'us-west-2' } : {}
 const systemManagerConfig = localstack ? { endpoint: 'http://localhost:4583', region: 'us-west-2' } : {}
+const stsConfig = localstack ? { endpoint: 'http://localhost:4592', region: 'us-west-2' } : {}
 
 module.exports = {
-  secretsManagerConfig,
-  systemManagerConfig
+  secretsManagerFactory: (opts) => {
+    if (localstack) {
+      opts = secretsManagerConfig
+    }
+    return new AWS.SecretsManager(opts)
+  },
+  systemManagerFactory: (opts) => {
+    if (localstack) {
+      opts = systemManagerConfig
+    }
+    return new AWS.SSM(opts)
+  },
+  assumeRole: (assumeRoleOpts) => {
+    const sts = new AWS.STS(stsConfig)
+    return new Promise((resolve, reject) => {
+      sts.assumeRole(assumeRoleOpts, (err, res) => {
+        if (err) {
+          return reject(err)
+        }
+        resolve(res)
+      })
+    })
+  }
 }
diff --git a/config/index.js b/config/index.js
index 1679695..04bfab2 100644
--- a/config/index.js
+++ b/config/index.js
@@ -1,6 +1,5 @@
 'use strict'
 
-const AWS = require('aws-sdk')
 const kube = require('kubernetes-client')
 const KubeRequest = require('kubernetes-client/backends/request')
 const pino = require('pino')
@@ -29,10 +28,16 @@ const customResourceManager = new CustomResourceManager({
   logger
 })
 
-const secretsManagerClient = new AWS.SecretsManager(awsConfig.secretsManagerConfig)
-const secretsManagerBackend = new SecretsManagerBackend({ client: secretsManagerClient, logger })
-const systemManagerClient = new AWS.SSM(awsConfig.systemManagerConfig)
-const systemManagerBackend = new SystemManagerBackend({ client: systemManagerClient, logger })
+const secretsManagerBackend = new SecretsManagerBackend({
+  clientFactory: awsConfig.secretsManagerFactory,
+  assumeRole: awsConfig.assumeRole,
+  logger
+})
+const systemManagerBackend = new SystemManagerBackend({
+  clientFactory: awsConfig.systemManagerFactory,
+  assumeRole: awsConfig.assumeRole,
+  logger
+})
 const backends = {
   secretsManager: secretsManagerBackend,
   systemManager: systemManagerBackend
diff --git a/examples/secretsmanager-example.yaml b/examples/secretsmanager-example.yaml
index c6817dc..a1c1470 100644
--- a/examples/secretsmanager-example.yaml
+++ b/examples/secretsmanager-example.yaml
@@ -4,6 +4,8 @@ metadata:
   name: demo-service
 secretDescriptor:
   backendType: secretsManager
+  # optional: specify role to assume when retrieving the data
+  roleArn: arn:aws:iam::123412341234:role/let-other-account-access-secrets
   data:
     - key: demo-service/credentials
       name: password
diff --git a/examples/ssm-example.yaml b/examples/ssm-example.yaml
index 9c8cdbe..98cb19a 100644
--- a/examples/ssm-example.yaml
+++ b/examples/ssm-example.yaml
@@ -4,6 +4,8 @@ metadata:
   name: ssm-secret-key
 secretDescriptor:
   backendType: systemManager
+  # optional: specify role to assume when retrieving the data
+  roleArn: arn:aws:iam::123456789012:role/test-role
   data:
-    - key: /path/variable-name
+    - key: /foo/name1
       name: variable-name
diff --git a/lib/backends/kv-backend.js b/lib/backends/kv-backend.js
index 047405e..6992f7c 100644
--- a/lib/backends/kv-backend.js
+++ b/lib/backends/kv-backend.js
@@ -20,12 +20,13 @@ class KVBackend extends AbstractBackend {
    * @param {string} secretProperties[].name - Kubernetes Secret property name.
    * @param {string} secretProperties[].property - If the backend secret is an
    *   object, this is the property name of the value to use.
+   * @param {string} secretProperties[].roleArn - If the client should assume a role before fetching the secret
    * @returns {Promise} Promise object representing secret property values.
    */
-  _fetchSecretPropertyValues ({ externalData }) {
+  _fetchSecretPropertyValues ({ externalData, roleArn }) {
     return Promise.all(externalData.map(async secretProperty => {
-      this._logger.info(`fetching secret property ${secretProperty.name}`)
-      const value = await this._get({ secretKey: secretProperty.key })
+      this._logger.info(`fetching secret property ${secretProperty.name} with role: ${roleArn}`)
+      const value = await this._get({ secretKey: secretProperty.key, roleArn })
 
       if ('property' in secretProperty) {
         let parsedValue
@@ -66,7 +67,8 @@ class KVBackend extends AbstractBackend {
     // Use secretDescriptor.properties to be backwards compatible.
     const externalData = secretDescriptor.data || secretDescriptor.properties
     const secretPropertyValues = await this._fetchSecretPropertyValues({
-      externalData
+      externalData,
+      roleArn: secretDescriptor.roleArn
     })
     externalData.forEach((secretProperty, index) => {
       data[secretProperty.name] = (Buffer.from(secretPropertyValues[index], 'utf8')).toString('base64')
diff --git a/lib/backends/secrets-manager-backend.js b/lib/backends/secrets-manager-backend.js
index 33ff192..8f8eb43 100644
--- a/lib/backends/secrets-manager-backend.js
+++ b/lib/backends/secrets-manager-backend.js
@@ -9,9 +9,11 @@ class SecretsManagerBackend extends KVBackend {
    * @param {Object} client - Client for interacting with Secrets Manager.
    * @param {Object} logger - Logger for logging stuff.
    */
-  constructor ({ client, logger }) {
+  constructor ({ clientFactory, assumeRole, logger }) {
     super({ logger })
-    this._client = client
+    this._client = clientFactory()
+    this._clientFactory = clientFactory
+    this._assumeRole = assumeRole
   }
 
   /**
@@ -19,8 +21,21 @@ class SecretsManagerBackend extends KVBackend {
    * @param {string} secretKey - Key used to store secret property value in Secrets Manager.
    * @returns {Promise} Promise object representing secret property value.
    */
-  async _get ({ secretKey }) {
-    const data = await this._client
+  async _get ({ secretKey, roleArn }) {
+    let client = this._client
+    if (roleArn) {
+      const res = await this._assumeRole({
+        RoleArn: roleArn,
+        RoleSessionName: 'k8s-external-secrets'
+      })
+      client = this._clientFactory({
+        accessKeyId: res.Credentials.AccessKeyId,
+        secretAccessKey: res.Credentials.SecretAccessKey,
+        sessionToken: res.Credentials.SessionToken
+      })
+    }
+
+    const data = await client
       .getSecretValue({ SecretId: secretKey })
       .promise()
 
diff --git a/lib/backends/system-manager-backend.js b/lib/backends/system-manager-backend.js
index ad4e5b6..9f30a82 100644
--- a/lib/backends/system-manager-backend.js
+++ b/lib/backends/system-manager-backend.js
@@ -9,9 +9,11 @@ class SystemManagerBackend extends KVBackend {
    * @param {Object} client - Client for interacting with System Manager.
    * @param {Object} logger - Logger for logging stuff.
    */
-  constructor ({ client, logger }) {
+  constructor ({ clientFactory, assumeRole, logger }) {
     super({ logger })
-    this._client = client
+    this._client = clientFactory()
+    this._clientFactory = clientFactory
+    this._assumeRole = assumeRole
   }
 
   /**
@@ -19,8 +21,20 @@ class SystemManagerBackend extends KVBackend {
    * @param {string} secretKey - Key used to store secret property value in System Manager.
    * @returns {Promise} Promise object representing secret property value.
    */
-  async _get ({ secretKey }) {
-    const data = await this._client
+  async _get ({ secretKey, roleArn }) {
+    let client = this._client
+    if (roleArn) {
+      const res = await this._assumeRole({
+        RoleArn: roleArn,
+        RoleSessionName: 'k8s-external-secrets'
+      })
+      client = this._clientFactory({
+        accessKeyId: res.Credentials.AccessKeyId,
+        secretAccessKey: res.Credentials.SecretAccessKey,
+        sessionToken: res.Credentials.SessionToken
+      })
+    }
+    const data = await client
       .getParameter({
         Name: secretKey,
         WithDecryption: true
diff --git a/lib/poller.js b/lib/poller.js
index bfb5ebe..bcbb29a 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 {
   /**
@@ -98,9 +100,16 @@ class Poller {
    * @returns {Promise} Promise object representing operation result.
    */
   async _upsertKubernetesSecret () {
-    const secretManifest = await this._createSecretManifest()
+    const secretDescriptor = this._secretDescriptor
     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 = 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 ${this._secretDescriptor.name} in ${this._namespace}`)
     try {
       return await kubeNamespace.secrets.post({ body: secretManifest })
@@ -110,6 +119,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
+   */
+  _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/package.json b/package.json
index df2a94c..7d8ed8d 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
     "coverage": "nyc ./node_modules/mocha/bin/_mocha --recursive lib",
     "lint": "eslint --fix --ignore-pattern /coverage/ ./",
     "local": "LOCALSTACK=1 nodemon",
-    "localstack": "docker run -it -p 4583:4583 -p 4584:4584 -p 9999:8080 -e DEBUG=1 --rm localstack/localstack:0.9.4",
+    "localstack": "docker run -it -p 4583:4583 -p 4584:4584 -p 4592:4592 -p 9999:8080 -e DEBUG=1 --rm localstack/localstack:0.10.1.2",
     "release": "standard-version --tag-prefix='' && ./release.sh",
     "start": "./bin/daemon.js",
     "nodemon": "nodemon ./bin/daemon.js",
diff --git a/release.sh b/release.sh
index 6540475..ae150e1 100755
--- a/release.sh
+++ b/release.sh
@@ -27,6 +27,6 @@ echo ""
 echo "Do the following to publish:"
 echo ""
 echo "  1. inspect local changes (e.g., git status, git diff)"
-echo "  2. git add --all && git commit \"chore(release): godaddy/kubernetes-external-secrets:$TAG\""
+echo "  2. git add --all && git commit -m \"chore(release): godaddy/kubernetes-external-secrets:$TAG\""
 echo "  2. git push --follow-tags origin master && docker push godaddy/kubernetes-external-secrets:$TAG && docker push godaddy/kubernetes-external-secrets:latest"
 echo ""


Test patch:
diff --git a/lib/backends/kv-backend.test.js b/lib/backends/kv-backend.test.js
index 5b902e8..8db1ff1 100644
--- a/lib/backends/kv-backend.test.js
+++ b/lib/backends/kv-backend.test.js
@@ -80,13 +80,43 @@ describe('SecretsManagerBackend', () => {
         }]
       })
 
-      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1')).to.equal(true)
-      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2')).to.equal(true)
+      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1 with role: undefined')).to.equal(true)
+      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2 with role: undefined')).to.equal(true)
       expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey1'
+        secretKey: 'fakePropertyKey1',
+        roleArn: undefined
       })).to.equal(true)
       expect(kvBackend._get.calledWith({
-        secretKey: 'fakePropertyKey2'
+        secretKey: 'fakePropertyKey2',
+        roleArn: undefined
+      })).to.equal(true)
+      expect(secretPropertyValues).deep.equals(['fakePropertyValue1', 'fakePropertyValue2'])
+    })
+
+    it('fetches secret property values using the specified role', async () => {
+      kvBackend._get.onFirstCall().resolves('fakePropertyValue1')
+      kvBackend._get.onSecondCall().resolves('fakePropertyValue2')
+
+      const secretPropertyValues = await kvBackend._fetchSecretPropertyValues({
+        externalData: [{
+          key: 'fakePropertyKey1',
+          name: 'fakePropertyName1'
+        }, {
+          key: 'fakePropertyKey2',
+          name: 'fakePropertyName2'
+        }],
+        roleArn: 'secretDescriptiorRole'
+      })
+
+      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1 with role: secretDescriptiorRole')).to.equal(true)
+      expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2 with role: secretDescriptiorRole')).to.equal(true)
+      expect(kvBackend._get.calledWith({
+        secretKey: 'fakePropertyKey1',
+        roleArn: 'secretDescriptiorRole'
+      })).to.equal(true)
+      expect(kvBackend._get.calledWith({
+        secretKey: 'fakePropertyKey2',
+        roleArn: 'secretDescriptiorRole'
       })).to.equal(true)
       expect(secretPropertyValues).deep.equals(['fakePropertyValue1', 'fakePropertyValue2'])
     })
@@ -138,7 +168,8 @@ describe('SecretsManagerBackend', () => {
         }, {
           key: 'fakePropertyKey2',
           name: 'fakePropertyName2'
-        }]
+        }],
+        roleArn: undefined
       })).to.equal(true)
       expect(manifestData).deep.equals({
         fakePropertyName1: 'ZmFrZVByb3BlcnR5VmFsdWUx', // base 64 value
diff --git a/lib/backends/secrets-manager-backend.test.js b/lib/backends/secrets-manager-backend.test.js
index 34f2753..3d0acd1 100644
--- a/lib/backends/secrets-manager-backend.test.js
+++ b/lib/backends/secrets-manager-backend.test.js
@@ -8,13 +8,24 @@ const SecretsManagerBackend = require('./secrets-manager-backend')
 
 describe('SecretsManagerBackend', () => {
   let clientMock
+  let clientFactoryMock
+  let assumeRoleMock
   let secretsManagerBackend
+  const assumeRoleCredentials = {
+    Credentials: {
+      AccessKeyId: '1234',
+      SecretAccessKey: '3123123',
+      SessionToken: 'asdasdasdad'
+    }
+  }
 
   beforeEach(() => {
     clientMock = sinon.mock()
-
+    clientFactoryMock = sinon.fake.returns(clientMock)
+    assumeRoleMock = sinon.fake.returns(Promise.resolve(assumeRoleCredentials))
     secretsManagerBackend = new SecretsManagerBackend({
-      client: clientMock
+      clientFactory: clientFactoryMock,
+      assumeRole: assumeRoleMock
     })
   })
 
@@ -39,7 +50,37 @@ describe('SecretsManagerBackend', () => {
       expect(clientMock.getSecretValue.calledWith({
         SecretId: 'fakeSecretKey'
       })).to.equal(true)
+      expect(clientFactoryMock.getCall(0).args).deep.equals([])
+      expect(assumeRoleMock.callCount).equals(0)
       expect(secretPropertyValue).equals('fakeSecretPropertyValue')
     })
+
+    it('returns secret property value assuming a role', async () => {
+      getSecretValuePromise.promise.resolves({
+        SecretString: 'fakeAssumeRoleSecretValue'
+      })
+
+      const secretPropertyValue = await secretsManagerBackend._get({
+        secretKey: 'fakeSecretKey',
+        roleArn: 'my-role'
+      })
+
+      expect(clientFactoryMock.lastArg).deep.equals({
+        accessKeyId: assumeRoleCredentials.Credentials.AccessKeyId,
+        secretAccessKey: assumeRoleCredentials.Credentials.SecretAccessKey,
+        sessionToken: assumeRoleCredentials.Credentials.SessionToken
+      })
+      expect(clientMock.getSecretValue.calledWith({
+        SecretId: 'fakeSecretKey'
+      })).to.equal(true)
+      expect(clientFactoryMock.getCall(0).args).deep.equals([])
+      expect(clientFactoryMock.getCall(1).args).deep.equals([{
+        accessKeyId: assumeRoleCredentials.Credentials.AccessKeyId,
+        secretAccessKey: assumeRoleCredentials.Credentials.SecretAccessKey,
+        sessionToken: assumeRoleCredentials.Credentials.SessionToken
+      }])
+      expect(assumeRoleMock.callCount).equals(1)
+      expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue')
+    })
   })
 })
diff --git a/lib/backends/system-manager-backend.test.js b/lib/backends/system-manager-backend.test.js
index 4adc8a5..652d57e 100644
--- a/lib/backends/system-manager-backend.test.js
+++ b/lib/backends/system-manager-backend.test.js
@@ -8,13 +8,26 @@ const SystemManagerBackend = require('./system-manager-backend')
 
 describe('SystemManagerBackend', () => {
   let clientMock
+  let clientFactoryMock
+  let assumeRoleMock
   let systemManagerBackend
+  const assumeRoleCredentials = {
+    Credentials: {
+      AccessKeyId: '1234',
+      SecretAccessKey: '3123123',
+      SessionToken: 'asdasdasdad'
+    }
+  }
 
   beforeEach(() => {
     clientMock = sinon.mock()
+    clientFactoryMock = sinon.fake.returns(clientMock)
+    assumeRoleMock = sinon.fake.returns(Promise.resolve(assumeRoleCredentials))
 
     systemManagerBackend = new SystemManagerBackend({
-      client: clientMock
+      client: clientMock,
+      clientFactory: clientFactoryMock,
+      assumeRole: assumeRoleMock
     })
   })
 
@@ -42,7 +55,38 @@ describe('SystemManagerBackend', () => {
         Name: 'fakeSecretKey',
         WithDecryption: true
       })).to.equal(true)
+      expect(clientFactoryMock.getCall(0).args).deep.equals([])
+      expect(assumeRoleMock.callCount).equals(0)
       expect(secretPropertyValue).equals('fakeSecretPropertyValue')
     })
+
+    it('returns secret property value assuming a role', async () => {
+      getParameterPromise.promise.resolves({
+        Parameter: {
+          Value: 'fakeAssumeRoleSecretValue'
+        }
+      })
+
+      const secretPropertyValue = await systemManagerBackend._get({
+        secretKey: 'fakeSecretKey',
+        roleArn: 'my-role'
+      })
+      expect(clientFactoryMock.lastArg).deep.equals({
+        accessKeyId: assumeRoleCredentials.Credentials.AccessKeyId,
+        secretAccessKey: assumeRoleCredentials.Credentials.SecretAccessKey,
+        sessionToken: assumeRoleCredentials.Credentials.SessionToken
+      })
+      expect(clientMock.getParameter.calledWith({
+        Name: 'fakeSecretKey',
+        WithDecryption: true
+      })).to.equal(true)
+      expect(clientFactoryMock.getCall(0).args).deep.equals([])
+      expect(clientFactoryMock.getCall(1).args).deep.equals([{
+        accessKeyId: assumeRoleCredentials.Credentials.AccessKeyId,
+        secretAccessKey: assumeRoleCredentials.Credentials.SecretAccessKey,
+        sessionToken: assumeRoleCredentials.Credentials.SessionToken
+      }])
+      expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue')
+    })
   })
 })
diff --git a/lib/poller.test.js b/lib/poller.test.js
index 8c001fc..5464a0c 100644
--- a/lib/poller.test.js
+++ b/lib/poller.test.js
@@ -194,6 +194,7 @@ describe('Poller', () => {
   describe('_upsertKubernetesSecret', () => {
     let kubeNamespaceMock
     let poller
+    let fakeNamespace
 
     beforeEach(() => {
       poller = pollerFactory({
@@ -201,8 +202,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)
@@ -244,6 +253,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()
 
@@ -264,6 +274,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
@@ -329,4 +360,51 @@ describe('Poller', () => {
       expect(poller._interval).to.equal(null)
     })
   })
+  describe('assume-role permissions', () => {
+    let poller
+    beforeEach(() => {
+      poller = pollerFactory()
+    })
+
+    it('should restrict access to certain roles per namespace ', () => {
+      const testcases = [
+        {
+          // no annotations at all
+          ns: { metadata: {} },
+          descriptor: {},
+          permitted: true
+        },
+        {
+          // empty annotation
+          ns: { metadata: { annotations: { 'iam.amazonaws.com/permitted': '' } } },
+          descriptor: {},
+          permitted: true
+        },
+        {
+          // test regex
+          ns: { metadata: { annotations: { 'iam.amazonaws.com/permitted': '.*' } } },
+          descriptor: { roleArn: 'whatever' },
+          permitted: true
+        },
+        {
+          // test regex: deny access
+          ns: { metadata: { annotations: { 'iam.amazonaws.com/permitted': '^$' } } },
+          descriptor: { roleArn: 'whatever' },
+          permitted: false
+        },
+        {
+          // real world example
+          ns: { metadata: { annotations: { 'iam.amazonaws.com/permitted': 'arn:aws:iam::123456789012:role/.*' } } },
+          descriptor: { roleArn: 'arn:aws:iam::123456789012:role/somerole' },
+          permitted: true
+        }
+      ]
+
+      for (let i = 0; i < testcases.length; i++) {
+        const testcase = testcases[i]
+        const verdict = poller._isPermitted(testcase.ns, testcase.descriptor)
+        expect(verdict.allowed).to.equal(testcase.permitted)
+      }
+    })
+  })
 })