1
2
3
4
5
6
7
8
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
require_relative "base"
require_relative "../exceptions"
require "json" unless defined?(JSON)
require "net/http" unless defined?(Net::HTTP)
require "uri" unless defined?(URI)
class Chef
class SecretFetcher
# == Chef::SecretFetcher::AzureKeyVault
# A fetcher that fetches a secret from Azure Key Vault. Supports fetching with version.
#
# In this initial iteration this authenticates via token obtained from the OAuth2 /token
# endpoint.
#
# Validation of required configuration (vault name) is not performed until
# `fetch` time, to allow for embedding the vault name in with the secret
# name, such as "my_vault/secretkey1".
#
# @example
#
# fetcher = SecretFetcher.for_service(:azure_key_vault, { vault: "my_vault" }, run_context)
# fetcher.fetch("secretkey1", "v1")
#
# @example
#
# fetcher = SecretFetcher.for_service(:azure_key_vault, {}, run_context)
# fetcher.fetch("my_vault/secretkey1", "v1")
#
# @example
#
# fetcher = SecretFetcher.for_service(:azure_key_vault, { client_id: "540d76b6-7f76-456c-b68b-ccae4dc9d99d" }, run_context)
# fetcher.fetch("my_vault/secretkey1", "v1")
#
class AzureKeyVault < Base
def do_fetch(name, version)
token = fetch_token
vault, name = resolve_vault_and_secret_name(name)
if vault.nil?
raise Chef::Exceptions::Secret::ConfigurationInvalid.new("You must provide a vault name to fetcher options as vault: 'vault_name' or in the secret name as 'vault_name/secret_name'")
end
# Note that `version` is optional after the final `/`. If nil/"", the latest secret version will be fetched.
secret_uri = URI.parse("https://#{vault}.vault.azure.net/secrets/#{name}/#{version}?api-version=7.2")
http = Net::HTTP.new(secret_uri.host, secret_uri.port)
http.use_ssl = true
response = http.get(secret_uri, { "Authorization" => "Bearer #{token}",
"Content-Type" => "application/json" })
# If an exception is not raised, we can be reasonably confident of the
# shape of the result.
result = JSON.parse(response.body)
if result.key? "value"
result["value"]
else
raise Chef::Exceptions::Secret::FetchFailed.new("#{result["error"]["code"]}: #{result["error"]["message"]}")
end
end
def validate!
raise Chef::Exceptions::Secret::ConfigurationInvalid, "You may only specify one (these are mutually exclusive): :object_id, :client_id, or :mi_res_id" if [object_id, client_id, mi_res_id].count { |x| !x.nil? } > 1
end
private
# Determine the vault name and secret name from the provided name.
# If it is not in the provided name in the form "vault_name/secret_name"
# it will determine the vault name from `config[:vault]`.
# @param name [String] the secret name or vault and secret name in the form "vault_name/secret_name"
# @return Array[String, String] vault and secret name respectively
def resolve_vault_and_secret_name(name)
# We support a simplified approach where the vault name is not passed i
# into configuration, but
if name.include?("/")
name.split("/", 2)
else
[config[:vault], name]
end
end
def api_version
"2018-02-01"
end
def resource
"https://vault.azure.net"
end
def object_id
config[:object_id]
end
def client_id
config[:client_id]
end
def mi_res_id
config[:mi_res_id]
end
def token_query
@token_query ||= begin
p = {}
p["api-version"] = api_version
p["resource"] = resource
p["object_id"] = object_id if object_id
p["client_id"] = client_id if client_id
p["mi_res_id"] = mi_res_id if mi_res_id
URI.encode_www_form(p)
end
end
def fetch_token
token_uri = URI.parse("http://169.254.169.254/metadata/identity/oauth2/token")
token_uri.query = token_query
http = Net::HTTP.new(token_uri.host, token_uri.port)
response = http.get(token_uri, { "Metadata" => "true" })
case response
when Net::HTTPSuccess
body = JSON.parse(response.body)
body["access_token"]
when Net::HTTPBadRequest
body = JSON.parse(response.body)
raise Chef::Exceptions::Secret::Azure::IdentityNotFound if body["error_description"] =~ /identity not found/i
else
body = JSON.parse(response.body)
body["access_token"]
end
end
end
end
end
|