Class: VaultSession

Inherits:
Object
  • Object
show all
Defined in:
lib/puppet_x/vault_secrets/vaultsession.rb

Overview

Class provides methods to interface with Hashicorp Vault

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ VaultSession

Returns a new instance of VaultSession.

Raises:

  • (Puppet::Error)


10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 10

def initialize(args)
  # @summary Provide methods to interface with Hashicorp Vault
  # @param [Hash] args Configuration options for the Vault connection.
  # @option args [String] :uri Required The URL of a Vault API endpoint
  # @option args [Integer] :timeout Optional Seconds to wait for connection attempts. (5)
  # @option args [Boolean] :secure Optional When true, security certificates will be validated against the 'ca_file' (true)
  # @option args [String] :ca_file Optional path to a file containing the trusted certificate authority chain.
  # @option args [String] :token Optional token used to access the Vault API, otherwise attempts certificate authentication using the Puppet agent certificate.
  # @option args [String] :auth_path The Vault path of the "cert" authentication type for Puppet certificates
  # @option args [String] :auth_name The optional Vault certificate named role to authenticate against
  # @option args [Boolean] :fail_hard Optional Raise an exception on errors when true, or return an empty hash when false. (true)
  # @option args [String] :version The version of the Vault key/value secrets engine, either 'v1' or 'v2'. (v1)
  raise Puppet::Error, "The #{self.class.name} class requires a 'uri'." unless args.key?('uri')
  @uri = URI(args['uri'])
  raise Puppet::Error, "Unable to parse a hostname from #{args['uri']}" unless uri.hostname
  @fail_hard = if [true, false].include? args.dig('fail_hard')
                 args.dig('fail_hard')
               else
                 true
               end
  timeout = if args.dig('timeout').is_a? Integer
              args['timeout']
            else
              5
            end
  @version = if args.dig('version') == 'v2'
               'v2'
             else
               'v1'
             end
  http = Net::HTTP.new(uri.host, uri.port)
  http.open_timeout = timeout
  http.read_timeout = timeout
  secure = true unless args.dig('secure') == false || @uri.scheme == 'http'
  if secure
    ca_trust = if args.dig('ca_trust').is_a? String
                 args['ca_trust']
               else
                 nil
               end
    http.use_ssl = true
    http.ssl_version = :TLSv1_2
    http.ca_file = get_ca_file(ca_trust)
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  elsif @uri.scheme == 'https'
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  else
    http.use_ssl = false
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end
  @http = http
  token = if args.dig('token')
            args['token']
          else
            raise Puppet::Error, "An 'auth_path' must be defined when not using a token." unless args.key?('auth_path')
            get_token(args['auth_path'], args['auth_name'])
          end
  @headers = {
    'Content-Type': 'application/json',
    'X-Vault-Token': token,
  }
end

Instance Attribute Details

#fail_hardObject

Returns the value of attribute fail_hard.



74
75
76
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 74

def fail_hard
  @fail_hard
end

#httpObject

Returns the value of attribute http.



74
75
76
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 74

def http
  @http
end

#secureObject

Returns the value of attribute secure.



74
75
76
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 74

def secure
  @secure
end

#uriObject

Returns the value of attribute uri.



74
75
76
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 74

def uri
  @uri
end

Instance Method Details

#append_api_errors(message, response) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 92

def append_api_errors(message, response)
  # @summary Add meaningful(maybe?) messages to errors
  # @param [String] :message The error string before appending any API errors.
  # @param [Net::HTTPResponse] :response The method will try to read errors from the response and append to 'message'
  # @return [String] The updated error message including any errors found in the response.
  errors = begin
             JSON.parse_json(response.body)['errors']
           rescue
             nil
           end
  message << " (api errors: #{errors})" if errors
  message
end

#err_check(response) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 76

def err_check(response)
  # @summary Consistent error handling for common failures
  # @param response An instance of Net::HTTPResponse
  # @return nil
  if response.is_a?(Net::HTTPNotFound)
    err_message = "Vault path not found. (#{response.code} from #{@uri})"
    raise Puppet::Error, append_api_errors(err_message, response) if fail_hard
    Puppet.debug append_api_errors(err_message, response)
  elsif !response.is_a?(Net::HTTPOK)
    err_message = "Vault request failed. (#{response.code}) from #{@uri})"
    raise Puppet::Error, append_api_errors(err_message, response) if fail_hard
    Puppet.debug append_api_errors(err_message, response)
  end
  nil
end

#get(uri_path = @uri.path, version = @version) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 131

def get(uri_path = @uri.path, version = @version)
  # @summary Submit an HTTP GET request to the given 'uri_path'
  # @param [String] :uri_path A relative path component of a URI, or reference URI.path
  # @param [String] :version The version of the Vault key/value secrets engine (v1)
  # @retrun [Hash] A hash containing the secret key/value pairs.
  @uri_path = uri_path
  request = Net::HTTP::Get.new(uri_path)
  @headers.each do |key, value|
    request[key] = value
  end
  response = http.request(request)
  err_check(response)
  parse_response(response, version)
end

#get_ca_file(ca_trust) ⇒ Object

Raises:

  • (Puppet::Error)


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 192

def get_ca_file(ca_trust)
  # @summary Try known paths for trusted CA certificates when not specified
  # @param [String] :ca_trust The path to a trusted certificate authority file. If nil, some defaults are attempted.
  # @return [String] The verified file path to a trusted certificate authority file.
  ca_file = if ca_trust && File.exist?(ca_trust)
              ca_trust
            elsif File.exist?('/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem')
              '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem'
            elsif File.exist?('/etc/ssl/certs/ca-certificates.crt')
              '/etc/ssl/certs/ca-certificates.crt'
            else
              nil
            end
  raise Puppet::Error, 'Failed to get the trusted CA certificate file.' if ca_file.nil?
  ca_file
end

#get_token(auth_path, auth_name) ⇒ Object

Raises:

  • (Puppet::Error)


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 164

def get_token(auth_path, auth_name)
  # @summary Use the Puppet host certificate and private key to authenticate to Vault
  # @param [String] :auth_path The Vault path of the "cert" authentication type for Puppet
  # @param [String] :auth_name The optional Vault named certificate role to authenticate against
  # @return [String] A Vault token.

  # Get the client certificate and private key files for Vault authenticaion
  hostcert = File.expand_path(Puppet.settings[:hostcert])
  hostprivkey = File.expand_path(Puppet.settings[:hostprivkey])
  http.cert = OpenSSL::X509::Certificate.new(File.read(hostcert))
  http.key = OpenSSL::PKey::RSA.new(File.read(hostprivkey))

  data = auth_name ? { name: auth_name } : nil

  # Submit the request to the auth_path login endpoint
  response = post("/v1/auth/#{auth_path.gsub(%r{/$}, '')}/login", data)
  err_check(response)

  # Extract the token value from the response
  begin
    token = JSON.parse(response.body)['auth']['client_token']
  rescue
    raise Puppet::Error, 'Unable to parse client_token from vault response.'
  end
  raise Puppet::Error, 'No client_token found.' if token.nil?
  token
end

#parse_response(response, version = @version) ⇒ Object

Raises:

  • (Puppet::Error)


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 106

def parse_response(response, version = @version)
  # @summary Process an HTTP response as a JSON string and return Vault secrets
  # @param [Net::HTTPResponse] :response The object body will be parsed as JSON.
  # @param [String] :version The version of the Vault key/value secrets engine in the response, either 'v1' or 'v2' (v1)
  # @return [Hash] The returned hash contains the secret key/value pairs.
  begin
    output = if version == 'v2'
               JSON.parse(response.body)['data']['data']
             else
               JSON.parse(response.body)['data']
             end
  rescue
    nil
  end
  err_message = "Failed to parse #{version} key/value data from response body: (#{@uri_path})"
  raise Puppet::Error, err_message if output.nil? && fail_hard
  Puppet.debug err_message if output.nil?
  output ||= {}
  v1_warn = "Data from '#{@uri_path}' was requested as key/value v2, but may be v1 or just be empty."
  Puppet.debug v1_warn if @version == 'v2' &&  output.empty?
  v2_warn = "Data from '#{@uri_path}' appears to be key/value v2, but was requested as v1"
  Puppet.debug v2_warn if @version == 'v1' &&  output.dig('data') && output.dig('metadata')
  output
end

#post(uri_path = @uri.path, data = {}) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/puppet_x/vault_secrets/vaultsession.rb', line 146

def post(uri_path = @uri.path, data = {})
  # @summary Submit an http post request to the given 'uri_path'
  # @param [String] :uri_path A relative path component of a URI, or reference to a URI.path.
  # @param [Hash] :data A hash of values to submit with the HTTP POST request.
  # return [Net::HTTPResponse]
  @uri_path = uri_path
  request = Net::HTTP::Post.new(uri_path)
  # This function may be called before instance variable is defined as part of initialize
  @headers ||=  {}
  @headers.each do |key, value|
    request[key] = value
  end
  request.body = data.to_json
  response = http.request(request)
  err_check(response)
  response
end