diff options
author | Tim Smith <tsmith@chef.io> | 2021-09-08 10:58:45 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-08 10:58:45 -0700 |
commit | 65700a53f6f180a66be4a575c22a76da95139a4d (patch) | |
tree | 3d176b9b1103c18b1f95ee0e05aaea5696139fb6 | |
parent | e6914064b4913f238936ec8854e69bb711ab7b6e (diff) | |
parent | 99ae3b1f37e1e0be822fea5a3a3196c338f1c844 (diff) | |
download | chef-65700a53f6f180a66be4a575c22a76da95139a4d.tar.gz |
Merge pull request #12008 from chef/mp/11963
Update HashiCorp Vault fetcher to support token auth
-rw-r--r-- | lib/chef/secret_fetcher/hashi_vault.rb | 54 | ||||
-rw-r--r-- | spec/unit/secret_fetcher/hashi_vault_spec.rb | 57 |
2 files changed, 87 insertions, 24 deletions
diff --git a/lib/chef/secret_fetcher/hashi_vault.rb b/lib/chef/secret_fetcher/hashi_vault.rb index be975fc34f..7cca57542f 100644 --- a/lib/chef/secret_fetcher/hashi_vault.rb +++ b/lib/chef/secret_fetcher/hashi_vault.rb @@ -19,7 +19,6 @@ require_relative "base" require "aws-sdk-core" # Support for aws instance profile auth require "vault" - class Chef class SecretFetcher # == Chef::SecretFetcher::HashiVault @@ -29,32 +28,60 @@ class Chef # In this initial iteration the only supported authentication is IAM role-based # # Required config: + # :auth_method - one of :iam_role, :token. default: :iam_role # :vault_addr - the address of a running Vault instance, eg https://vault.example.com:8200 - # If not explicitly provided, the environment variable VAULT_ADDR will be used. - # :role_name - the name of the role in Vault that was created to support authentication - # via IAM. See the Vault documentation for details[1]. A Terraform example is also available[2] + # + # For `:token` auth: `:token` - a Vault token valid for authentication. + # + # For `:iam_role`: `:role_name` - the name of the role in Vault that was created + # to support authentication via IAM. See the Vault documentation for details[1]. + # A Terraform example is also available[2] + # # # [1] https://www.vaultproject.io/docs/auth/aws#recommended-vault-iam-policy # [2] https://registry.terraform.io/modules/hashicorp/vault/aws/latest/examples/vault-iam-auth # an IAM principal ARN bound to it. # + # Optional config + # :namespace - the namespace under which secrets are kept. Only supported in with Vault Enterprise + # # @example # # fetcher = SecretFetcher.for_service(:hashi_vault, { role_name: "testing-role", vault_addr: https://localhost:8200}, run_context ) # fetcher.fetch("secretkey1") + # + # @example + # + # fetcher = SecretFetcher.for_service(:hashi_vault, { auth_method: :token, token: "s.1234abcdef", vault_addr: https://localhost:8200}, run_context ) + # fetcher.fetch("secretkey1") + SUPPORTED_AUTH_TYPES = %i{iam_role token}.freeze class HashiVault < Base + + # Validate and authenticate the current session using the configurated auth strategy and parameters def validate! - if config[:role_name].nil? - raise Chef::Exceptions::Secret::ConfigurationInvalid.new("You must provide the authenticating Vault role name in the configuration as :role_name ") - end if config[:vault_addr].nil? raise Chef::Exceptions::Secret::ConfigurationInvalid.new("You must provide the Vault address in the configuration as :vault_addr") end - # Note that the token here is cached internal to the Vault implementation. - Vault.auth.aws_iam(config[:role_name], - Aws::InstanceProfileCredentials.new, - config[:vault_addr] || ENV["VAULT_ADDR"]) + Vault.address = config[:vault_addr] + Vault.namespace = config[:namespace] unless config[:namespace].nil? + + case config[:auth_method] + when :token + if config[:token].nil? + raise Chef::Exceptions::Secret::ConfigurationInvalid.new("You must provide the token in the configuration as :token") + end + + Vault.auth.token(config[:token]) + when :iam_role, nil + if config[:role_name].nil? + raise Chef::Exceptions::Secret::ConfigurationInvalid.new("You must provide the authenticating Vault role name in the configuration as :role_name") + end + + Vault.auth.aws_iam(config[:role_name], Aws::InstanceProfileCredentials.new) + else + raise Chef::Exceptions::Secret::ConfigurationInvalid.new("Invalid :auth_method provided. You gave #{config[:auth_method]}, expected one of :#{SUPPORTED_AUTH_TYPES.join(", :")} ") + end end # @param identifier [String] Identifier of the secret to be fetched, which should @@ -62,7 +89,10 @@ class Chef # @param _version [String] not used in this implementation # @return [Hash] containing key/value pairs stored at the location given in 'identifier' def do_fetch(identifier, _version) - Vault.logical.read(identifier).data + result = Vault.logical.read(identifier) + raise Chef::Exceptions::Secret::FetchFailed.new("No secret found at #{identifier}. Check to ensure that there is a secrets engine configured for that path") if result.nil? + + result.data end end end diff --git a/spec/unit/secret_fetcher/hashi_vault_spec.rb b/spec/unit/secret_fetcher/hashi_vault_spec.rb index db93a051e4..e69c397c17 100644 --- a/spec/unit/secret_fetcher/hashi_vault_spec.rb +++ b/spec/unit/secret_fetcher/hashi_vault_spec.rb @@ -15,7 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# require_relative "../../spec_helper" require "chef/secret_fetcher/hashi_vault" @@ -24,23 +23,57 @@ describe Chef::SecretFetcher::HashiVault do let(:node) { {} } let(:run_context) { double("run_context", node: node) } - context "when validating HashiVault provided configuration" do - it "raises ConfigurationInvalid when the role_name is not provided" do - fetcher = Chef::SecretFetcher::HashiVault.new( { vault_addr: "vault.example.com" }, run_context) - expect { fetcher.validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) + context "when validating provided HashiVault configuration" do + it "raises ConfigurationInvalid when the :auth_method is not valid" do + fetcher = Chef::SecretFetcher::HashiVault.new( { auth_method: :invalid, vault_addr: "https://vault.example.com:8200" }, run_context) + expect { fetcher.validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid, /:auth_method/) end it "raises ConfigurationInvalid when the vault_addr is not provided" do - fetcher = Chef::SecretFetcher::HashiVault.new( { role_name: "vault.example.com" }, run_context) + fetcher = Chef::SecretFetcher::HashiVault.new( { auth_method: :iam_role, role_name: "example-role" }, run_context) expect { fetcher.validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) end - it "obtains a token via AWS IAM auth to allow the gem to do its own validations when all required config is provided" do - fetcher = Chef::SecretFetcher::HashiVault.new( { vault_addr: "vault.example.com", role_name: "example-role" }, run_context) - auth_stub = - allow(Aws::InstanceProfileCredentials).to receive(:new).and_return double("credentials") - allow(Vault).to receive(:auth).and_return(instance_double(Vault::Authenticate, aws_iam: nil)) - fetcher.validate! + context "and using auth_method: :iam_role" do + it "raises ConfigurationInvalid when the role_name is not provided" do + fetcher = Chef::SecretFetcher::HashiVault.new( { auth_method: :iam_role, vault_addr: "https://vault.example.com:8200" }, run_context) + expect { fetcher.validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) + end + + it "obtains a token via AWS IAM auth to allow the gem to do its own validations when all required config is provided" do + fetcher = Chef::SecretFetcher::HashiVault.new( { auth_method: :iam_role, vault_addr: "https://vault.example.com:8200", role_name: "example-role" }, run_context) + allow(Aws::InstanceProfileCredentials).to receive(:new).and_return instance_double(Aws::InstanceProfileCredentials) + auth_double = instance_double(Vault::Authenticate) + expect(auth_double).to receive(:aws_iam) + allow(Vault).to receive(:auth).and_return(auth_double) + fetcher.validate! + end + end + + context "and using auth_method: :token" do + it "raises ConfigurationInvalid when no token is provided" do + fetcher = Chef::SecretFetcher::HashiVault.new( { auth_method: :token, vault_addr: "https://vault.example.com:8200" }, run_context) + expect { fetcher.validate! }.to raise_error(Chef::Exceptions::Secret::ConfigurationInvalid) + end + + it "authenticates using the token during validation when all configuration is correct" do + fetcher = Chef::SecretFetcher::HashiVault.new( { auth_method: :token, token: "t.1234abcd", vault_addr: "https://vault.example.com:8200" }, run_context) + auth = instance_double(Vault::Authenticate) + auth_double = instance_double(Vault::Authenticate) + expect(auth_double).to receive(:token) + allow(Vault).to receive(:auth).and_return(auth_double) + fetcher.validate! + end + end + end + + context "when fetching a secret from Hashi Vault" do + it "raises an FetchFailed message when no secret is returned due to invalid engine path" do + fetcher = Chef::SecretFetcher::HashiVault.new( { auth_method: :invalid, vault_addr: "https://vault.example.com:8200" }, run_context) + logical_double = instance_double(Vault::Logical) + expect(logical_double).to receive(:read).and_return nil + expect(Vault).to receive(:logical).and_return(logical_double) + expect { fetcher.do_fetch("anything", nil) }.to raise_error(Chef::Exceptions::Secret::FetchFailed) end end end |