skip to Main Content

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:

Sequence diagram showing interactions between User, API, and AWS. Repeated in the code block below the image.

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


  1. Chosen as BEST ANSWER

    At work the following technique was shared with me, hence answering my own question.

    Sequence diagram described below in code.

    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/[email protected]</Arn>
        <UserId>AROAXYZ123EXAMPLE:[email protected]</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
    

  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search