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

Commit

Permalink
feat: add option to assume role (#144)
Browse files Browse the repository at this point in the history
* feat: add option to assume role when retrieving secrets

Signed-off-by: Moritz Johner <[email protected]>

* 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 <[email protected]>

* chore: add test to verify assume-role access control

* docs: add policy for secrets manager

* docs: add assume-role limits per ns

Signed-off-by: Moritz Johner <[email protected]>

* docs: fix spelling

Signed-off-by: Moritz Johner <[email protected]>

* chore: remove stupid code
  • Loading branch information
moolen authored and keweilu committed Sep 27, 2019
1 parent ded45f3 commit f0ce6ed
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 31 deletions.
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
27 changes: 25 additions & 2 deletions config/aws-config.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
}
}
15 changes: 10 additions & 5 deletions config/index.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/secretsmanager-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion examples/ssm-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 6 additions & 4 deletions lib/backends/kv-backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
41 changes: 36 additions & 5 deletions lib/backends/kv-backend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
})
Expand Down Expand Up @@ -138,7 +168,8 @@ describe('SecretsManagerBackend', () => {
}, {
key: 'fakePropertyKey2',
name: 'fakePropertyName2'
}]
}],
roleArn: undefined
})).to.equal(true)
expect(manifestData).deep.equals({
fakePropertyName1: 'ZmFrZVByb3BlcnR5VmFsdWUx', // base 64 value
Expand Down
23 changes: 19 additions & 4 deletions lib/backends/secrets-manager-backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,33 @@ 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
}

/**
* Get secret property value from Secrets Manager.
* @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()

Expand Down
45 changes: 43 additions & 2 deletions lib/backends/secrets-manager-backend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})

Expand All @@ -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')
})
})
})
Loading

0 comments on commit f0ce6ed

Please sign in to comment.