-
Notifications
You must be signed in to change notification settings - Fork 124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow STS regional requests without 'Host' header #2827
Conversation
a22b71d
to
05a48df
Compare
@@ -54,7 +54,27 @@ def extract_relevant_data(response) | |||
|
|||
# Call to AWS STS endpoint using the provided authentication header | |||
def attempt_signed_request(signed_headers) | |||
aws_request = URI("https://#{signed_headers['host']}/?Action=GetCallerIdentity&Version=2011-06-15") | |||
sts_host = signed_headers['host'] || extract_sts_host(signed_headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend we move the signed header resolution into the extract_sts_host
method. This allows us to keep all logic related to the endpoint lookup in one place.
sts_host = signed_headers['host'] || extract_sts_host(signed_headers)
Then we push the logic for checking header value or attempting to extract the region from the credential:
# Extract AWS region from the authorization header's credential string, i.e.:
# Credential=AKIAIOSFODNN7EXAMPLE/20220830/us-east-1/sts/aws4_request
def extract_sts_host(signed_headers)
return signed_headers['host'] if signed_headers['host'].present?
region = signed_headers['authorization'].match(%r{Credential=[^/]+/[^/]+/([^/]+)/})&.captures&.first
raise(Errors::Authentication::AuthnIam::InvalidAWSHeaders, 'Failed to extract AWS region from authorization header') unless region
"sts.#{region}.amazonaws.com"
end
@@ -54,7 +54,27 @@ def extract_relevant_data(response) | |||
|
|||
# Call to AWS STS endpoint using the provided authentication header | |||
def attempt_signed_request(signed_headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like with the multiple forms of region lookup, paired with fallback validation, we should move to region extraction and STS request validation. What do you think about having extract_sts_host
return the region? Based on that response, we can build the STS url and validate it while falling back to the global endpoint if the regional validation fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this approach would you ignore the Host
header and always rely on the region from the credential string? Otherwise I don't see a benefit to adding another pattern match to extract the STS region from the Host
header when the STS url is readily available if it exists
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend something like:
def aws_call(region:, headers:)
host = if region == 'global'
'sts.amazonaws.com'
else
"sts.#{region}.amazonaws.com"
end
aws_request = URI("https://#{host}/?Action=GetCallerIdentity&Version=2011-06-15")
begin
@client.get_response(aws_request, headers)
rescue => e
# Handle any network failures with a generic verification error
raise(Errors::Authentication::AuthnIam::VerificationError, e)
end
end
# Call to AWS STS endpoint using the provided authentication header
def attempt_signed_request(signed_headers)
region = extract_sts_region(signed_headers)
# Attempt check using the discovered region
response = aws_call(region: region, headers: signed_headers)
return response if response.code.to_i == 200
# If the discovered region is `us-east-1`, also check if the request
# was made from the
if region == 'us-east-1'
response = aws_call(region: 'global', headers: signed_headers)
return response if response.code.to_i == 200
end
raise(Errors::Authentication::AuthnIam::VerificationError, 'Signature is invalid')
end
def extract_sts_region(signed_headers)
if signed_headers['host'].present?
if signed_headers['host'] == 'sts.amazonaws.com'
'global'
elsif match = signed_headers['host'].match(%r{sts.([\w\-]+).amazonaws.com})
match.captures.first
end
else
match = signed_headers['authorization'].match(%r{Credential=[^/]+/[^/]+/([^/]+)/})
return match.captures.first if match
raise(Errors::Authentication::AuthnIam::InvalidAWSHeaders, 'Failed to extract AWS region from authorization header')
end
end
This separates the following into separate methods:
- Region extraction
- Retry behavior on the global endpoint
- STS lookup
Note: the above is pseudo code, so please refactor as makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great feedback, thanks! I've incorporated this but let me know if there's anything else
3934bd4
to
d7af3d1
Compare
## [1.19.6] - 2023-07-05 | ||
|
||
### Fixed | ||
- Support Authn-IAM regional requests when host value is missing from signed headers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lists should be surrounded by blank lines
end | ||
|
||
match = host&.match(%r{sts.([\w\-]+).amazonaws.com}) | ||
return match&.captures&.first if match |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if match
makes match&
irrelevant - you're right to include it because we don't want to return anything if match == nil
, but if match
should prevent this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This applies to line 115 as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this again, and even given my suggestion:
return match.captures&.first if match
I wonder if we still need this Safe Navigation Operator.
.match
returnsnil
if there are no matches found.match
returns aMatchData
object for 1+ matches found
That means:
if match
catches cases with no matches- Otherwise, there is guaranteed to be at least one match -
match.captures
will not benil
.
We might be able to do this instead, with no change in behavior and less &
s:
return match.captures.first if match
# Attempt request using the discovered region and return immediately if successful | ||
response = aws_call(region: region, headers: signed_headers) | ||
return response if response.code.to_i == 200 | ||
|
||
# If the discovered region is `us-east-1`, fallback to the global endpoint | ||
if region == 'us-east-1' | ||
@logger.debug(LogMessages::Authentication::AuthnIam::RetryWithGlobalEndpoint.new) | ||
fallback_response = aws_call(region: 'global', headers: signed_headers) | ||
return fallback_response if fallback_response.code.to_i == 200 | ||
end | ||
|
||
# Handle any network failures with a generic verification error | ||
return response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder if in the case where region == 'us-east-1'
and fallback_response.code.to_i != 200
we want to return the fallback_response
instead of the primary response
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't too sure about this. There isn't a way to definitively know whether the regional endpoint or the global endpoint was used to generate the signed headers even after attempting both requests. AWS throws a fairly generic The request signature we calculated does not match the signature you provided
error message either way
My thinking was that the built-in retry logic should essentially be obscured from the UX so prioritize the information in original response in this case
Code Climate has analyzed commit da7dff4 and detected 8 issues on this pull request. Here's the issue category breakdown:
The test coverage on the diff in this pull request is 100.0% (50% is the threshold). This pull request will bring the total coverage in the repository to 87.1% (-1.2% change). View more on Code Climate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Desired Outcome
Currently we assume that the signed request will feature a 'Host' header telling Conjur which STS endpoint to use.
This assumption is not valid for all HTTP clients (unless explicitly added to the request), including the Golang AWS SDK. Instead we should the following prioritized order in determining which endpoint to use:
us-east-1
and the regional endpoint failed)Implemented Changes
Connected Issue/Story
CNJR-1904
Definition of Done
At least 1 todo must be completed in the sections below for the PR to be
merged.
Changelog
CHANGELOG update
Test coverage
changes, or
Documentation
README
s) were updated in this PRBehavior
Security