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
2
Answers
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:This pre-signed request can be passed to the API, and the API can issue the request (via HTTP
POST
in the case ofsts: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, andcurl
can be used to validate it.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:
Your application needs its callers to prove that they possess a particular set of AWS credentials without revealing those credentials to you. The normal way to solve this problem is for the caller to reveal a one-way function of those credentials to you, which you can then calculate yourself and compare. However, that won’t work in this case because you, unlike AWS, don’t have access to your user’s credentials.
A design that might work for you is the following. Create a Lambda function that accepts calls by your AWS principals. When called by an authenticated principal, it generates a temporary token containing a timestamp and the caller’s AWS identity, signs the token using a key known to your non-AWS application, then returns the token to the caller. The caller then provides that token when it calls your non-AWS application. Upon receipt of a token, your non-AWS application verifies that the signature is valid and that the token has not expired; if so, then the application can trust that the caller has that AWS identity.
Upon further reflection, this is essentially a simplified SAML. There is probably a way to set up a SAML provider, or something similar, within AWS that you could use. I just don’t know off hand what that is.