node-vault-client 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +200 -200
- package/README.md +264 -154
- package/package.json +44 -44
- package/src/Lease.js +70 -57
- package/src/MountResolver.js +167 -0
- package/src/VaultApiClient.js +55 -55
- package/src/VaultClient.js +366 -263
- package/src/VaultNodeConfig.js +105 -105
- package/src/auth/AuthToken.js +94 -94
- package/src/auth/VaultAppRoleAuth.js +47 -40
- package/src/auth/VaultBaseAuth.js +140 -140
- package/src/auth/VaultIAMAuth.js +187 -187
- package/src/auth/VaultKubernetesAuth.js +48 -48
- package/src/auth/VaultTokenAuth.js +34 -34
- package/src/errors.js +21 -21
- package/src/kvTransform.js +90 -0
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const lt = require('long-timeout');
|
|
4
|
-
const prettyMs = require('pretty-ms');
|
|
5
|
-
const AuthToken = require('./AuthToken');
|
|
6
|
-
const errors = require('../errors');
|
|
7
|
-
|
|
8
|
-
class VaultBaseAuth {
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {VaultApiClient} apiClient
|
|
12
|
-
* @param {Object} logger
|
|
13
|
-
* @param {String} mount - Vault's mount point
|
|
14
|
-
*/
|
|
15
|
-
constructor(apiClient, logger, mount) {
|
|
16
|
-
this.__apiClient = apiClient;
|
|
17
|
-
/** @protected */
|
|
18
|
-
this._log = logger;
|
|
19
|
-
this._mount = mount;
|
|
20
|
-
|
|
21
|
-
/** @type AuthToken */
|
|
22
|
-
this.__authToken = null;
|
|
23
|
-
this.__refreshTimeout = null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @protected
|
|
28
|
-
* @returns {Promise<AuthToken>}
|
|
29
|
-
*/
|
|
30
|
-
_authenticate() {
|
|
31
|
-
throw new Error('Method should be overridden');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
getAuthToken() {
|
|
35
|
-
if (this.__authToken === null || (this.__authToken instanceof AuthToken && this.__authToken.isExpired() && this._reauthenticationAllowed())) {
|
|
36
|
-
if (this.__authToken !== null && this.__authToken.isExpired() && !this._reauthenticationAllowed()) {
|
|
37
|
-
throw new errors.AuthTokenExpiredError('Auth token has expired & cannot be refreshed since auth method doesn\'t support this.');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
this._log.info('getting auth token (mount=%s)', this._mount);
|
|
41
|
-
|
|
42
|
-
const tokenPromise = this._authenticate().then(authToken => {
|
|
43
|
-
this.__authToken = authToken;
|
|
44
|
-
|
|
45
|
-
if (this.__authToken.isRenewable()) {
|
|
46
|
-
this._log.debug(
|
|
47
|
-
'setting refresh timer for token %s',
|
|
48
|
-
authToken.getId()
|
|
49
|
-
);
|
|
50
|
-
this.__setupTokenRefreshTimer(this.__authToken);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return this.__authToken;
|
|
54
|
-
}).catch(e => {
|
|
55
|
-
this.__authToken = null;
|
|
56
|
-
throw e;
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
this.__authToken = tokenPromise;
|
|
60
|
-
return tokenPromise;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
this._log.debug('token already exist');
|
|
64
|
-
return Promise.resolve(this.__authToken);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @protected
|
|
69
|
-
* @returns {Promise<AuthToken>}
|
|
70
|
-
*/
|
|
71
|
-
_getTokenEntity(tokenId) {
|
|
72
|
-
return this.__apiClient.makeRequest('GET', '/auth/token/lookup-self', null, {'X-Vault-Token': tokenId})
|
|
73
|
-
.then(res => {
|
|
74
|
-
return AuthToken.fromResponse(res);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @protected
|
|
80
|
-
* @returns {boolean}
|
|
81
|
-
*/
|
|
82
|
-
_reauthenticationAllowed() {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {AuthToken} authToken
|
|
88
|
-
* @private
|
|
89
|
-
*/
|
|
90
|
-
__setupTokenRefreshTimer(authToken) {
|
|
91
|
-
if (this.__refreshTimeout !== null) {
|
|
92
|
-
lt.clearTimeout(this.__refreshTimeout);
|
|
93
|
-
this.__refreshTimeout = null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!authToken.isRenewable() || authToken.isExpired()) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const timer = Math.max((authToken.getExpiresAt() - Math.floor(Date.now() / 1000)) / 2, 1) * 1000;
|
|
101
|
-
|
|
102
|
-
this.__refreshTimeout = lt.setTimeout(() => {
|
|
103
|
-
this.__renewToken(authToken).then(authToken => {
|
|
104
|
-
this.__authToken = authToken;
|
|
105
|
-
this.__setupTokenRefreshTimer(authToken);
|
|
106
|
-
}).catch(err => {
|
|
107
|
-
this.__setupTokenRefreshTimer(authToken);
|
|
108
|
-
|
|
109
|
-
this._log.error(`Cannot refresh auth token with "${authToken.getAccessor()}" accessor. Error: ${err.message}`);
|
|
110
|
-
this._log.error(err);
|
|
111
|
-
});
|
|
112
|
-
}, timer);
|
|
113
|
-
|
|
114
|
-
this._log.debug(
|
|
115
|
-
'sleeping for %s',
|
|
116
|
-
prettyMs(timer)
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* @param {AuthToken} authToken
|
|
122
|
-
* @returns {Promise.<AuthToken>}
|
|
123
|
-
* @private
|
|
124
|
-
*/
|
|
125
|
-
__renewToken(authToken) {
|
|
126
|
-
this._log.debug('renewing vault token');
|
|
127
|
-
|
|
128
|
-
return this.__apiClient.makeRequest('POST', '/auth/token/renew-self', null, {'X-Vault-Token': authToken.getId()})
|
|
129
|
-
.then(() => {
|
|
130
|
-
this._log.info('successfully renewed token');
|
|
131
|
-
return this._getTokenEntity(authToken.getId());
|
|
132
|
-
})
|
|
133
|
-
.catch((reason) => {
|
|
134
|
-
this._log.error('token renew failed: %s', reason.message);
|
|
135
|
-
throw reason;
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
module.exports = VaultBaseAuth;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const lt = require('long-timeout');
|
|
4
|
+
const prettyMs = require('pretty-ms');
|
|
5
|
+
const AuthToken = require('./AuthToken');
|
|
6
|
+
const errors = require('../errors');
|
|
7
|
+
|
|
8
|
+
class VaultBaseAuth {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {VaultApiClient} apiClient
|
|
12
|
+
* @param {Object} logger
|
|
13
|
+
* @param {String} mount - Vault's mount point
|
|
14
|
+
*/
|
|
15
|
+
constructor(apiClient, logger, mount) {
|
|
16
|
+
this.__apiClient = apiClient;
|
|
17
|
+
/** @protected */
|
|
18
|
+
this._log = logger;
|
|
19
|
+
this._mount = mount;
|
|
20
|
+
|
|
21
|
+
/** @type AuthToken */
|
|
22
|
+
this.__authToken = null;
|
|
23
|
+
this.__refreshTimeout = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @protected
|
|
28
|
+
* @returns {Promise<AuthToken>}
|
|
29
|
+
*/
|
|
30
|
+
_authenticate() {
|
|
31
|
+
throw new Error('Method should be overridden');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getAuthToken() {
|
|
35
|
+
if (this.__authToken === null || (this.__authToken instanceof AuthToken && this.__authToken.isExpired() && this._reauthenticationAllowed())) {
|
|
36
|
+
if (this.__authToken !== null && this.__authToken.isExpired() && !this._reauthenticationAllowed()) {
|
|
37
|
+
throw new errors.AuthTokenExpiredError('Auth token has expired & cannot be refreshed since auth method doesn\'t support this.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this._log.info('getting auth token (mount=%s)', this._mount);
|
|
41
|
+
|
|
42
|
+
const tokenPromise = this._authenticate().then(authToken => {
|
|
43
|
+
this.__authToken = authToken;
|
|
44
|
+
|
|
45
|
+
if (this.__authToken.isRenewable()) {
|
|
46
|
+
this._log.debug(
|
|
47
|
+
'setting refresh timer for token %s',
|
|
48
|
+
authToken.getId()
|
|
49
|
+
);
|
|
50
|
+
this.__setupTokenRefreshTimer(this.__authToken);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return this.__authToken;
|
|
54
|
+
}).catch(e => {
|
|
55
|
+
this.__authToken = null;
|
|
56
|
+
throw e;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.__authToken = tokenPromise;
|
|
60
|
+
return tokenPromise;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this._log.debug('token already exist');
|
|
64
|
+
return Promise.resolve(this.__authToken);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @protected
|
|
69
|
+
* @returns {Promise<AuthToken>}
|
|
70
|
+
*/
|
|
71
|
+
_getTokenEntity(tokenId) {
|
|
72
|
+
return this.__apiClient.makeRequest('GET', '/auth/token/lookup-self', null, {'X-Vault-Token': tokenId})
|
|
73
|
+
.then(res => {
|
|
74
|
+
return AuthToken.fromResponse(res);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @protected
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
_reauthenticationAllowed() {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {AuthToken} authToken
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
__setupTokenRefreshTimer(authToken) {
|
|
91
|
+
if (this.__refreshTimeout !== null) {
|
|
92
|
+
lt.clearTimeout(this.__refreshTimeout);
|
|
93
|
+
this.__refreshTimeout = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!authToken.isRenewable() || authToken.isExpired()) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const timer = Math.max((authToken.getExpiresAt() - Math.floor(Date.now() / 1000)) / 2, 1) * 1000;
|
|
101
|
+
|
|
102
|
+
this.__refreshTimeout = lt.setTimeout(() => {
|
|
103
|
+
this.__renewToken(authToken).then(authToken => {
|
|
104
|
+
this.__authToken = authToken;
|
|
105
|
+
this.__setupTokenRefreshTimer(authToken);
|
|
106
|
+
}).catch(err => {
|
|
107
|
+
this.__setupTokenRefreshTimer(authToken);
|
|
108
|
+
|
|
109
|
+
this._log.error(`Cannot refresh auth token with "${authToken.getAccessor()}" accessor. Error: ${err.message}`);
|
|
110
|
+
this._log.error(err);
|
|
111
|
+
});
|
|
112
|
+
}, timer);
|
|
113
|
+
|
|
114
|
+
this._log.debug(
|
|
115
|
+
'sleeping for %s',
|
|
116
|
+
prettyMs(timer)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {AuthToken} authToken
|
|
122
|
+
* @returns {Promise.<AuthToken>}
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
__renewToken(authToken) {
|
|
126
|
+
this._log.debug('renewing vault token');
|
|
127
|
+
|
|
128
|
+
return this.__apiClient.makeRequest('POST', '/auth/token/renew-self', null, {'X-Vault-Token': authToken.getId()})
|
|
129
|
+
.then(() => {
|
|
130
|
+
this._log.info('successfully renewed token');
|
|
131
|
+
return this._getTokenEntity(authToken.getId());
|
|
132
|
+
})
|
|
133
|
+
.catch((reason) => {
|
|
134
|
+
this._log.error('token renew failed: %s', reason.message);
|
|
135
|
+
throw reason;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = VaultBaseAuth;
|
package/src/auth/VaultIAMAuth.js
CHANGED
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const VaultBaseAuth = require('./VaultBaseAuth');
|
|
4
|
-
const aws4 = require('aws4');
|
|
5
|
-
const { fromNodeProviderChain } = require('@aws-sdk/credential-providers');
|
|
6
|
-
const _ = require('lodash');
|
|
7
|
-
const errors = require('../errors');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Implementation of AWS Auth Backend :: IAM Authentication Method
|
|
11
|
-
* @link https://www.vaultproject.io/docs/auth/aws.html#iam-authentication-method
|
|
12
|
-
*
|
|
13
|
-
* @usage
|
|
14
|
-
*
|
|
15
|
-
* ```bash
|
|
16
|
-
* vault write auth/aws/config/client secret_key=AWS_SECRET_KEY access_key=AWS_ACCESS_KEY
|
|
17
|
-
* vault write auth/aws/config/client iam_server_id_header_value=VAULT_ADDR
|
|
18
|
-
* vault write auth/aws/role/iam_name_of_role auth_type=iam bound_iam_principal_arn=arn:aws:iam::.... max_ttl=500h
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* ```js
|
|
22
|
-
*
|
|
23
|
-
* VaultClient.boot('main', {
|
|
24
|
-
* api: { url: VAULT_ADDR },
|
|
25
|
-
* auth: {
|
|
26
|
-
* type: 'iam',
|
|
27
|
-
* mount: 'some_other_aws_mount_point', // Optional
|
|
28
|
-
* config: {
|
|
29
|
-
* role: 'my_iam_role',
|
|
30
|
-
* iam_server_id_header_value: VAULT_ADDR, // Optional
|
|
31
|
-
* namespace: 'some_namespace', // Optional
|
|
32
|
-
* credentials: { // Optional
|
|
33
|
-
* accessKeyId: AWS_ACCESS_KEY,
|
|
34
|
-
* secretAccessKey: AWS_SECRET_KEY,
|
|
35
|
-
* },
|
|
36
|
-
* }
|
|
37
|
-
* }
|
|
38
|
-
* })
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
*/
|
|
42
|
-
class VaultIAMAuth extends VaultBaseAuth {
|
|
43
|
-
/**
|
|
44
|
-
* @typedef AWSCredentials
|
|
45
|
-
* @property {String} accessKeyId
|
|
46
|
-
* @property {String} secretAccessKey
|
|
47
|
-
*
|
|
48
|
-
* @typedef {Object} VaultIAMAuthConfig
|
|
49
|
-
* @property {String} role - Role name of the auth/{mount}/role/{name} backend.
|
|
50
|
-
* @property [AWSCredentials] [credentials] - Optional. AWS Credentials
|
|
51
|
-
* @property {String} [iam_server_id_header_value] - Optional. Header's value X-Vault-AWS-IAM-Server-ID.
|
|
52
|
-
*
|
|
53
|
-
* @param {VaultApiClient} api - see {@link VaultBaseAuth#constructor}
|
|
54
|
-
* @param {Object} logger
|
|
55
|
-
* @param {VaultIAMAuthConfig} config
|
|
56
|
-
* @param {String} mount - Vault's AWS Auth Backend mount point ("aws" by default)
|
|
57
|
-
*/
|
|
58
|
-
constructor(api, logger, config, mount) {
|
|
59
|
-
super(api, logger, mount || 'aws');
|
|
60
|
-
|
|
61
|
-
this.__role = config.role;
|
|
62
|
-
this.__iam_server_id_header_value = config.iam_server_id_header_value;
|
|
63
|
-
this.__namespace = config.namespace;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const { credentials } = config;
|
|
67
|
-
this._validateCredentialsConfig(credentials);
|
|
68
|
-
|
|
69
|
-
this.__credentialChain = credentials ? () => Promise.resolve(credentials) : fromNodeProviderChain();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @inheritDoc
|
|
74
|
-
*/
|
|
75
|
-
_authenticate() {
|
|
76
|
-
this._log.info(
|
|
77
|
-
'making authentication request: role=%s',
|
|
78
|
-
this.__role
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
var headers = {};
|
|
82
|
-
|
|
83
|
-
if (this.__namespace) {
|
|
84
|
-
headers = {
|
|
85
|
-
'X-Vault-Namespace': this.__namespace,
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return Promise.resolve()
|
|
91
|
-
.then(() => this.__getCredentials())
|
|
92
|
-
.then((credentials) => {
|
|
93
|
-
return this.__apiClient.makeRequest(
|
|
94
|
-
'POST',
|
|
95
|
-
`/auth/${this._mount}/login`,
|
|
96
|
-
this.__getVaultAuthRequestBody(this.__getStsRequest(credentials)),
|
|
97
|
-
headers
|
|
98
|
-
);
|
|
99
|
-
})
|
|
100
|
-
.then((response) => {
|
|
101
|
-
this._log.debug(
|
|
102
|
-
'receive token: %s',
|
|
103
|
-
response.auth.client_token
|
|
104
|
-
);
|
|
105
|
-
return this._getTokenEntity(response.auth.client_token)
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Credentials resolved by {@see @aws-sdk/credential-providers}
|
|
111
|
-
*
|
|
112
|
-
* @returns {Promise<AwsCredentialIdentity>}
|
|
113
|
-
* @private
|
|
114
|
-
*/
|
|
115
|
-
__getCredentials() {
|
|
116
|
-
return this.__credentialChain();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Prepare vault auth request body
|
|
121
|
-
*
|
|
122
|
-
* @param stsRequest
|
|
123
|
-
* @returns {Object} {@link https://www.vaultproject.io/docs/auth/aws.html#via-the-api}
|
|
124
|
-
* @private
|
|
125
|
-
*/
|
|
126
|
-
__getVaultAuthRequestBody(stsRequest) {
|
|
127
|
-
return {
|
|
128
|
-
iam_http_request_method: stsRequest.method,
|
|
129
|
-
iam_request_headers: this.__base64encode(
|
|
130
|
-
JSON.stringify(this.__headersLikeGolangStyle(stsRequest.headers))
|
|
131
|
-
),
|
|
132
|
-
iam_request_body: this.__base64encode(stsRequest.body),
|
|
133
|
-
iam_request_url: this.__base64encode(`https://${stsRequest.hostname}${stsRequest.path}`),
|
|
134
|
-
role: this.__role
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Prepare signed request to AWS STS :: GetCallerIdentity
|
|
140
|
-
*
|
|
141
|
-
* @param credentials
|
|
142
|
-
* @private
|
|
143
|
-
*/
|
|
144
|
-
__getStsRequest(credentials) {
|
|
145
|
-
return aws4.sign({
|
|
146
|
-
service: 'sts',
|
|
147
|
-
method: 'POST',
|
|
148
|
-
body: 'Action=GetCallerIdentity&Version=2011-06-15',
|
|
149
|
-
headers: this.__iam_server_id_header_value ? {
|
|
150
|
-
'X-Vault-AWS-IAM-Server-ID': this.__iam_server_id_header_value,
|
|
151
|
-
} : {}
|
|
152
|
-
}, credentials);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* @param string
|
|
157
|
-
* @private
|
|
158
|
-
*/
|
|
159
|
-
__base64encode(string) {
|
|
160
|
-
return new Buffer(string).toString('base64')
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* @link https://github.com/hashicorp/vault/issues/2810
|
|
165
|
-
* @link https://golang.org/pkg/net/http/#Header
|
|
166
|
-
*
|
|
167
|
-
* @param {Object} headers
|
|
168
|
-
* @returns {Object}
|
|
169
|
-
* @private
|
|
170
|
-
*/
|
|
171
|
-
__headersLikeGolangStyle(headers) {
|
|
172
|
-
return _.mapValues(headers, (value) => [`${value}`]);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
_validateCredentialsConfig(credentials) {
|
|
176
|
-
if (Array.isArray(credentials)) {
|
|
177
|
-
throw new errors.InvalidAWSCredentialsError('Invalid AWS credentials provided in config. See CHANGELOG if migating from 0.x.x');
|
|
178
|
-
}
|
|
179
|
-
if (credentials && typeof credentials === 'object') {
|
|
180
|
-
if (!credentials.secretAccessKey || !credentials.accessKeyId) {
|
|
181
|
-
throw new errors.InvalidAWSCredentialsError('Invalid AWS credentials provided in config: accessKeyId and secretAccessKey are required.');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
module.exports = VaultIAMAuth;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const VaultBaseAuth = require('./VaultBaseAuth');
|
|
4
|
+
const aws4 = require('aws4');
|
|
5
|
+
const { fromNodeProviderChain } = require('@aws-sdk/credential-providers');
|
|
6
|
+
const _ = require('lodash');
|
|
7
|
+
const errors = require('../errors');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Implementation of AWS Auth Backend :: IAM Authentication Method
|
|
11
|
+
* @link https://www.vaultproject.io/docs/auth/aws.html#iam-authentication-method
|
|
12
|
+
*
|
|
13
|
+
* @usage
|
|
14
|
+
*
|
|
15
|
+
* ```bash
|
|
16
|
+
* vault write auth/aws/config/client secret_key=AWS_SECRET_KEY access_key=AWS_ACCESS_KEY
|
|
17
|
+
* vault write auth/aws/config/client iam_server_id_header_value=VAULT_ADDR
|
|
18
|
+
* vault write auth/aws/role/iam_name_of_role auth_type=iam bound_iam_principal_arn=arn:aws:iam::.... max_ttl=500h
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ```js
|
|
22
|
+
*
|
|
23
|
+
* VaultClient.boot('main', {
|
|
24
|
+
* api: { url: VAULT_ADDR },
|
|
25
|
+
* auth: {
|
|
26
|
+
* type: 'iam',
|
|
27
|
+
* mount: 'some_other_aws_mount_point', // Optional
|
|
28
|
+
* config: {
|
|
29
|
+
* role: 'my_iam_role',
|
|
30
|
+
* iam_server_id_header_value: VAULT_ADDR, // Optional
|
|
31
|
+
* namespace: 'some_namespace', // Optional
|
|
32
|
+
* credentials: { // Optional
|
|
33
|
+
* accessKeyId: AWS_ACCESS_KEY,
|
|
34
|
+
* secretAccessKey: AWS_SECRET_KEY,
|
|
35
|
+
* },
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
*/
|
|
42
|
+
class VaultIAMAuth extends VaultBaseAuth {
|
|
43
|
+
/**
|
|
44
|
+
* @typedef AWSCredentials
|
|
45
|
+
* @property {String} accessKeyId
|
|
46
|
+
* @property {String} secretAccessKey
|
|
47
|
+
*
|
|
48
|
+
* @typedef {Object} VaultIAMAuthConfig
|
|
49
|
+
* @property {String} role - Role name of the auth/{mount}/role/{name} backend.
|
|
50
|
+
* @property [AWSCredentials] [credentials] - Optional. AWS Credentials
|
|
51
|
+
* @property {String} [iam_server_id_header_value] - Optional. Header's value X-Vault-AWS-IAM-Server-ID.
|
|
52
|
+
*
|
|
53
|
+
* @param {VaultApiClient} api - see {@link VaultBaseAuth#constructor}
|
|
54
|
+
* @param {Object} logger
|
|
55
|
+
* @param {VaultIAMAuthConfig} config
|
|
56
|
+
* @param {String} mount - Vault's AWS Auth Backend mount point ("aws" by default)
|
|
57
|
+
*/
|
|
58
|
+
constructor(api, logger, config, mount) {
|
|
59
|
+
super(api, logger, mount || 'aws');
|
|
60
|
+
|
|
61
|
+
this.__role = config.role;
|
|
62
|
+
this.__iam_server_id_header_value = config.iam_server_id_header_value;
|
|
63
|
+
this.__namespace = config.namespace;
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
const { credentials } = config;
|
|
67
|
+
this._validateCredentialsConfig(credentials);
|
|
68
|
+
|
|
69
|
+
this.__credentialChain = credentials ? () => Promise.resolve(credentials) : fromNodeProviderChain();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @inheritDoc
|
|
74
|
+
*/
|
|
75
|
+
_authenticate() {
|
|
76
|
+
this._log.info(
|
|
77
|
+
'making authentication request: role=%s',
|
|
78
|
+
this.__role
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
var headers = {};
|
|
82
|
+
|
|
83
|
+
if (this.__namespace) {
|
|
84
|
+
headers = {
|
|
85
|
+
'X-Vault-Namespace': this.__namespace,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
return Promise.resolve()
|
|
91
|
+
.then(() => this.__getCredentials())
|
|
92
|
+
.then((credentials) => {
|
|
93
|
+
return this.__apiClient.makeRequest(
|
|
94
|
+
'POST',
|
|
95
|
+
`/auth/${this._mount}/login`,
|
|
96
|
+
this.__getVaultAuthRequestBody(this.__getStsRequest(credentials)),
|
|
97
|
+
headers
|
|
98
|
+
);
|
|
99
|
+
})
|
|
100
|
+
.then((response) => {
|
|
101
|
+
this._log.debug(
|
|
102
|
+
'receive token: %s',
|
|
103
|
+
response.auth.client_token
|
|
104
|
+
);
|
|
105
|
+
return this._getTokenEntity(response.auth.client_token)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Credentials resolved by {@see @aws-sdk/credential-providers}
|
|
111
|
+
*
|
|
112
|
+
* @returns {Promise<AwsCredentialIdentity>}
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
__getCredentials() {
|
|
116
|
+
return this.__credentialChain();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Prepare vault auth request body
|
|
121
|
+
*
|
|
122
|
+
* @param stsRequest
|
|
123
|
+
* @returns {Object} {@link https://www.vaultproject.io/docs/auth/aws.html#via-the-api}
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
__getVaultAuthRequestBody(stsRequest) {
|
|
127
|
+
return {
|
|
128
|
+
iam_http_request_method: stsRequest.method,
|
|
129
|
+
iam_request_headers: this.__base64encode(
|
|
130
|
+
JSON.stringify(this.__headersLikeGolangStyle(stsRequest.headers))
|
|
131
|
+
),
|
|
132
|
+
iam_request_body: this.__base64encode(stsRequest.body),
|
|
133
|
+
iam_request_url: this.__base64encode(`https://${stsRequest.hostname}${stsRequest.path}`),
|
|
134
|
+
role: this.__role
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Prepare signed request to AWS STS :: GetCallerIdentity
|
|
140
|
+
*
|
|
141
|
+
* @param credentials
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
__getStsRequest(credentials) {
|
|
145
|
+
return aws4.sign({
|
|
146
|
+
service: 'sts',
|
|
147
|
+
method: 'POST',
|
|
148
|
+
body: 'Action=GetCallerIdentity&Version=2011-06-15',
|
|
149
|
+
headers: this.__iam_server_id_header_value ? {
|
|
150
|
+
'X-Vault-AWS-IAM-Server-ID': this.__iam_server_id_header_value,
|
|
151
|
+
} : {}
|
|
152
|
+
}, credentials);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param string
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
__base64encode(string) {
|
|
160
|
+
return new Buffer(string).toString('base64')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @link https://github.com/hashicorp/vault/issues/2810
|
|
165
|
+
* @link https://golang.org/pkg/net/http/#Header
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} headers
|
|
168
|
+
* @returns {Object}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
__headersLikeGolangStyle(headers) {
|
|
172
|
+
return _.mapValues(headers, (value) => [`${value}`]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_validateCredentialsConfig(credentials) {
|
|
176
|
+
if (Array.isArray(credentials)) {
|
|
177
|
+
throw new errors.InvalidAWSCredentialsError('Invalid AWS credentials provided in config. See CHANGELOG if migating from 0.x.x');
|
|
178
|
+
}
|
|
179
|
+
if (credentials && typeof credentials === 'object') {
|
|
180
|
+
if (!credentials.secretAccessKey || !credentials.accessKeyId) {
|
|
181
|
+
throw new errors.InvalidAWSCredentialsError('Invalid AWS credentials provided in config: accessKeyId and secretAccessKey are required.');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = VaultIAMAuth;
|