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

feat: add option to assume role #144

Merged
merged 7 commits into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,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 @@ -107,6 +119,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 @@ -124,6 +138,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 @@ -150,7 +202,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 @@ -177,6 +229,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' } : {}
moolen marked this conversation as resolved.
Show resolved Hide resolved

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