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

Commit f0ce6ed

Browse files
moolenkeweilu
authored andcommitted
feat: add option to assume role (#144)
* 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
1 parent ded45f3 commit f0ce6ed

14 files changed

+381
-31
lines changed

README.md

+55-1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ If not running on EKS you will have to use an IAM user (in lieu of a role).
8686
Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env vars in the session/pod.
8787
You can use envVarsFromSecret in the helm chart to create these env vars from existing k8s secrets
8888

89+
Additionally, you can specify a `roleArn` which will be assumed before retrieving the secret.
90+
You can limit the range of roles which can be assumed by this particular *namespace* by using annotations on the namespace resource.
91+
The annotation value is evaluated as a regular expression and tries to match the `roleArn`.
92+
93+
```yaml
94+
kind: Namespace
95+
metadata:
96+
name: iam-example
97+
annotations:
98+
iam.amazonaws.com/permitted: "arn:aws:iam::123456789012:role/.*"
99+
```
100+
89101
### Add a secret
90102
91103
Add your secret data to your backend. For example, AWS Secrets Manager:
@@ -109,6 +121,8 @@ metadata:
109121
name: hello-service
110122
secretDescriptor:
111123
backendType: secretsManager
124+
# optional: specify role to assume when retrieving the data
125+
roleArn: arn:aws:iam::123456789012:role/test-role
112126
data:
113127
- key: hello-service/password
114128
name: password
@@ -126,6 +140,44 @@ secretDescriptor:
126140
name: password
127141
```
128142
143+
The following IAM policy allows a user or role to access parameters matching `prod-*`.
144+
```json
145+
{
146+
"Version": "2012-10-17",
147+
"Statement": [
148+
{
149+
"Effect": "Allow",
150+
"Action": "ssm:GetParameter",
151+
"Resource": "arn:aws:ssm:us-west-2:123456789012:parameter/prod-*"
152+
}
153+
]
154+
}
155+
```
156+
157+
The IAM policy for Secrets Manager is similar ([see docs](https://docs.aws.amazon.com/mediaconnect/latest/ug/iam-policy-examples-asm-secrets.html)):
158+
159+
```json
160+
{
161+
"Version": "2012-10-17",
162+
"Statement": [
163+
{
164+
"Effect": "Allow",
165+
"Action": [
166+
"secretsmanager:GetResourcePolicy",
167+
"secretsmanager:GetSecretValue",
168+
"secretsmanager:DescribeSecret",
169+
"secretsmanager:ListSecretVersionIds"
170+
],
171+
"Resource": [
172+
"arn:aws:secretsmanager:us-west-2:111122223333:secret:aes128-1a2b3c",
173+
"arn:aws:secretsmanager:us-west-2:111122223333:secret:aes192-4D5e6F",
174+
"arn:aws:secretsmanager:us-west-2:111122223333:secret:aes256-7g8H9i"
175+
]
176+
}
177+
]
178+
}
179+
```
180+
129181
Save the file and run:
130182

131183
```sh
@@ -152,7 +204,7 @@ data:
152204

153205
## Backends
154206

155-
kubernetes-external-secrets supports only AWS Secrets Manager.
207+
kubernetes-external-secrets supports both AWS Secrets Manager and AWS System Manager.
156208

157209
### AWS Secrets Manager
158210

@@ -179,6 +231,8 @@ metadata:
179231
name: hello-service
180232
secretDescriptor:
181233
backendType: secretsManager
234+
# optional: specify role to assume when retrieving the data
235+
roleArn: arn:aws:iam::123456789012:role/test-role
182236
data:
183237
- key: hello-service/credentials
184238
name: password

config/aws-config.js

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
'use strict'
22

33
/* eslint-disable no-process-env */
4+
const AWS = require('aws-sdk')
45

56
const localstack = process.env.LOCALSTACK || 0
67

78
const secretsManagerConfig = localstack ? { endpoint: 'http://localhost:4584', region: 'us-west-2' } : {}
89
const systemManagerConfig = localstack ? { endpoint: 'http://localhost:4583', region: 'us-west-2' } : {}
10+
const stsConfig = localstack ? { endpoint: 'http://localhost:4592', region: 'us-west-2' } : {}
911

1012
module.exports = {
11-
secretsManagerConfig,
12-
systemManagerConfig
13+
secretsManagerFactory: (opts) => {
14+
if (localstack) {
15+
opts = secretsManagerConfig
16+
}
17+
return new AWS.SecretsManager(opts)
18+
},
19+
systemManagerFactory: (opts) => {
20+
if (localstack) {
21+
opts = systemManagerConfig
22+
}
23+
return new AWS.SSM(opts)
24+
},
25+
assumeRole: (assumeRoleOpts) => {
26+
const sts = new AWS.STS(stsConfig)
27+
return new Promise((resolve, reject) => {
28+
sts.assumeRole(assumeRoleOpts, (err, res) => {
29+
if (err) {
30+
return reject(err)
31+
}
32+
resolve(res)
33+
})
34+
})
35+
}
1336
}

config/index.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
const AWS = require('aws-sdk')
43
const kube = require('kubernetes-client')
54
const KubeRequest = require('kubernetes-client/backends/request')
65
const pino = require('pino')
@@ -29,10 +28,16 @@ const customResourceManager = new CustomResourceManager({
2928
logger
3029
})
3130

32-
const secretsManagerClient = new AWS.SecretsManager(awsConfig.secretsManagerConfig)
33-
const secretsManagerBackend = new SecretsManagerBackend({ client: secretsManagerClient, logger })
34-
const systemManagerClient = new AWS.SSM(awsConfig.systemManagerConfig)
35-
const systemManagerBackend = new SystemManagerBackend({ client: systemManagerClient, logger })
31+
const secretsManagerBackend = new SecretsManagerBackend({
32+
clientFactory: awsConfig.secretsManagerFactory,
33+
assumeRole: awsConfig.assumeRole,
34+
logger
35+
})
36+
const systemManagerBackend = new SystemManagerBackend({
37+
clientFactory: awsConfig.systemManagerFactory,
38+
assumeRole: awsConfig.assumeRole,
39+
logger
40+
})
3641
const backends = {
3742
secretsManager: secretsManagerBackend,
3843
systemManager: systemManagerBackend

examples/secretsmanager-example.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ metadata:
44
name: demo-service
55
secretDescriptor:
66
backendType: secretsManager
7+
# optional: specify role to assume when retrieving the data
8+
roleArn: arn:aws:iam::123412341234:role/let-other-account-access-secrets
79
data:
810
- key: demo-service/credentials
911
name: password

examples/ssm-example.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ metadata:
44
name: ssm-secret-key
55
secretDescriptor:
66
backendType: systemManager
7+
# optional: specify role to assume when retrieving the data
8+
roleArn: arn:aws:iam::123456789012:role/test-role
79
data:
8-
- key: /path/variable-name
10+
- key: /foo/name1
911
name: variable-name

lib/backends/kv-backend.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ class KVBackend extends AbstractBackend {
2020
* @param {string} secretProperties[].name - Kubernetes Secret property name.
2121
* @param {string} secretProperties[].property - If the backend secret is an
2222
* object, this is the property name of the value to use.
23+
* @param {string} secretProperties[].roleArn - If the client should assume a role before fetching the secret
2324
* @returns {Promise} Promise object representing secret property values.
2425
*/
25-
_fetchSecretPropertyValues ({ externalData }) {
26+
_fetchSecretPropertyValues ({ externalData, roleArn }) {
2627
return Promise.all(externalData.map(async secretProperty => {
27-
this._logger.info(`fetching secret property ${secretProperty.name}`)
28-
const value = await this._get({ secretKey: secretProperty.key })
28+
this._logger.info(`fetching secret property ${secretProperty.name} with role: ${roleArn}`)
29+
const value = await this._get({ secretKey: secretProperty.key, roleArn })
2930

3031
if ('property' in secretProperty) {
3132
let parsedValue
@@ -66,7 +67,8 @@ class KVBackend extends AbstractBackend {
6667
// Use secretDescriptor.properties to be backwards compatible.
6768
const externalData = secretDescriptor.data || secretDescriptor.properties
6869
const secretPropertyValues = await this._fetchSecretPropertyValues({
69-
externalData
70+
externalData,
71+
roleArn: secretDescriptor.roleArn
7072
})
7173
externalData.forEach((secretProperty, index) => {
7274
data[secretProperty.name] = (Buffer.from(secretPropertyValues[index], 'utf8')).toString('base64')

lib/backends/kv-backend.test.js

+36-5
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,43 @@ describe('SecretsManagerBackend', () => {
8080
}]
8181
})
8282

83-
expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1')).to.equal(true)
84-
expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2')).to.equal(true)
83+
expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1 with role: undefined')).to.equal(true)
84+
expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2 with role: undefined')).to.equal(true)
8585
expect(kvBackend._get.calledWith({
86-
secretKey: 'fakePropertyKey1'
86+
secretKey: 'fakePropertyKey1',
87+
roleArn: undefined
8788
})).to.equal(true)
8889
expect(kvBackend._get.calledWith({
89-
secretKey: 'fakePropertyKey2'
90+
secretKey: 'fakePropertyKey2',
91+
roleArn: undefined
92+
})).to.equal(true)
93+
expect(secretPropertyValues).deep.equals(['fakePropertyValue1', 'fakePropertyValue2'])
94+
})
95+
96+
it('fetches secret property values using the specified role', async () => {
97+
kvBackend._get.onFirstCall().resolves('fakePropertyValue1')
98+
kvBackend._get.onSecondCall().resolves('fakePropertyValue2')
99+
100+
const secretPropertyValues = await kvBackend._fetchSecretPropertyValues({
101+
externalData: [{
102+
key: 'fakePropertyKey1',
103+
name: 'fakePropertyName1'
104+
}, {
105+
key: 'fakePropertyKey2',
106+
name: 'fakePropertyName2'
107+
}],
108+
roleArn: 'secretDescriptiorRole'
109+
})
110+
111+
expect(loggerMock.info.calledWith('fetching secret property fakePropertyName1 with role: secretDescriptiorRole')).to.equal(true)
112+
expect(loggerMock.info.calledWith('fetching secret property fakePropertyName2 with role: secretDescriptiorRole')).to.equal(true)
113+
expect(kvBackend._get.calledWith({
114+
secretKey: 'fakePropertyKey1',
115+
roleArn: 'secretDescriptiorRole'
116+
})).to.equal(true)
117+
expect(kvBackend._get.calledWith({
118+
secretKey: 'fakePropertyKey2',
119+
roleArn: 'secretDescriptiorRole'
90120
})).to.equal(true)
91121
expect(secretPropertyValues).deep.equals(['fakePropertyValue1', 'fakePropertyValue2'])
92122
})
@@ -138,7 +168,8 @@ describe('SecretsManagerBackend', () => {
138168
}, {
139169
key: 'fakePropertyKey2',
140170
name: 'fakePropertyName2'
141-
}]
171+
}],
172+
roleArn: undefined
142173
})).to.equal(true)
143174
expect(manifestData).deep.equals({
144175
fakePropertyName1: 'ZmFrZVByb3BlcnR5VmFsdWUx', // base 64 value

lib/backends/secrets-manager-backend.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,33 @@ class SecretsManagerBackend extends KVBackend {
99
* @param {Object} client - Client for interacting with Secrets Manager.
1010
* @param {Object} logger - Logger for logging stuff.
1111
*/
12-
constructor ({ client, logger }) {
12+
constructor ({ clientFactory, assumeRole, logger }) {
1313
super({ logger })
14-
this._client = client
14+
this._client = clientFactory()
15+
this._clientFactory = clientFactory
16+
this._assumeRole = assumeRole
1517
}
1618

1719
/**
1820
* Get secret property value from Secrets Manager.
1921
* @param {string} secretKey - Key used to store secret property value in Secrets Manager.
2022
* @returns {Promise} Promise object representing secret property value.
2123
*/
22-
async _get ({ secretKey }) {
23-
const data = await this._client
24+
async _get ({ secretKey, roleArn }) {
25+
let client = this._client
26+
if (roleArn) {
27+
const res = await this._assumeRole({
28+
RoleArn: roleArn,
29+
RoleSessionName: 'k8s-external-secrets'
30+
})
31+
client = this._clientFactory({
32+
accessKeyId: res.Credentials.AccessKeyId,
33+
secretAccessKey: res.Credentials.SecretAccessKey,
34+
sessionToken: res.Credentials.SessionToken
35+
})
36+
}
37+
38+
const data = await client
2439
.getSecretValue({ SecretId: secretKey })
2540
.promise()
2641

lib/backends/secrets-manager-backend.test.js

+43-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,24 @@ const SecretsManagerBackend = require('./secrets-manager-backend')
88

99
describe('SecretsManagerBackend', () => {
1010
let clientMock
11+
let clientFactoryMock
12+
let assumeRoleMock
1113
let secretsManagerBackend
14+
const assumeRoleCredentials = {
15+
Credentials: {
16+
AccessKeyId: '1234',
17+
SecretAccessKey: '3123123',
18+
SessionToken: 'asdasdasdad'
19+
}
20+
}
1221

1322
beforeEach(() => {
1423
clientMock = sinon.mock()
15-
24+
clientFactoryMock = sinon.fake.returns(clientMock)
25+
assumeRoleMock = sinon.fake.returns(Promise.resolve(assumeRoleCredentials))
1626
secretsManagerBackend = new SecretsManagerBackend({
17-
client: clientMock
27+
clientFactory: clientFactoryMock,
28+
assumeRole: assumeRoleMock
1829
})
1930
})
2031

@@ -39,7 +50,37 @@ describe('SecretsManagerBackend', () => {
3950
expect(clientMock.getSecretValue.calledWith({
4051
SecretId: 'fakeSecretKey'
4152
})).to.equal(true)
53+
expect(clientFactoryMock.getCall(0).args).deep.equals([])
54+
expect(assumeRoleMock.callCount).equals(0)
4255
expect(secretPropertyValue).equals('fakeSecretPropertyValue')
4356
})
57+
58+
it('returns secret property value assuming a role', async () => {
59+
getSecretValuePromise.promise.resolves({
60+
SecretString: 'fakeAssumeRoleSecretValue'
61+
})
62+
63+
const secretPropertyValue = await secretsManagerBackend._get({
64+
secretKey: 'fakeSecretKey',
65+
roleArn: 'my-role'
66+
})
67+
68+
expect(clientFactoryMock.lastArg).deep.equals({
69+
accessKeyId: assumeRoleCredentials.Credentials.AccessKeyId,
70+
secretAccessKey: assumeRoleCredentials.Credentials.SecretAccessKey,
71+
sessionToken: assumeRoleCredentials.Credentials.SessionToken
72+
})
73+
expect(clientMock.getSecretValue.calledWith({
74+
SecretId: 'fakeSecretKey'
75+
})).to.equal(true)
76+
expect(clientFactoryMock.getCall(0).args).deep.equals([])
77+
expect(clientFactoryMock.getCall(1).args).deep.equals([{
78+
accessKeyId: assumeRoleCredentials.Credentials.AccessKeyId,
79+
secretAccessKey: assumeRoleCredentials.Credentials.SecretAccessKey,
80+
sessionToken: assumeRoleCredentials.Credentials.SessionToken
81+
}])
82+
expect(assumeRoleMock.callCount).equals(1)
83+
expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue')
84+
})
4485
})
4586
})

0 commit comments

Comments
 (0)