-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds Authentication using IAM role (#586)
This PR adds IAM authentication to the core. This allows an organization to leverage a host's existing IAM roles to authenticate with Conjur.
- Loading branch information
1 parent
17848f0
commit f3f636f
Showing
6 changed files
with
250 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
require 'json' | ||
|
||
module Authentication | ||
module AuthnIam | ||
class Authenticator | ||
|
||
InvalidAWSHeaders = ::Util::ErrorClass.new( | ||
"'Invalid or Expired AWS Headers: {0}") | ||
|
||
def initialize(env:) | ||
@env = env | ||
end | ||
|
||
def valid?(input) | ||
|
||
signed_aws_headers = JSON.parse input.password # input.password is JSON holding the AWS signed headers | ||
|
||
response_hash = identity_hash(response_from_signed_request(signed_aws_headers)) | ||
trusted = response_hash != false | ||
|
||
trusted && iam_role_matches?(input.username, response_hash) | ||
|
||
end | ||
|
||
def identity_hash(response) | ||
|
||
Rails.logger.debug("AWS IAM get_caller_identity body\n#{response.body} ") | ||
|
||
if response.code < 300 | ||
Hash.from_xml(response.body) | ||
else | ||
Rails.logger.error("Verification of IAM identity failed with HTTP code: #{response.code}") | ||
false | ||
end | ||
|
||
end | ||
|
||
def iam_role_matches?(login, response_hash) | ||
|
||
is_allowed_role = false | ||
|
||
split_assumed_role = response_hash["GetCallerIdentityResponse"]["GetCallerIdentityResult"]["Arn"].split(":") | ||
|
||
# removes the last 2 parts of login to be substituted by the info from getCallerIdentity | ||
host_prefix = (login.split("/")[0..-3]).join("/") | ||
aws_role_name = split_assumed_role[5].split("/")[1] | ||
aws_account_id = response_hash["GetCallerIdentityResponse"]["GetCallerIdentityResult"]["Account"] | ||
aws_user_id = response_hash["GetCallerIdentityResponse"]["GetCallerIdentityResult"]["UserId"] | ||
host_to_match = "#{host_prefix}/#{aws_account_id}/#{aws_role_name}" | ||
|
||
Rails.logger.debug("IAM Role authentication attempt by AWS user #{aws_user_id} with host to match = #{host_to_match}") | ||
|
||
login.eql? host_to_match | ||
|
||
end | ||
|
||
def aws_signed_url | ||
return 'https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15' | ||
end | ||
|
||
def response_from_signed_request(aws_headers) | ||
|
||
Rails.logger.debug("Retrieving IAM identity") | ||
RestClient.log = Rails.logger | ||
begin | ||
RestClient.get(aws_signed_url, headers = aws_headers) | ||
rescue RestClient::ExceptionWithResponse => e | ||
Rails.logger.error("Verification of IAM identity Exception #{e.to_s}") | ||
raise InvalidAWSHeaders, e.to_s | ||
end | ||
|
||
end | ||
|
||
end | ||
|
||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
- !grant | ||
role: !group conjur/authn-iam/prod/clients | ||
member: !host /myapp/011915987442/MyApp | ||
|
||
- !grant | ||
role: !group conjur/authn-iam/prod/clients | ||
member: !host /myapp/011915987442/lambda-function-role |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
|
||
--- | ||
- !policy | ||
id: myapp | ||
body: | ||
- &variables | ||
- !variable database/username | ||
- !variable database/password | ||
|
||
# Create a group that will have permission to retrieve variables | ||
- !group secrets-users | ||
|
||
# Give the `secrets-users` group permission to retrieve variables | ||
- !permit | ||
role: !group secrets-users | ||
privilege: [ read, execute ] | ||
resource: *variables | ||
|
||
# Create a layer to hold this application's hosts | ||
- !layer | ||
|
||
# Create a host using the namespace `aws` to identify this as an AWS resource. | ||
# The host ID needs to match the AWS ARN of the role we wish to to authenticate. | ||
- !host 011915987442/MyApp | ||
- !host 011915987442/lambda-function-role | ||
|
||
# Add our host into our layer | ||
- !grant | ||
role: !layer | ||
member: !host 011915987442/MyApp | ||
|
||
# Add our host into our layer | ||
- !grant | ||
role: !layer | ||
member: !host 011915987442/lambda-function-role | ||
|
||
# Give the host in our layer permission to retrieve variables | ||
- !grant | ||
member: !layer | ||
role: !group secrets-users |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
|
||
- !policy | ||
id: conjur/authn-iam/prod | ||
body: | ||
- !webservice | ||
|
||
- !group clients | ||
|
||
- !permit | ||
role: !group clients | ||
privilege: [ read, authenticate ] | ||
resource: !webservice |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
spec/app/domain/authentication/authn_iam/authenticator_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
require 'spec_helper' | ||
|
||
RSpec.describe Authentication::AuthnIam::Authenticator do | ||
|
||
def expired_aws_headers | ||
"{\"host\":\"sts.amazonaws.com\",\"x-amz-date\":\"20180620T025910Z\","\ | ||
"\"x-amz-security-token\":\"FQoDYXdzEPv//////////wEaDHwvkDqh5pHmZNe5hSK3AzevmnHjzweG6m1in"\ | ||
"/CQ8NB7PCY0nTtWsCXLU5FsHmOoXs6KgVOu8ucghebak4b/iaDCpSprH3GPjLcNatywkUEQqX8rQKy2DoKMy7ZMHNT1ivhEn "\ | ||
"vE3HR0GPkkGGWYhLTTrQDdI5fBcb3yJ /TyrcmUuBTKXwQJmvcnDe505SPpuSZm7tdrDX5SpItMngqGcrRhCjuprpk5nPVwSQ"\ | ||
"q6usp7hJYPmu/6u9eVP3rQ TFPldhRvRxu5rcssURdrIwbjMugZQff/8XERxyxPrTkJQekcqMvvV6gexDZcBOS1JtIsKfJEXU"\ | ||
"mK3kwV4liQsUevxyanWMc4jT0tiBkDj2 nvXUFt6dejppdTTRdEtBXg5xZUrGDCQDUU9eBgydoTLGav9rWiM7bWtpP4A1m0E9"\ | ||
"LoX47FScSDkqk0Hy6Dr9jzhb4HOodlgaldTs8BNlgN9xXgACdacdPqnhaYLCgAWsaUZPKuZmdyH96F59rcrVscf456ivXTrXp"\ | ||
"t6pL1ZQyRCc04hkovErvv1L2CwEaGAc k0bvbq0pbzTftTh7 9xY3pFxbL AALoR0t2/CfhyomvoG72Cl/nvAo7 "\ | ||
"m2QU\",\"x-amz-content-sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b"\ | ||
"855\",\"authorization\":\"AWS4-HMAC-SHA256 Credential=ASIAJJTVXJS5KDKXKNPQ/20180620/us-east-1/sts/aw"\ | ||
"s4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=230fa"\ | ||
"38a232969747b77e82f6c845f63941ebde89eb2cc20ed1c6f2dbabc92b6\"}" | ||
end | ||
|
||
def valid_response | ||
double('HTTPResponse', | ||
code: 200, | ||
body: %( | ||
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"> | ||
<GetCallerIdentityResult> | ||
<Arn>arn:aws:sts::011915987442:assumed-role/MyApp/i-0a5702a5a078e1a00</Arn> | ||
<UserId>AROAIYXQMEFIAVEOFMW5Y:i-0a5702a5a078e1a00</UserId> | ||
<Account>011915987442</Account> | ||
</GetCallerIdentityResult> | ||
<ResponseMetadata> | ||
<RequestId>f555066a-7417-11e8-8ded-8daed431985e</RequestId> | ||
</ResponseMetadata> | ||
</GetCallerIdentityResponse> | ||
) | ||
) | ||
end | ||
|
||
def invalid_response | ||
double('HTTPResponse', | ||
code: 404, | ||
body: "Error" | ||
) | ||
end | ||
|
||
def valid_login | ||
"host/myapp/011915987442/MyApp" | ||
end | ||
|
||
def invalid_login | ||
"host/myapp/InvalidAccount/InvalidRole" | ||
end | ||
|
||
let (:authenticator_instance) do | ||
Authentication::AuthnIam::Authenticator.new(env:[]) | ||
end | ||
|
||
it "valid? with expired AWS headers" do | ||
subject = authenticator_instance | ||
parameters = double('AuthenticationParameters', password: expired_aws_headers) | ||
expect{subject.valid?(parameters)}.to( | ||
raise_error(Authentication::AuthnIam::Authenticator::InvalidAWSHeaders) | ||
) | ||
end | ||
|
||
it "validates identity_hash with valid response" do | ||
subject = authenticator_instance | ||
expect { subject.identity_hash(valid_response) }.to_not raise_error | ||
|
||
expect(subject.identity_hash(valid_response)).to have_key("GetCallerIdentityResponse") | ||
|
||
expected = { | ||
"GetCallerIdentityResponse" => a_hash_including( | ||
"GetCallerIdentityResult" => a_hash_including( | ||
"Arn" => "arn:aws:sts::011915987442:assumed-role/MyApp/i-0a5702a5a078e1a00", | ||
"UserId" => anything, | ||
"Account" => anything | ||
) | ||
) | ||
} | ||
|
||
expect(subject.identity_hash(valid_response)).to include(expected) | ||
expect(subject.identity_hash(valid_response)) | ||
|
||
end | ||
|
||
it "validates identity_hash with invalid response" do | ||
subject = authenticator_instance | ||
expect(subject.identity_hash(invalid_response)).to eq(false) | ||
end | ||
|
||
it "matches valid login to AWS IAM role (based on AWS response)" do | ||
subject = authenticator_instance | ||
identity_hash = subject.identity_hash(valid_response) | ||
|
||
expect(subject.iam_role_matches?(valid_login, identity_hash)).to eq(true) | ||
expect(subject.iam_role_matches?(invalid_login, identity_hash)).to eq(false) | ||
end | ||
|
||
it "fails invalid login with AWS IAM role (based on AWS response)" do | ||
subject = authenticator_instance | ||
identity_hash = subject.identity_hash(valid_response) | ||
|
||
expect(subject.iam_role_matches?(invalid_login, identity_hash)).to eq(false) | ||
end | ||
|
||
end |