summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc A. Paradise <marc.paradise@gmail.com>2021-07-15 17:19:25 -0400
committerMarc A. Paradise <marc.paradise@gmail.com>2021-07-15 17:43:28 -0400
commit0d04189350f483edfbc725d9d77aee9946e4f72c (patch)
tree96b659936901123ce2f5115fd6bf6552cc428c13
parent10b6ff915c6fe24ad0708f0df8fa7514d9bca933 (diff)
downloadchef-0d04189350f483edfbc725d9d77aee9946e4f72c.tar.gz
Add experimental fetcher for Azure Key Vault
Usage in a recipe looks like this: value = secret(name: "test1", version: "v1", service: :azure_key_vault, config: { vault: "myvault" } ) Signed-off-by: Marc A. Paradise <marc.paradise@gmail.com>
-rw-r--r--lib/chef/exceptions.rb3
-rw-r--r--lib/chef/secret_fetcher.rb6
-rw-r--r--lib/chef/secret_fetcher/azure_key_vault.rb56
-rw-r--r--spec/unit/secret_fetcher/azure_key_vault_spec.rb63
-rw-r--r--spec/unit/secret_fetcher_spec.rb4
5 files changed, 129 insertions, 3 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 6a90b0cc52..dcb7146510 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -295,7 +295,7 @@ class Chef
class ConfigurationInvalid < RuntimeError; end
class FetchFailed < RuntimeError; end
class MissingSecretName < RuntimeError; end
-
+ class InvalidSecretName < RuntimeError; end
class InvalidFetcherService < RuntimeError
def initialize(given, fetcher_service_names)
super("#{given} is not a supported secrets service. Supported services are: :#{fetcher_service_names.join(" :")}")
@@ -308,6 +308,7 @@ class Chef
end
end
+ class MissingVaultName < RuntimeError; end
end
# Exception class for collecting multiple failures. Used when running
diff --git a/lib/chef/secret_fetcher.rb b/lib/chef/secret_fetcher.rb
index 6745da00f6..9b8acd3790 100644
--- a/lib/chef/secret_fetcher.rb
+++ b/lib/chef/secret_fetcher.rb
@@ -21,7 +21,7 @@ require_relative "exceptions"
class Chef
class SecretFetcher
- SECRET_FETCHERS = %i{example aws_secrets_manager}.freeze
+ SECRET_FETCHERS = %i{example aws_secrets_manager azure_key_vault}.freeze
# Returns a configured and validated instance
# of a [Chef::SecretFetcher::Base] for the given
@@ -38,6 +38,9 @@ class Chef
when :aws_secrets_manager
require_relative "secret_fetcher/aws_secrets_manager"
Chef::SecretFetcher::AWSSecretsManager.new(config)
+ when :azure_key_vault
+ require_relative "secret_fetcher/azure_key_vault"
+ Chef::SecretFetcher::AzureKeyVault.new(config)
when nil, ""
raise Chef::Exceptions::Secret::MissingFetcher.new(SECRET_FETCHERS)
else
@@ -46,7 +49,6 @@ class Chef
fetcher.validate!
fetcher
end
-
end
end
diff --git a/lib/chef/secret_fetcher/azure_key_vault.rb b/lib/chef/secret_fetcher/azure_key_vault.rb
new file mode 100644
index 0000000000..05452f0667
--- /dev/null
+++ b/lib/chef/secret_fetcher/azure_key_vault.rb
@@ -0,0 +1,56 @@
+require_relative "base"
+
+class Chef
+ class SecretFetcher
+ # == Chef::SecretFetcher::AWSSecretsManager
+ # 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.
+ #
+ # Usage Example:
+ #
+ # fetcher = SecretFetcher.for_service(:azure_key_vault)
+ # fetcher.fetch("secretkey1", "v1")
+ class AzureKeyVault < Base
+ def validate!
+ @vault = config[:vault]
+ if @vault.nil?
+ raise Chef::Exceptions::Secret::MissingVaultName.new("You must provide a vault name to service options as vault: 'vault_name'")
+ end
+ end
+
+ def do_fetch(name, version)
+ token = fetch_token
+
+ # 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 fetch_token
+ token_uri = URI.parse("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net")
+ http = Net::HTTP.new(token_uri.host, token_uri.port)
+ response = http.get(token_uri, { "Metadata" => "true"})
+ body = JSON.parse(response.body)
+ body["access_token"]
+ end
+ end
+ end
+end
+
+
+
diff --git a/spec/unit/secret_fetcher/azure_key_vault_spec.rb b/spec/unit/secret_fetcher/azure_key_vault_spec.rb
new file mode 100644
index 0000000000..61af88caad
--- /dev/null
+++ b/spec/unit/secret_fetcher/azure_key_vault_spec.rb
@@ -0,0 +1,63 @@
+
+#
+# Author:: Marc Paradise <marc@chef.io>
+# Copyright:: Copyright (c) Chef Software Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require_relative "../../spec_helper"
+require "chef/secret_fetcher"
+require "chef/secret_fetcher/azure_key_vault"
+
+describe Chef::SecretFetcher::AzureKeyVault do
+ let(:config) { { vault: "myvault" } }
+ let(:fetcher) { Chef::SecretFetcher::AzureKeyVault.new(config) }
+
+ context "when validating configuration and configuration is missing :vault" do
+ context "and configuration does not have a 'vault'" do
+ let(:config) { { } }
+ it "raises a MissingVaultError error on validate!" do
+ expect{fetcher.validate!}.to raise_error(Chef::Exceptions::Secret::MissingVaultName)
+ end
+ end
+ end
+
+ context "when performing a fetch" do
+ let(:body) { "" }
+ let(:response_mock) { double("response", body: body) }
+ let(:http_mock) { double("http", :get => response_mock, :use_ssl= => nil) }
+
+ before do
+ allow(fetcher).to receive(:fetch_token).and_return "a token"
+ allow(Net::HTTP).to receive(:new).and_return(http_mock)
+ end
+
+ context "and a valid response is received" do
+ let(:body) { '{ "value" : "my secret value" }' }
+ it "returns the expected response" do
+ expect(fetcher.fetch("value")).to eq "my secret value"
+ end
+ end
+
+ context "and an error response is received in the body" do
+ let(:body) { '{ "error" : { "code" : 404, "message" : "secret not found" } }' }
+ it "raises FetchFailed" do
+ expect{fetcher.fetch("value")}.to raise_error(Chef::Exceptions::Secret::FetchFailed)
+ end
+ end
+
+ end
+end
+
diff --git a/spec/unit/secret_fetcher_spec.rb b/spec/unit/secret_fetcher_spec.rb
index f2a1f96cbb..545176f65a 100644
--- a/spec/unit/secret_fetcher_spec.rb
+++ b/spec/unit/secret_fetcher_spec.rb
@@ -39,6 +39,10 @@ describe Chef::SecretFetcher do
Chef::SecretFetcher.for_service(:example, {})
end
+ it "resolves the Azure Key Vault fetcher without error" do
+ Chef::SecretFetcher.for_service(:azure_key_vault, vault: "invalid")
+ end
+
it "resolves the AWS fetcher without error" do
Chef::SecretFetcher.for_service(:aws_secrets_manager, region: "invalid")
end