class Chef::SecretFetcher::AzureKeyVault

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”)

Public Instance Methods

do_fetch(name, version) click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 36
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
validate!() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 61
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 [config_object_id, client_id, mi_res_id].count { |x| !x.nil? } > 1
end

Private Instance Methods

api_version() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 82
def api_version
  "2018-02-01"
end
client_id() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 94
def client_id
  config[:client_id]
end
config_object_id() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 90
def config_object_id
  config[:object_id]
end
fetch_token() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 114
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 /identity not found/i.match?(body["error_description"])
  else
    body = JSON.parse(response.body)
    body["access_token"]
  end
end
mi_res_id() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 98
def mi_res_id
  config[:mi_res_id]
end
resolve_vault_and_secret_name(name) click to toggle source

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`. @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

# File lib/chef/secret_fetcher/azure_key_vault.rb, line 72
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
resource() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 86
def resource
  "https://vault.azure.net"
end
token_query() click to toggle source
# File lib/chef/secret_fetcher/azure_key_vault.rb, line 102
def token_query
  @token_query ||= begin
    p = {}
    p["api-version"] = api_version
    p["resource"] = resource
    p["object_id"] = config_object_id if config_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