Class: PuppetX::VaultLookup::Lookup

Inherits:
Object
  • Object
show all
Defined in:
lib/puppet_x/vault_lookup/lookup.rb

Overview

Internal class for looking up data from Vault.

Class Method Summary collapse

Class Method Details

.append_api_errors(message, response) ⇒ Object



192
193
194
195
196
197
198
199
200
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 192

def self.append_api_errors(message, response)
  errors   = json_parse(response, 'errors')
  warnings = json_parse(response, 'warnings')
  # Can't modify frozen String, so we copy.
  copy = message.dup
  copy << " (api errors: #{errors})" if errors
  copy << " (api warnings: #{warnings})" if warnings
  copy
end

.auth_login_body(cert_role) ⇒ Object



111
112
113
114
115
116
117
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 111

def self.(cert_role)
  if cert_role
    { name: cert_role }.to_json
  else
    ''
  end
end

.ensure_trailing_slash(path) ⇒ Object



144
145
146
147
148
149
150
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 144

def self.ensure_trailing_slash(path)
  if path.end_with?('/')
    path
  else
    "#{path}/"
  end
end

.get_approle_auth_token(client, vault_addr, path_segment, role_id, secret_id, namespace) ⇒ Object



159
160
161
162
163
164
165
166
167
168
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 159

def self.get_approle_auth_token(client, vault_addr, path_segment, role_id, secret_id, namespace)
  vault_request_data = {
    role_id: role_id,
    secret_id: secret_id
  }.to_json

  segment = ensure_trailing_slash(path_segment)
   = vault_addr + segment + 'login' # rubocop:disable Style/StringConcatenation
  get_token(client, , vault_request_data, namespace)
end

.get_cert_auth_token(client, vault_addr, cert_path_segment, cert_role, namespace) ⇒ Object



152
153
154
155
156
157
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 152

def self.get_cert_auth_token(client, vault_addr, cert_path_segment, cert_role, namespace)
  role_data = (cert_role)
  segment = ensure_trailing_slash(cert_path_segment)
   = vault_addr + segment + 'login' # rubocop:disable Style/StringConcatenation
  get_token(client, , role_data, namespace)
end

.get_secret(client:, uri:, token:, namespace:, key:) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 119

def self.get_secret(client:, uri:, token:, namespace:, key:)
  headers = { 'X-Vault-Token' => token, 'X-Vault-Namespace' => namespace }.delete_if { |_key, value| value.nil? }
  secret_response = client.get(uri,
                               headers: headers,
                               options: { include_system_store: true })
  unless secret_response.success?
    message = "Received #{secret_response.code} response code from vault at #{uri} for secret lookup"
    raise Puppet::Error, append_api_errors(message, secret_response)
  end
  begin
    json_data = JSON.parse(secret_response.body)
    if key.nil? && json_data['data'].key?('data')
      json_data['data']['data']
    elsif key.nil?
      json_data['data']
    elsif json_data['data'].key?('data')
      json_data['data']['data'][key]
    else
      json_data['data'][key]
    end
  rescue StandardError
    raise Puppet::Error, 'Error parsing json secret data from vault response'
  end
end

.get_token(client, login_url, request_data, namespace) ⇒ Object

Raises:

  • (Puppet::Error)


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_lookup/lookup.rb', line 170

def self.get_token(client, , request_data, namespace)
  headers = { 'Content-Type' => 'application/json', 'X-Vault-Namespace' => namespace }.delete_if { |_key, value| value.nil? }
  response = client.post(,
                         request_data,
                         headers: headers,
                         options: { include_system_store: true })
  unless response.success?
    message = "Received #{response.code} response code from vault at #{} for authentication"
    raise Puppet::Error, append_api_errors(message, response)
  end

  begin
    token = JSON.parse(response.body)['auth']['client_token']
  rescue StandardError
    raise Puppet::Error, 'Unable to parse client_token from vault response'
  end

  raise Puppet::Error, 'No client_token found' if token.nil?

  token
end

.json_parse(response, field) ⇒ Object



202
203
204
205
206
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 202

def self.json_parse(response, field)
  JSON.parse(response.body)[field]
rescue StandardError
  nil
end

.lookup(cache:, path:, vault_addr: nil, cert_path_segment: nil, cert_role: nil, namespace: nil, auth_namespace: nil, field: nil, auth_method: nil, role_id: nil, secret_id: nil, approle_path_segment: nil, agent_sink_file: nil) ⇒ Object

Raises:

  • (Puppet::Error)


9
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 9

def self.lookup(cache:,
                path:,
                vault_addr: nil,
                cert_path_segment: nil,
                cert_role: nil,
                namespace: nil,
                auth_namespace: nil,
                field: nil,
                auth_method: nil,
                role_id: nil,
                secret_id: nil,
                approle_path_segment: nil,
                agent_sink_file: nil)

  if vault_addr.nil?
    Puppet.debug 'No Vault address was set on function, defaulting to value from VAULT_ADDR env value'
    vault_addr = ENV.fetch('VAULT_ADDR', nil)
    raise Puppet::Error, 'No vault_addr given and VAULT_ADDR env variable not set' if vault_addr.nil?
  end

  if namespace.nil?
    Puppet.debug 'No Vault namespace was set on function, defaulting to value from VAULT_NAMESPACE env value'
    namespace = ENV.fetch('VAULT_NAMESPACE', nil)
  end

  # Check the cache.
  # The path, vault_addr, and namepsace fields could result in a different
  # secret value, so use them for the cache key.
  cache_key = [path, vault_addr, namespace, field]
  cache_hash = cache.retrieve(self)
  prior_result = cache_hash[cache_key]
  unless prior_result.nil?
    Puppet.debug "Using cached result for #{path}: #{prior_result}"
    return prior_result
  end

  auth_method = ENV.fetch('VAULT_AUTH_METHOD', 'cert') if auth_method.nil?
  auth_namespace = namespace if auth_namespace.nil?
  role_id = ENV.fetch('VAULT_ROLE_ID', nil) if role_id.nil?
  secret_id = ENV.fetch('VAULT_SECRET_ID', nil) if secret_id.nil?
  cert_path_segment = 'v1/auth/cert/' if cert_path_segment.nil?
  approle_path_segment = 'v1/auth/approle/' if approle_path_segment.nil?

  vault_base_uri = URI(vault_addr)
  # URI is used here to parse the vault_addr into a host string
  # and port; it's possible to generate a URI::Generic when a scheme
  # is not defined, so double check here to make sure at least
  # host is defined.
  raise Puppet::Error, "Unable to parse a hostname from #{vault_addr}" unless vault_base_uri.hostname

  client = Puppet.runtime[:http]

  case auth_method
  when 'cert'
    token = get_cert_auth_token(client,
                                vault_base_uri,
                                cert_path_segment,
                                cert_role,
                                auth_namespace)
  when 'approle'
    raise Puppet::Error, 'role_id and VAULT_ROLE_ID are both nil' if role_id.nil?
    raise Puppet::Error, 'secret_id and VAULT_SECRET_ID are both nil' if secret_id.nil?

    token = get_approle_auth_token(client,
                                   vault_base_uri,
                                   approle_path_segment,
                                   role_id,
                                   secret_id,
                                   auth_namespace)
  when 'agent'
    # Setting the token to nil causes the 'X-Vault-Token' header to not be
    # added by this function when making requests to Vault. Instead, we're
    # relying on the local Vault agent's cache to add the token into the
    # headers of our request. This assumes that 'use_auto_auth_token = true'
    # is in the Vault agent's cache config.
    # @see https://developer.hashicorp.com/vault/docs/agent/caching#using-auto-auth-token
    token = nil
  when 'agent_sink'
    # This assumes the token is availble in a sink file populated by the Vault Agent.
    # @see https://developer.hashicorp.com/vault/docs/agent/autoauth/sinks/file
    if agent_sink_file.nil?
      Puppet.debug "No agent sink file was set on function, defaulting to VAULT_AGENT_SINK_FILE env var: #{ENV.fetch('VAULT_AGENT_SINK_FILE', nil)}"
      agent_sink_file = ENV.fetch('VAULT_AGENT_SINK_FILE', nil)
    end
    raise Puppet::Error, 'agent_sink_file must be defined when using the agent_sink auth method' if agent_sink_file.nil?

    token = read_token_from_sink(sink: agent_sink_file)
  end

  secret_uri = vault_base_uri + "/v1/#{path.delete_prefix('/')}"
  data = get_secret(client: client,
                    uri: secret_uri,
                    token: token,
                    namespace: namespace,
                    key: field)

  sensitive_data = Puppet::Pops::Types::PSensitiveType::Sensitive.new(data)
  Puppet.debug "Caching found data for #{path}"
  cache_hash[cache_key] = sensitive_data
  sensitive_data
end

.read_token_from_sink(sink:) ⇒ Object

Raises:

  • (Puppet::Error)


208
209
210
211
212
# File 'lib/puppet_x/vault_lookup/lookup.rb', line 208

def self.read_token_from_sink(sink:)
  raise Puppet::Error, "The agent_sink_file does not exist or is not readable: #{sink}" unless Puppet::FileSystem.exist?(sink)

  Puppet::FileSystem.read(sink).chomp
end