Skip to content

Commit

Permalink
Merge pull request #161 from maschwenk/feature.add-ec2-iam-auth
Browse files Browse the repository at this point in the history
Add IAM EC2 auth
  • Loading branch information
evanphx authored Feb 27, 2018
2 parents 4780e89 + 63a54f7 commit ed2c063
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 0 deletions.
70 changes: 70 additions & 0 deletions lib/vault/api/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,60 @@ def aws_ec2(role, pkcs7, nonce = nil)
return secret
end

# Authenticate via AWS IAM auth method by providing a AWS CredentialProvider (either ECS, AssumeRole, etc.)
# If authentication is successful, the resulting token will be stored on the client and used
# for future requests.
#
# @example
# Vault.auth.aws_iam("dev-role-iam", Aws::AssumeRoleCredentials.new, "vault.example.com", "https://sts.us-east-2.amazonaws.com") #=> #<Vault::Secret lease_id="">
#
# @param [String] role
# @param [CredentialProvider] credentials_provider
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CredentialProvider.html
# @param [String] iam_auth_header_value optional
# As of Jan 2018, Vault will accept ANY or NO header if none is configured by the Vault server admin
# @param [String] sts_endpoint optional
# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html
# @return [Secret]
def aws_iam(role, credentials_provider, iam_auth_header_value = nil, sts_endpoint = 'https://sts.amazonaws.com')
require "aws-sigv4"
require "base64"

request_body = 'Action=GetCallerIdentity&Version=2011-06-15'
request_method = 'POST'

vault_headers = {
'User-Agent' => Vault::Client::USER_AGENT,
'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8'
}

vault_headers['X-Vault-AWS-IAM-Server-ID'] = iam_auth_header_value if iam_auth_header_value

sig4_headers = Aws::Sigv4::Signer.new(
service: 'sts',
region: region_from_sts_endpoint(sts_endpoint),
credentials_provider: credentials_provider
).sign_request(
http_method: request_method,
url: sts_endpoint,
headers: vault_headers,
body: request_body
).headers

payload = {
role: role,
iam_http_request_method: request_method,
iam_request_url: Base64.strict_encode64(sts_endpoint),
iam_request_headers: Base64.strict_encode64(vault_headers.merge(sig4_headers).to_json),
iam_request_body: Base64.strict_encode64(request_body)
}

json = client.post('/v1/auth/aws/login', JSON.fast_generate(payload))
secret = Secret.decode(json)
client.token = secret.auth.client_token
return secret
end

# Authenticate via a TLS authentication method. If authentication is
# successful, the resulting token will be stored on the client and used
# for future requests.
Expand Down Expand Up @@ -215,5 +269,21 @@ def tls(pem = nil, path = 'cert')
client.token = secret.auth.client_token
return secret
end

private

# Parse an AWS region from a STS endpoint
# STS in the China (Beijing) region (cn-north-1) is sts.cn-north-1.amazonaws.com.cn
# Take care changing below regex with that edge case in mind
#
# @param [String] sts_endpoint
# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html
#
# @return [String] aws region
def region_from_sts_endpoint(sts_endpoint)
valid_sts_endpoint = %r{https:\/\/sts\.?(.*).amazonaws.com}.match(sts_endpoint)
raise "Unable to parse STS endpoint #{sts_endpoint}" unless valid_sts_endpoint
valid_sts_endpoint[1].empty? ? 'us-east-1' : valid_sts_endpoint[1]
end
end
end
47 changes: 47 additions & 0 deletions spec/integration/api/auth_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "spec_helper"
require "aws-sigv4"

module Vault
describe Auth do
Expand Down Expand Up @@ -211,5 +212,51 @@ module Vault
}.to_not change { subject.token }
end
end

describe "#aws_iam", vault: "> 0.7.3" do
before(:context) do
vault_test_client.sys.enable_auth("aws", "aws", nil)
vault_test_client.post("/v1/auth/aws/config/client", JSON.fast_generate("iam_server_id_header_value" => "iam_header_canary"))
end

after(:context) do
vault_test_client.sys.disable_auth("aws")
end

let!(:old_token) { subject.token }
let(:credentials_provider) do
double(
credentials:
double(access_key_id: 'very', secret_access_key: 'secure', session_token: 'thing')
)
end
let(:secret) { double(auth: double(client_token: 'a great token')) }

after do
subject.token = old_token
end

it "does not authenticate if iam_server_id_header_value does not match" do
expect(::Aws::Sigv4::Signer).to(
receive(:new).with(
service: 'sts', region: 'cn-north-1', credentials_provider: credentials_provider
).and_call_original
)
expect do
subject.auth.aws_iam('a_rolename', credentials_provider, 'mismatched_iam_header', 'https://sts.cn-north-1.amazonaws.com.cn')
end.to raise_error(Vault::HTTPClientError, /expected iam_header_canary but got mismatched_iam_header/)
end

it "authenticates and saves the token on the client" do
expect(subject).to receive(:post).and_return 'huzzah!'
expect(Secret).to receive(:decode).and_return secret
expect(::Aws::Sigv4::Signer).to(
receive(:new).with(
service: 'sts', region: 'cn-north-1', credentials_provider: credentials_provider
).and_call_original
)
subject.auth.aws_iam('a_rolename', credentials_provider, 'iam_header_canary', 'https://sts.cn-north-1.amazonaws.com.cn')
end
end
end
end
30 changes: 30 additions & 0 deletions spec/unit/auth_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "spec_helper"

module Vault
describe Authenticate do
let(:auth) { Authenticate.new(client: nil) }
describe "#region_from_sts_endpoint" do
subject { auth.send(:region_from_sts_endpoint, sts_endpoint) }

context 'with a china endpoint' do
let(:sts_endpoint) { "https://sts.cn-north-1.amazonaws.com.cn" }
it { is_expected.to eq 'cn-north-1' }
end

context 'with a GovCloud endpoint' do
let(:sts_endpoint) { "https://sts.us-gov-west-1.amazonaws.com" }
it { is_expected.to eq 'us-gov-west-1' }
end

context 'with no regional endpoint' do
let(:sts_endpoint) { "https://sts.amazonaws.com" }
it { is_expected.to eq 'us-east-1' }
end

context 'with a malformed url' do
let(:sts_endpoint) { "https:sts.amazonaws.com" }
it { expect { subject }.to raise_exception(StandardError, "Unable to parse STS endpoint https:sts.amazonaws.com") }
end
end
end
end
2 changes: 2 additions & 0 deletions vault.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_runtime_dependency "aws-sigv4"

spec.add_development_dependency "bundler"
spec.add_development_dependency "pry"
spec.add_development_dependency "rake", "~> 12.0"
Expand Down

0 comments on commit ed2c063

Please sign in to comment.