cerberus_client 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module CerberusUtils
2
+ VERSION = "2.0.0"
3
+ end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cerberus_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
+ - Shaun Ford
7
8
  - Joe Teibel
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2017-08-25 00:00:00.000000000 Z
12
+ date: 2017-10-17 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: aws-sdk
@@ -52,13 +53,13 @@ dependencies:
52
53
  - - ~>
53
54
  - !ruby/object:Gem::Version
54
55
  version: '1.13'
55
- description: This is a Ruby based client library for communicating with Vault via
56
+ description: This is a Ruby based client library for communicating with Cerberus via
56
57
  HTTP and enables authentication schemes specific to AWS and Cerberus. This client
57
58
  currently supports read-only operations (write operations are not yet implemented,
58
59
  feel free to open a pull request to implement write operations). To learn more about
59
60
  Cerberus, please visit the Cerberus website.
60
61
  email:
61
- - joe.teibel@nike.com
62
+ - shaun.ford@nike.com
62
63
  executables: []
63
64
  extensions: []
64
65
  extra_rdoc_files: []
@@ -73,9 +74,10 @@ files:
73
74
  - README.md
74
75
  - cerberus_client.gemspec
75
76
  - lib/cerberus/assumed_role_credentials_provider_chain.rb
77
+ - lib/cerberus/aws_assumed_role_credentials_provider.rb
76
78
  - lib/cerberus/aws_principal_credentials_provider.rb
77
- - lib/cerberus/aws_role_credentials_provider.rb
78
- - lib/cerberus/aws_role_info.rb
79
+ - lib/cerberus/cerberus_auth_info.rb
80
+ - lib/cerberus/cerberus_client.rb
79
81
  - lib/cerberus/cerberus_client_token.rb
80
82
  - lib/cerberus/default_credentials_provider_chain.rb
81
83
  - lib/cerberus/default_url_resolver.rb
@@ -84,12 +86,12 @@ files:
84
86
  - lib/cerberus/exception/http_error.rb
85
87
  - lib/cerberus/exception/no_valid_providers.rb
86
88
  - lib/cerberus/exception/no_value_error.rb
87
- - lib/cerberus/vault_client.rb
88
89
  - lib/cerberus_client.rb
89
- - lib/cerberus_client/default_logger.rb
90
- - lib/cerberus_client/http.rb
91
- - lib/cerberus_client/log.rb
92
- - lib/cerberus_client/version.rb
90
+ - lib/cerberus_utils/default_logger.rb
91
+ - lib/cerberus_utils/http.rb
92
+ - lib/cerberus_utils/log.rb
93
+ - lib/cerberus_utils/utils.rb
94
+ - lib/cerberus_utils/version.rb
93
95
  homepage: https://github.com/Nike-Inc/cerberus-ruby-client
94
96
  licenses:
95
97
  - Apache License Version 2
@@ -1,239 +0,0 @@
1
- module Cerberus
2
-
3
- require_relative('../cerberus_client/log')
4
- require_relative('../cerberus_client/http')
5
- require_relative('../cerberus_client')
6
- require_relative('exception/no_value_error')
7
- require_relative('exception/http_error')
8
- require_relative('cerberus_client_token')
9
- require_relative('aws_role_info')
10
- require('aws-sdk')
11
- require('net/http')
12
- require('json')
13
- require('base64')
14
-
15
- ##
16
- # The AWS IAM role credentials provider
17
- ##
18
- class AwsRoleCredentialsProvider
19
-
20
- # AWS metadata instance URL
21
- INSTANCE_METADATA_SVC_BASE_URL = "http://169.254.169.254/latest/meta-data"
22
- # relative URI to look up AZ in AWS metadata svc
23
- REGION_REL_URI = "/placement/availability-zone"
24
- # relative URI to look up IAM role in AWS metadata svc
25
- IAM_ROLE_INFO_REL_URI = "/iam/info"
26
- # reference into the metadata data json we get to look up IAM role
27
- IAM_ROLE_ARN_KEY = 'InstanceProfileArn'
28
- # magic number is the index into a split role ARN to grab the acccount ID
29
- ROLE_ARN_ARRAY_INDEX_OF_ACCOUNTNUM = 4
30
- # magic number is the index into a split role ARN to grab the role name
31
- ROLE_ARN_ARRAY_INDEX_OF_ROLENAME = 1
32
- # relative URI to get encrypted auth data from Cerberus
33
- ROLE_AUTH_REL_URI = "/v1/auth/iam-role"
34
- # reference into the decrypted auth data json we get from Cerberus
35
- CERBERUS_AUTH_DATA_CLIENT_TOKEN_KEY = "client_token"
36
- CERBERUS_AUTH_DATA_LEASE_DURATION_KEY = "lease_duration"
37
- CERBERUS_AUTH_DATA_POLICIES_KEY = "policies"
38
-
39
- LOGGER = CerberusClient::Log.instance
40
-
41
- ##
42
- # Init AWS role provider - needs vault base url. Instance metadata service url is optional to make unit tests
43
- # easier and so we can provide a hook to set this via config as needed
44
- ##
45
- def initialize(vaultBaseUrl, instanceMdSvcBaseUrl = nil, roleName = nil, roleRegion = nil, roleAccountId = nil)
46
- @vaultBaseUrl = vaultBaseUrl
47
- @clientToken = nil
48
- @role = get_role_info(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId)
49
-
50
- LOGGER.debug("AwsRoleCredentialsProvider initialized with vault base url #{@vaultBaseUrl}")
51
- end
52
-
53
- ##
54
- # Get credentials using AWS IAM role
55
- ##
56
- def getClientToken
57
-
58
- if (@role.nil?)
59
- raise Cerberus::Exception::NoValueError
60
- end
61
-
62
- if (@clientToken.nil?)
63
- @clientToken = getCredentialsFromCerberus
64
- end
65
-
66
- # using two if statements here just to make the logging easier..
67
- # the above we expect on startup, expiration is an interesting event worth a debug log all its own
68
- if (@clientToken.expired?)
69
- LOGGER.debug("Existing ClientToken has expired - refreshing from Cerberus...")
70
- @clientToken = getCredentialsFromCerberus
71
- end
72
-
73
- return @clientToken.authToken
74
-
75
- end
76
-
77
- ##
78
- # Policy: if we are given these three pieces of data, we will assume a role to do auth
79
- ##
80
- def should_assume_role?(roleAccountId, roleName, roleRegion)
81
- !(roleName.nil? || roleAccountId.nil? || roleRegion.nil?)
82
- end
83
-
84
- ##
85
- # Policy: if we do not have an instance MD service URL and we can't assume a role, then this instance
86
- # of the provider cannot use a role to provide credentials. Primarily used for testing.
87
- ##
88
- def have_access_to_role?(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId)
89
- (!instanceMdSvcBaseUrl.nil? || should_assume_role?(roleName, roleRegion, roleAccountId))
90
- end
91
-
92
- private
93
-
94
- ##
95
- # Uses provided data to determine how to construct the AwsRoleInfo use by this provider
96
- ##
97
- def get_role_info(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId)
98
-
99
- # if we have no metedata about how to auth, we do nothing
100
- # this is used in unit testing primarily
101
- if (!have_access_to_role?(instanceMdSvcBaseUrl, roleName, roleRegion, roleAccountId))
102
- return nil;
103
- elsif (should_assume_role?(roleAccountId, roleName, roleRegion))
104
- # we are assuming a role to do auth
105
- return get_role_from_provided_info(roleName, roleRegion, roleAccountId)
106
- else
107
- # we are using a role that the instance has associated with it
108
- @instanceMdSvcBaseUrl = instanceMdSvcBaseUrl.nil? ? INSTANCE_METADATA_SVC_BASE_URL : instanceMdSvcBaseUrl
109
-
110
- # collect instance MD we need to auth with Cerberus
111
- return get_role_from_instance_metadata
112
- end
113
- end
114
-
115
-
116
- ##
117
- # Get an AwsRoleInfo object from the provided data
118
- ##
119
- def get_role_from_provided_info(roleName, roleRegion, roleAccountId)
120
-
121
- role_creds = Aws::AssumeRoleCredentials.new(
122
- client: Aws::STS::Client.new(region: roleRegion),
123
- role_arn: "arn:aws:iam::#{roleAccountId}:role/#{roleName}",
124
- role_session_name: "hiera-cpe-build")
125
-
126
- return AwsRoleInfo.new(roleName, roleRegion, roleAccountId, credentials: role_creds)
127
- end
128
-
129
- ##
130
- # Use the instance metadata to extract the role information
131
- # This function should only be called from an EC2 instance otherwise the http
132
- # call will fail.
133
- ##
134
- def get_role_from_instance_metadata
135
- role_arn = getIAMRoleARN
136
- region = getRegionFromAZ(getAvailabilityZone)
137
- account_id = getAccountIdFromRoleARN(role_arn)
138
- role_name = getRoleNameFromRoleARN(role_arn)
139
-
140
- LOGGER.debug("roleARN #{role_arn}")
141
- LOGGER.debug("region #{region}")
142
- LOGGER.debug("accountId #{account_id}")
143
- LOGGER.debug("roleName #{role_name}")
144
-
145
- return AwsRoleInfo.new(role_name, region, account_id, nil)
146
- end
147
-
148
- ##
149
- # Reach out to the Cerberus management service and get an auth token
150
- ##
151
- def getCredentialsFromCerberus
152
- begin
153
- authData = doAuthWithCerberus(@role.account_id, @role.name, @role.region)
154
-
155
- LOGGER.debug("Got auth data from Cerberus. Attempting to decrypt...")
156
-
157
- # decrypt the data we got from cerberus to get the vault token
158
- kms = Aws::KMS::Client.new(region: @role.region, credentials: @role.credentials[:credentials])
159
-
160
- decryptedAuthDataJson = JSON.parse(kms.decrypt(ciphertext_blob: Base64.decode64(authData)).plaintext)
161
-
162
- LOGGER.debug("Decrypt successful. Passing back Cerberus auth token.")
163
- # pass back a credentials object that will allow us to reuse it until it expires
164
- CerberusClientToken.new(decryptedAuthDataJson[CERBERUS_AUTH_DATA_CLIENT_TOKEN_KEY],
165
- decryptedAuthDataJson[CERBERUS_AUTH_DATA_LEASE_DURATION_KEY],
166
- decryptedAuthDataJson[CERBERUS_AUTH_DATA_POLICIES_KEY])
167
-
168
- rescue Cerberus::Exception::HttpError
169
- # catch http errors here and assert no value
170
- # this may not actually be the case, there are legitimate reasons HTTP can fail when it "should" work
171
- # but this is handled by logging - a warning is set in the log in during the HTTP call
172
- raise Cerberus::Exception::NoValueError
173
- end
174
- end
175
-
176
- ##
177
- # Get the AWS account ID from the role ARN
178
- # Expects formatting [some value]:[some value]:[some value]::[account id]
179
- ##
180
- def getAccountIdFromRoleARN(roleARN)
181
- roleARN.split(':')[ROLE_ARN_ARRAY_INDEX_OF_ACCOUNTNUM]
182
- end
183
-
184
- ##
185
- # Get the role name from the role ARN
186
- # Expects formatting [some value]:[some value]:[some value]::[account id]
187
- ##
188
- def getRoleNameFromRoleARN(roleARN)
189
- roleARN.split('/')[ROLE_ARN_ARRAY_INDEX_OF_ROLENAME]
190
- end
191
-
192
- ##
193
- # Read the IAM role ARN from the instance metadata
194
- # Will throw an HTTP exception if there is no IAM role associated with the instance
195
- ##
196
- def getIAMRoleARN
197
- response = doHttpToMDService(IAM_ROLE_INFO_REL_URI)
198
- jsonResponseBody = JSON.parse(response.body)
199
- jsonResponseBody[IAM_ROLE_ARN_KEY]
200
- end
201
-
202
- ##
203
- # Get the region from AWS instance metadata
204
- ##
205
- def getAvailabilityZone
206
- doHttpToMDService(REGION_REL_URI).body
207
- end
208
-
209
- ##
210
- # Get region from AZ
211
- ##
212
- def getRegionFromAZ(az)
213
- az[0, az.length-1]
214
- end
215
-
216
- ##
217
- # Call the instance metadata service with a relative URI and return the response if the call succeeds
218
- # else throw an IOError for non-2xx responses and RuntimeError for any exceptions down the stack
219
- ##
220
- def doHttpToMDService(relUri)
221
- url = URI(@instanceMdSvcBaseUrl + relUri)
222
- CerberusClient::Http.new.doHttp(url, 'GET', false)
223
- end
224
-
225
- ##
226
- #
227
- ##
228
- def doAuthWithCerberus(accountId, roleName, region)
229
- postJsonData = JSON.generate({:account_id => accountId, :role_name => roleName, :region => region})
230
- authUrl = URI(@vaultBaseUrl + ROLE_AUTH_REL_URI)
231
- useSSL = ! ("#{@vaultBaseUrl}".include? "localhost")
232
- authResponse = CerberusClient::Http.new.doHttp(authUrl, 'POST', useSSL, postJsonData)
233
- # if we got this far, we should have a valid response with encrypted data
234
- # send back the encrypted data
235
- JSON.parse(authResponse.body)['auth_data']
236
- end
237
-
238
- end
239
- end
@@ -1,15 +0,0 @@
1
- module Cerberus
2
- class AwsRoleInfo
3
- attr_reader :name
4
- attr_reader :region
5
- attr_reader :credentials
6
- attr_reader :account_id
7
-
8
- def initialize(name, region, account_id, credentials = nil)
9
- @name = name
10
- @region = region
11
- @account_id = account_id
12
- @credentials = credentials
13
- end
14
- end
15
- end
@@ -1,207 +0,0 @@
1
-
2
-
3
- module Cerberus
4
-
5
- require_relative('../cerberus_client/log')
6
- require_relative('exception/http_error')
7
- require_relative('exception/ambiguous_vault_bad_request')
8
- require_relative('default_credentials_provider_chain')
9
- require('json')
10
-
11
- ##
12
- # Client for interacting with the Vault API
13
- ##
14
- class VaultClient
15
-
16
- # relative path to the Vault secrets API
17
- SECRET_PATH_PREFIX = "/v1/secret/"
18
- SECRET_MAP_DATA_KEY = VAULT_LIST_DATA_KEY = "data"
19
- VAULT_TOKEN_HEADER_KEY = 'X-Vault-Token'
20
- VAULT_ERRORS_KEY = "errors"
21
- VAULT_PERMISSION_DENIED_ERR = "permission denied"
22
- VAULT_LIST_KEYS_KEY = "keys"
23
- VAULT_LIST_PARAM_KEY = "list"
24
- SLASH = "/"
25
-
26
- attr_reader :vaultBaseUrl
27
- attr_reader :credentialsProvider
28
-
29
- ##
30
- # Init with the base URL for vault
31
- ##
32
- def initialize(urlResolver, credentialsProviderChain)
33
-
34
- require 'net/https'
35
-
36
- @vaultBaseUrl = CerberusClient.getUrlFromResolver(urlResolver)
37
- @credentialsProvider = credentialsProviderChain.getCredentialsProvider
38
-
39
- end # initialize
40
-
41
- ##
42
- # Read operation for a specified path.
43
- ##
44
- def read(path)
45
- begin
46
- response = doVaultHttpGet(SECRET_PATH_PREFIX + path)
47
- CerberusClient::Log.instance.debug("VaultClient::read(path) HTTP response: #{response.code}, #{response.message}")
48
- response.body
49
-
50
- rescue => ex
51
- CerberusClient::Log.instance.error("VaultClient::read(#{path}) unhandled exception trying to read: #{ex.message}")
52
- raise ex
53
- end
54
- end # read
55
-
56
- ##
57
- # Read operation for a specified path.
58
- ##
59
- def readKey(path, key)
60
- begin
61
- CerberusClient::Log.instance.error("VaultClient::read(#{path}, #{key})")
62
-
63
- readPathAndIterateOnDataWithProc(path, &(Proc.new { |k, v| if(key == k); return v; end }) )
64
-
65
- # else, we didn't find it
66
- return nil
67
-
68
- rescue => ex
69
- CerberusClient::Log.instance.error("VaultClient::read(#{path}, #{key}) unhandled exception trying to read: #{ex.message}")
70
- raise ex
71
- end
72
- end
73
-
74
- ##
75
- # Returns a list of key names at the specified location. Folders are suffixed with /.
76
- # The input must be a folder; list on a file will return nil
77
- ##
78
- def list(path)
79
- begin
80
- response = doVaultHttpGet(SECRET_PATH_PREFIX + path + "?list=true")
81
-
82
- CerberusClient::Log.instance.debug("VaultClient::list(#{path}) HTTP response: #{response.code}, #{response.message}")
83
-
84
- jsonResonseBody = JSON.parse(response.body)
85
- pathList = jsonResonseBody[VAULT_LIST_DATA_KEY][VAULT_LIST_KEYS_KEY]
86
- CerberusClient::Log.instance.debug("VaultClient::list returning #{pathList.join(", ")} ")
87
- pathList
88
-
89
- rescue => ex
90
-
91
- # check to see if we threw the Http error with a response object
92
- response = (ex.instance_of?(Cerberus::Exception::HttpError)) ? ex.response : nil
93
- if(!response.nil? && response.code.to_i == 404)
94
- return nil
95
- end
96
-
97
- CerberusClient::Log.instance.error("VaultClient::list(#{path}) unhandled exception trying to read: #{ex.message}")
98
- raise ex
99
- end
100
- end
101
-
102
- ##
103
- # This is potentially an expensive operation depending on the depth of the tree we're trying to parse
104
- # It recursively walks 'path' and returns a hash of all child [path] => [array of keys] found under 'path'
105
- # if 'path' is a folder, it must have a trailing slash ('/'). If 'path' is an "end node" or "vault file", then it
106
- # should not have a trailing slash ('/')
107
- ##
108
- def describe!(path, resultHash = nil)
109
-
110
- CerberusClient::Log.instance.debug("VaultClient::describe!(#{path})")
111
-
112
- if(resultHash == nil)
113
- resultHash = Hash.new()
114
- end
115
-
116
- curChildren = list(path)
117
-
118
- # if curChildren is nil, it's possible there are no children or that we don't have access
119
- # It's also possible it is the "end" of the path... what Vault calls "the file"
120
- # in that case, we should send back the keys in that path so give it a shot
121
- if(curChildren.nil?)
122
- resultHash[path] = Array.new
123
- readPathAndIterateOnDataWithProc(path, &(Proc.new { |key, value| resultHash[path] << key }) )
124
- return resultHash
125
- end
126
-
127
- curChildren.each { |childNode|
128
- curLocation = path + childNode
129
- # if childNode ends with '/' then we have a directory we need to call into
130
- if(childNode.end_with?(SLASH))
131
- describe!(curLocation, resultHash)
132
- else # it is a "directory" that contains keys
133
- resultHash[curLocation] = Array.new
134
- readPathAndIterateOnDataWithProc(curLocation, &(Proc.new { |key, value| resultHash[curLocation] << key }) )
135
- end
136
- }
137
- return resultHash
138
- end
139
-
140
- private
141
-
142
-
143
- ##
144
- # Attempts to execute the proc passed in on every key, value located in the 'data' element read at 'path'
145
- ##
146
- def readPathAndIterateOnDataWithProc(path, &p)
147
- jsonResponseBody = JSON.parse(read(path))
148
- jsonResponseBody[VAULT_LIST_DATA_KEY].each { |dataMapKey, dataMapValue|
149
- p.call(dataMapKey, dataMapValue)
150
- }
151
- end
152
-
153
- ##
154
- # Do an http request to Vault using the relative URI passed in
155
- ##
156
- def doVaultHttpGet(relativeUri)
157
-
158
- url = URI(@vaultBaseUrl + relativeUri)
159
- useSSL = ! ("#{@vaultBaseUrl}".include? "localhost")
160
-
161
- begin
162
- response = CerberusClient::Http.new.doHttp(url,
163
- 'GET', useSSL, nil,
164
- {VAULT_TOKEN_HEADER_KEY =>
165
- CerberusClient.getCredentialsFromProvider(@credentialsProvider)})
166
-
167
- rescue Cerberus::Exception::HttpError => ex
168
- # Since Vault wants to pass back 400 bad request for both paths we don't have access to
169
- # and paths that don't actually exist at all, I'm sending back a specific error so that implementing clients
170
- # can at least understand the situation they find themselves in
171
- #
172
- # This client could actually work around this problem by first getting a list of all paths we have access to and
173
- # determining if the path exists in that list. If not, 404 (which is more appropriate than 400).
174
- # TODO: implement "list > check for path" work around <-- NOTE: This is a relatively expensive operation
175
-
176
- if(!ex.response.nil? && (ex.response.code.to_i == 400) && (hasPermissionErrors?(ex.response.body)))
177
- raise Exception::AmbiguousVaultBadRequest.new
178
- else
179
- raise ex
180
- end
181
-
182
- end
183
-
184
- response
185
-
186
- end # doVaultHttp
187
-
188
- ##
189
- # Parse out the permission errors json
190
- ##
191
- def hasPermissionErrors?(jsonErrorsMsg)
192
- begin
193
- json = JSON.parse(jsonErrorsMsg)
194
- json[VAULT_ERRORS_KEY].each { |err|
195
- if(err == VAULT_PERMISSION_DENIED_ERR)
196
- return true
197
- end
198
- }
199
- rescue => ex
200
- CerberusClient::Log.instance.warn(
201
- "VaultClient::hasPermissionErrors? called and exception thrown parsing #{jsonErrorsMsg}: #{ex.message}")
202
- return false
203
- end
204
- end
205
-
206
- end
207
- end