AWS
IAM
python3
lambda
APIGateway

サービス間の認証を API Gateway + IAM で行う

API Gateway + lambda でマイクロサービスを作る時にサービス間での認証(と認可)をすることでセキュリティを強化できます。今回は Security Token Service(STS) を使って Lambda で認証情報を取得し、API Gateway の AWS_IAM の認証に利用します。

前提

  • Lambda を利用した API Gateway の AWS_IAM 認証方法について主に紹介
  • API Gateway + lambda の詳しいサービスの作り方説明しません

想定するフロー

Untitled (1).png

  1. 公開されているサービスの API Gateway にリクエスト
  2. バックエンドの Lambda が他のサービスを API Gateway を利用して呼び出す
    1. IAM の認証情報を Security Token Service(STS) で取得
    2. API Gateway へのリクエストに IAM の認証情報を用いる

今回このような流れを想定した時のサービス間認証方法について紹介します。
大切なのは認証方法の部分なので、Public に公開された API Gateway は実装しません。

Lambda の作成

ロールの作成

ここが一番のポイントです。
呼び出し側の lambda に与えるロールにアタッチする IAM ポリシーを2つ作成します。1つは 呼び出したいサービスの API Gateway へのアクセスを許可するポリシー、もうひとつは STS でロールの一時認証情報を取得するためのポリシーです。

  • API Gateway のアクセスを許可するポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "execute-api:Invoke",
                "execute-api:InvalidateCache"
            ],
            "Resource": "arn:aws:execute-api:us-east-1:account_id:restapis/*/*/*"
        }
    ]
}

Resource の書き方は「API を管理するためのアクセスの制御 - Amazon API Gateway」を参照してください。ここには呼び出し側の Lambda でアクセスを許す API Gateway の ARN を指定します。

  • STS で Role の一時認証情報を取得するためのポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::account_id:role/service-role/roleName"
        }
    ]
}

Resource には STS で一時認証情報を取得したいロールの ARN を指定します。

呼び出される側 Lambda ソースコード

def lambda_handler(event, context):
    return "Success!!"

呼び出す側 Lambda ソースコード

説明はコードの後に記述。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import os
import boto3
from aws_requests_auth.aws_auth import AWSRequestsAuth

def lambda_handler(event, context):
    client = boto3.client('sts')

    IAM_ROLE_ARN = os.environ['IAM_ROLE_ARN']
    IAM_ROLE_SESSION_NAME = 'test_session'
    REGION_NAME = 'us-east-1'

    # IAM の認証情報の取得
    response = client.assume_role(
            RoleArn=IAM_ROLE_ARN,
            RoleSessionName=IAM_ROLE_SESSION_NAME
    )
    #print("Receive credential: ", end="")
    #print(response)
    credentials=response['Credentials']

    # API のホスト名を指定
    aws_host='XXXXXXXX.execute-api.us-east-1.amazonaws.com'
    auth = AWSRequestsAuth(aws_access_key=credentials['AccessKeyId'],
                           aws_secret_access_key=credentials['SecretAccessKey'],
                           aws_host=aws_host,
                           aws_region='us-east-1',
                           aws_service='execute-api')

    # additional credential
    headers = {'x-amz-security-token':credentials['SessionToken']}

    response = requests.get('https://' + aws_host + '/prod/functionA-1', auth=auth, headers=headers)
    return "Call Function A -> code:" + str(response.status_code) + " content:" + response.text

IAM の一時的な認証情報を取得する

    client = boto3.client('sts')
    IAM_ROLE_ARN = os.environ['IAM_ROLE_ARN']
    IAM_ROLE_SESSION_NAME = 'test_session'
    REGION_NAME = 'us-east-1'

    # IAM の認証情報の取得
    response = client.assume_role(
            RoleArn=IAM_ROLE_ARN,
            RoleSessionName=IAM_ROLE_SESSION_NAME
    )

STS を使って一時認証情報を取得しています。
Lambda の環境変数を使って IAM_ROLE_ARN を取得します。IAM_ROLE_ARN には呼び出したいサービスの API Gateway を Invoke できるロールを指定します。

参考:何故 additional credential を header に入れるのか

※認証認可初心者の推測も入っています。事実と意見わけて書きます。
OAuth の認可情報 access_token を利用した OAuth 認証はセキュリティホールがある。単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる自分の解釈としては他のサイトで利用したトークンを利用してなりすましができるということだと思う。そんな経緯から今は認証の署名情報が入った id_token と認可情報 access_token を利用した OpenID Connect の利用が推奨されている。 STS で一時認証情報を一式を得て、 auth 変数に入れているのは認可情報、ヘッダーに追加しているのははなりすまし予防のセッション情報だと考えられる。サイトを見ていると header を追記していないサイトが多かったが、昔はいらなかったのかもしれない。

リクエストに署名する

    credentials=response['Credentials']

    # API のホスト名を指定
    aws_host='XXXXXXXX.execute-api.us-east-1.amazonaws.com'
    auth = AWSRequestsAuth(aws_access_key=credentials['AccessKeyId'],
                           aws_secret_access_key=credentials['SecretAccessKey'],
                           aws_host=aws_host,
                           aws_region='us-east-1',
                           aws_service='execute-api')

    # additional credential
    headers = {'x-amz-security-token':credentials['SessionToken']}

    response = requests.get('https://' + aws_host + '/prod/functionA-1', auth=auth, headers=headers)

aws_requests_auth を利用して署名を行います。
ポイントは headers で x-amz-security-token を追加しているところです。IAMの一時認証情報を利用する場合はsecreat_access_keyx-amz-security-tokenとして一緒に送らなければいけません。(REST リクエストの署名と認証 - Amazon Simple Storage Service

API Gateway の作成

AWS_IAM 有効化

IAM での認証を有効化します。AMC ではメソッドリクエストから変更できますが、実際に量が増えると CLI でやらないとかなり面倒ですね。
Screen Shot 2018-04-07 at 13.45.01.png

呼び出し側の Lambda を動かしてみる

これで準備ができたのでテストで Lambda を呼び出してみます。

Screen Shot 2018-04-07 at 13.51.28.png

無事認証され、レスポンスを受け取れました!!

アクセスできないことも確かめる(おまけ)

そもそも認証がうまく働いているの?というのを確かめるために、API Gateway を呼び出すためのポリシーを呼び出し側のロールからとってみます。

Screen Shot 2018-04-07 at 13.01.01.png

アクセスが拒否されました。AWS_IAMで認証ができていたことがわかります。

参考サイト