Assume that I'm working in a hybrid cloud setup where some services run in and use AWS, and some not. For an API not running in AWS (i.e., not in EC2, not in Lambda, not behind AWS API Gateway), I want to implement authentication based on AWS IAM roles. To implement the authentication, the user of the API must pass some information that the service behind the API can use to validate that the user does indeed have access to the given role.
Can the API ask for anything else than the role's access key, secret access key and session token and use sts:GetCallerIdentity
with those credentials to validate them? If not, what's the security best practice for avoiding the possibility for the service to impersonate the role? Is it to get role credentials with an sts:AssumeRole
call that attaches a policy that restricts the role session to only allow sts:GetCallerIdentity
?
For reference, here's the above design:
I'm also happy to hear completely different approaches to the problem too. If substantiated, I'm open to hearing about any alternative to IAM role based authentication that I should consider instead of this approach.
Here's the diagram source code for reference:
sequenceDiagram
participant User
participant API
participant AWS
User ->>+ AWS: sts:AssumeRole (attaching policy to deny all but sts:GetCallerIdentity)
activate User
AWS ->>- User: Access key, secret access key, security token
User ->>+ API: Pass access key, secret access key, security token
API ->>+ AWS: sts:GetCallerIdentity (using the passed credentials)
alt valid credentials
AWS ->> API: role info
API ->> User: Authenticated correctly
else invalid credentials
AWS ->>- API: invalid
API ->>- User: Not authenticated
deactivate User
end
At work the following technique was shared with me, hence answering my own question.
The User themselves can create a pre-signed HTTP request to the AWS API. In theory, a pre-signed request is a combination of a HTTP method, the HTTP URL (including parameters), HTTP headers and HTTP body. In practice, for the sts:GetCallerIdentity
call it is a single string containing the HTTP URL and the necessary parameters like this:
https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAT2AYFEXAMPLE2F20230425%2Fus-east-1%2Fsts%2Faws4_request&X-Amz-Date=20230425T082324Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=FwoGZEXAMPLE%2BAdonZkEXAMPLE%2B9A7INEXAMPLE%2BqGEXAMPLE%2Bf6oc35QaKEXAMPLE%2F8Ec6EXAMPLE%2B%2FKJEXAMPLE%2B7JEXAMPLE%2FAetEXAMPLE%2BA0EXAMPLE%2BVxZG9mEXAMPLE%2BZ%2FHD5rz1jg6YuEXAMPLE%2B2&X-Amz-Signature=af0fa1a53ec29dbadc825bade56e5505ffce6b575a7112345678901234567890
This pre-signed request can be passed to the API, and the API can issue the request (via HTTP POST
in the case of sts:GetCallerIdentity
) to AWS to validate it. The process from here on is the same as proposed in the original question. If the API gets a valid response with the expected credentials from AWS, the User is authenticated. Otherwise not.
As a demo, the boto3
Python library can be used to generate the pre-signed request, and curl
can be used to validate it.
$ # This would be done by the User's client
$ presigned_request="$(python3 -c \
"import boto3; sts = boto3.client('sts'); print(sts.generate_presigned_url(ClientMethod='get_caller_identity',Params={}))")"
$ echo "$presigned_request"
https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAT2AYFEXAMPLE2F20230425%2Fus-east-1%2Fsts%2Faws4_request&X-Amz-Date=20230425T082324Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=FwoGZEXAMPLE%2BAdonZkEXAMPLE%2B9A7INEXAMPLE%2BqGEXAMPLE%2Bf6oc35QaKEXAMPLE%2F8Ec6EXAMPLE%2B%2FKJEXAMPLE%2B7JEXAMPLE%2FAetEXAMPLE%2BA0EXAMPLE%2BVxZG9mEXAMPLE%2BZ%2FHD5rz1jg6YuEXAMPLE%2B2&X-Amz-Signature=af0fa1a53ec29dbadc825bade56e5505ffce6b575a7112345678901234567890
$ # This would be done by the API and then validated
$ curl -X POST "$presigned_request"
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:sts::123456789012:assumed-role/my_role/example.user@example.com</Arn>
<UserId>AROAXYZ123EXAMPLE:boldizsar.palotas@booking.com</UserId>
<Account>123456789012</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>12345678-1234-1234-9a05-35af438697c0</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
In general, the AWS signature process is documented here: AWS Documentation: Authenticating Requests (AWS Signature Version 4).
The diagram at the top of this answer, but in code:
sequenceDiagram
participant User
participant API
participant AWS
User ->> User: Create pre-signed request to sts:GetCallerIdentity
User ->>+ API: Pass pre-signed request to sts:GetCallerIdentity
API ->>+ AWS: Issue the pre-signed sts:GetCallerIdentity request
alt valid credentials
AWS ->> API: role info
API ->> User: Authenticated correctly
else invalid credentials
AWS ->>- API: invalid
API ->>- User: Not authenticated
end