AWS Security Token Service(STS)を使ってIAM認証のLambda関数を突破する

# はじめに
今回はLambda関数URLをIAM認証にして、Security Token Service(STS)で作成したIAMユーザーを利用して、Lambda関数を実行してます。

この記事はTDCソフト株式会社Advent Calendarの2日目です。

https://qiita.com/advent-calendar/2023/tdc

# Lambda関数URLとは

> 関数 URL は、Lambda 関数のための専用 HTTP エンドポイントです。関数 URL の作成と設定には、Lambda コンソールまたは Lambda API を使用します。関数 URL を作成すると、一意の URL エンドポイントが Lambda により自動的に生成されます。関数 URL を作成した後に、その URL エンドポイントが変更されることはありません。関数 URL のエンドポイントでは、次の形式を使用します。
`https://<url-id>.lambda-url.<region>.on.aws`

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-urls.html

今まではAPIGatewayやALBなどを用意して、外部からのアクセスしていましたが、関数内の専用のエンドポイントを実行するだけでLambda関数が実行できるようになりました。

また、今回はLambdaの実行時間がAPIGatewayのレスポンスのタイムアウト時間をオーバーすることが見込まれていたため、関数URLを採用することにしました。
関数URLはレスポンスのタイムアウトはLambda関数のタイムアウト値である15分までとなっています。

ただ関数URLは、現時点で「認証なし」か「IAM認証」しか選べません。
今回の実装ではクライアントから実行することを想定しており、IAM情報を持たせることができないため、Security Token Service(STS)を利用し、一時的なIAMユーザーを作成し、Lambda関数をエンドポイントから実行することにしました。

# Security Token Service(STS)とは
> AWS Security Token Service (AWS STS) を使用して、AWS リソースへのアクセスをコントロールできる一時的セキュリティ認証情報を持つ、信頼されたユーザーを作成および提供することができます。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_temp.html

Security Token Service(STS)は一時的なユーザーを作成できるサービスです。
IAMのアクセスキーとは異なり、有効期限を設定して一時的に許可できます。
また、STSへのリクエストに応じて、その都度動的に生成されるため、ユーザーに紐づいて保存されません。

# 構成
わざわざ図に示す必要もないくらいシンプルではありますが、最終的な構成は以下を目指します。

![sts記事.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/7c32b3f1-c7a2-6b62-6aff-5a0c1eab4ed8.png)

ユーザーはブラウザの静的なwebアプリから実行したいLambdaを実行します。
また、IAMユーザー発行時の認証は、記事では省きますが、実際はAPI Gateway Lambda オーソライザーを実装し、JWTのトークン検証を予定しています。

# 一時的なIAMユーザーを作成する
今回はLambdaでSTSによる一時的なIAMユーザーを作成します。
具体的にはlambdaに紐づくIAMロールを使用して、STSのAssumeRole APIを叩き、指定したロールが付与された一時的なIAMユーザーを発行します。

今回は以下の記事をとても参考にしました。

https://blog.engineer.adways.net/entry/2023/09/29/120000

https://dev.classmethod.jp/articles/iam-role-passrole-assumerole/

## 下準備
LambdaやIAMユーザーに紐づけるポリシーやロールを作成します。

まずは、Lambda関数を作成します。
なお、Lambda関数の作成方法については、省きます。

Lambda関数の作成が終わったら、IAMから先ほど作成（設定）したロールを編集していきます。
ロールに紐づいたポリシーに以下のStatementを追加します。

```json:許可ポリシー
{
    "Version": "2012-10-17"
    "Statement": [
        {
            "Action": "sts:AssumeRole",
            "Effect": "Allow",
            "Resource": "*"
        }
    ],
}
```

これで今回作成したLambdaに紐づくロールがSTSでAssumeRoleできるようになりました。

次に、一時的なIAMユーザーに付与するロールを作成します。
このロールにはLambdaの実行権限を付与します。

```json:許可ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Action": [
                "lambda:*"
            ],
            "Resource": "実行するlambdaのARN"
        }
    ]
}
```

次に、信頼関係タブから信頼ポリシーを編集します。
Principalには先ほど作成したLambda関数を実行するロールのARNを入力します。

```json:信頼ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "先ほど作成したロールのARN"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

これで一通りの準備は終わりました。

## ソースコード

実際にIAMユーザーを発行するLambda関数を実装していきます。

```python:lambda_function.py
import json
import boto3

sts = boto3.client('sts')

def lambda_handler(event, context):
    sts = boto3.client('sts')
    role = '一時的なIAMユーザーに付与するロールのARN'
    caller_info = sts.get_caller_identity()
    session = caller_info.get('Arn').split('/')[1]
    response = sts.assume_role(
        RoleArn=role,
        RoleSessionName=session
    )

    print('export AWS_ACCESS_KEY_ID=' + response.get('Credentials').get('AccessKeyId'))
    print('export AWS_SECRET_ACCESS_KEY=' + response.get('Credentials').get('SecretAccessKey'))
    print('export AWS_SESSION_TOKEN=' + response.get('Credentials').get('SessionToken'))
    print('export AWS_EXPIRATION=' + str(response.get('Credentials').get('Expiration')))

    return {
        'statusCode': 200,
        'body': json.dumps({
            "AWS_ACCESS_KEY_ID":response.get('Credentials').get('AccessKeyId'),
            "AWS_SECRET_ACCESS_KEY":response.get('Credentials').get('SecretAccessKey'),
            "AWS_SESSION_TOKEN":response.get('Credentials').get('SessionToken'),
            "AWS_EXPIRATION":str(response.get('Credentials').get('Expiration')),
        })
    }
```

これで、一時的なIAMユーザーを作成できるようになりました。

# Lambda関数 IAM認証を突破する

次にIAM認証がかかったAPIを叩くには、リクエストを署名する必要があります。
先ほど作ったLambda関数から受け取ったIAMユーザーを使用してリクエストの署名を実装しました。

>AWS SDK (「サンプルコードとライブラリ」を参照) または AWS コマンドライン (CLI) ツール を使用して API リクエストを AWS に送信する場合、SDK および CLI クライアントが指定したアクセスキーを使用してリクエストを認証するため、このセクションをスキップできます。正当な理由がない限り、常に SDK または CLI を使用することをお勧めします。
>複数の署名バージョンをサポートするリージョンでは、リクエストに手動で署名する場合、使用する署名バージョンを指定する必要があります。マルチリージョンアクセスポイントにリクエストを送信すると、SDK と CLI は Signature Version 4A の使用に自動的に切り替えます。追加の設定は不要です。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_aws-signing.html

なお、説明にもある通り、署名を自作するのは大変なので、SDKを利用して署名していきます。

## AWSの署名バージョン4とは

HTTP で送信される AWS リクエストに認証情報を追加するプロセスです。
HTTPのリクエスト情報とIAMのアクセスキー (アクセスキー ID、シークレットアクセスキー) を使用して署名します。
また、一部のリクエスト要素からリクエストのハッシュを計算し、リクエストに含まれているハッシュ値とAWS のサービス内で同様の情報を利用してでたハッシュ値を比較することで、改ざんを防ぐことができます。

## ソースコード

実際にクライアントサイドに実装していきます。
また、今回はSDKライブラリを利用するので、それぞれインストールします。
HTTP クライアントにはaxiosを使用しています。

```
npm i @smithy/protocol-http @smithy/signature-v4 @aws-crypto/sha256-js
```

```typescript:getAWS.ts
import axios from 'axios'
import { HttpRequest } from '@smithy/protocol-http'
import { SignatureV4 } from '@smithy/signature-v4'
import { Sha256 } from '@aws-crypto/sha256-js'
import { AwsCredentialIdentity, HttpRequest as IHttpRequest } from '@smithy/types'

// lambdaに渡すbody
type Body = {
  test: string
}

/**
 * AWSの署名バージョン4による署名を行います
 * @param url 実行したいlambda関数URL
 * @param body lambdaに渡すbody
 * @param IAMSecKey IAM認証情報
 * @returns 標準的なメッセージプロパティやアドレス指定情報
 */
async function getSignaturev4(url: string, body: Body, IAMSecKey: AwsCredentialIdentity) {
  const apiUrl = new URL(url)
  const json = JSON.stringify(body)
  const signatureV4 = new SignatureV4({
    service: 'lambda',
    region: 'ap-northeast-1',
    credentials: IAMSecKey,
    sha256: Sha256,
  })
  const httpRequest = new HttpRequest({
    headers: {
      'Content-Type': 'application/json',
      host: apiUrl.hostname,
    },
    hostname: apiUrl.hostname,
    method: 'POST',
    path: apiUrl.pathname,
    body: json,
  })
  const signedRequest = await signatureV4.sign(httpRequest)
  return signedRequest
}

/**
 * lambda関数URLを叩きます
 * @param url 実行したいlambda関数URL
 * @param signedRequest 署名済みリクエスト
 * @returns lambda関数の戻り値
 */
async function getLambda(url: string, signedRequest: IHttpRequest) {
  const response = await axios.post(url, signedRequest.body, {
    headers: signedRequest.headers,
  })
  return response.data
}
```

# まとめ
STSを使用してIAMユーザーを発行する際にロールやポリシー、ユーザーを気にしながら記述する必要があり、とても理解が深まりました。

また、実装序盤では別のライブラリを使用していたため、署名の検証に失敗していました。
署名部分で使用する暗号化のライブラリには複数あり、誤ったものを使用したため、失敗していたと思われます。

https://github.com/aws/aws-sdk-js-crypto-helpers/tree/master

今回はクライアントサイドだったため、`'@aws-crypto/sha256-js'`を利用することで成功しましたが、今後もう少し詳しく見ていきたいと思います。

# 参考

https://blog.engineer.adways.net/entry/2023/09/29/120000

https://dev.classmethod.jp/articles/iam-role-passrole-assumerole/