IAM認証のAWS API GatewayにPythonからSigV4署名してアクセスするには


AWSのAPI GatewayのAPIのメソッドの設定で、IAM認証を使っている場合、APIリクエスト時に署名が必要です。

API Gatewayのメソッド設定画面はこんな感じです。英語のマネジメントコンソールだと「Authorization」、日本語だと「認可」という欄です。

デフォルトはNONEという設定になっていますが、これをAWS_IAMにするとAPI Gatewayは届いたリクエストの署名を確認するようになります。リクエストする側が署名をする必要があります。

API Gatewayが署名を確認するようになるとAPI GatewayのリソースポリシーにてIAMユーザやIAMロールで制限をかけられるようになります。

Pythonにて署名を付けてAPIリクエストするサンプルコードを書いておきます。

前提

IAMユーザのアクセスキーが ~/.aws/credentials に設定されていて、そのIAMユーザからIAMロールにスイッチするIAM権限があり、そのIAMロールでAPI Gatewayにアクセスするものとします。(~/.aws/credentials があればよいので、以下のサンプルコードはGCPのCompute Engineインスタンスで動かしました)

Pythonコード

import boto3
from botocore.awsrequest import AWSRequest
from botocore.auth import SigV4Auth
import urllib.request
import sys

endpoint_host = "xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com"
endpoint = "https://" + endpoint_host + "/stage"
path = "/hello"
url = endpoint + path

# .aws/credentials または環境変数でアクセスキーが設定されているIAMユーザのセッションを生成。
session = boto3.session.Session()
# .aws/credentials に複数のIAMユーザがある場合は profile_name を指定。
#session = boto3.session.Session(profile_name = "foo")

sts_client = session.client("sts")

# IAMロールのARNを指定して assume_role 。
# IAMロールが不要でIAMユーザのままでAPIリクエストする場合は、ここは不要。
assume_role_response = sts_client.assume_role(
    RoleArn = "arn:aws:iam::XXXXXXXXXXXX:role/ROLENAME",
    RoleSessionName = "test")

# assume_role で得られたトークンなどからIAMロールでのセッションを生成。
# IAMロールが不要でIAMユーザのままでAPIリクエストする場合は、ここは不要。
session = boto3.session.Session(
    aws_access_key_id = assume_role_response['Credentials']['AccessKeyId'],
    aws_secret_access_key = assume_role_response['Credentials']['SecretAccessKey'],
    aws_session_token = assume_role_response['Credentials']['SessionToken'])

# セッション情報からAPIリクエストに署名。
credentials = session.get_credentials()
awsreq = AWSRequest(method = "GET", url = url)
SigV4Auth(credentials, "execute-api", "ap-northeast-1").add_auth(awsreq)

# urllib.request.Request 生成。
# この4つのリクエストヘッダーが必須。
# IAMロールが不要でIAMユーザのままでAPIリクエストする場合は、X-Amz-Security-Token は不要。
req = urllib.request.Request(url, headers = {
    "Authorization": awsreq.headers['Authorization'],
    "Host": endpoint_host,
    "X-Amz-Date": awsreq.context['timestamp'],
    "X-Amz-Security-Token": assume_role_response["Credentials"]["SessionToken"]
})

# APIリクエスト実行
try:
    with urllib.request.urlopen(req) as response:
        # レスポンス出力
        sys.stdout.buffer.write(response.read())
except urllib.error.HTTPError as err:
    # 403などの場合はここに到達
    # エラーを出力
    print(err)
    # レスポンスヘッダを出力
    print(err.headers)

エラーメッセージの例

IAMユーザに assume_role する権限がないと assume_role を呼び出したところで

botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling the AssumeRole operation

という例外が発生します。

X-Amz-Security-Token が必要なのに足りてないと、403 Forbidden が返され、レスポンスヘッダに次のように書かれます。

x-amzn-ErrorType: UnrecognizedClientException

API GatewayのリソースポリシーでこのIAMロールからのリクエストを拒否していると、403 Forbidden が返され、レスポンスヘッダに次のように書かれます。

x-amzn-ErrorType: AccessDeniedException

なお、IAMロールにAPI GatewayにアクセスするIAM権限がなくても、API Gatewayのリソースポリシーで許可していれば、AccessDeniedException にはならずに正常に処理できるようです。

リンク

C#での記事も書きました。