Help us understand the problem. What is going on with this article?

AWSサーバレスアーキテクチャでCloudFrontからWAFをかけてAPI Gatewayを呼ぶ

More than 3 years have passed since last update.

AWSサーバレスアーキテクチャで
不正なパラメータを含んでいるリクエスト(XSS、SQLインジェクションなど)のチェックをかけたいという要望があったので
WAF(Web Application Firewall)をかけて呼び出ししてみます。
WAFをかけるためには必然的にCloudFrontを利用することになります。
構図としては下記のような構図になります。
(この図では省略していますが、Cognito User Poolを導入することでさらにユーザ認証を導入することも可能です)

API認可WAF.png

また、CloudFrontは署名付きURL(ワンタイムURL)を利用する以外に外部から直接アクセスを防ぐ方法がないため、
認可API経由でワンタイムURLを払い出しています。
さらにCloudFront経由でAPI Gateway呼び出しをするため、
かなり複雑な構成、設定になります。

ワンタイム払い出し

Cognito+IAMで/onetime APIにAPI認可をかけます。
CognitoでのAPI認可に関しては下記を参考にしてください。
世界に先駆けてAWSサーバレスアーキテクチャでユーザ認証とAPI認可の実装をしてみた

IAMロールに次のように/onetime APIの認可をかけます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "execute-api:Invoke",
                "cognito-sync:*",
                "cognito-identity:*"
            ],
            "Resource": [
                "arn:aws:execute-api:us-east-1:(アカウント情報)/*/POST/onetime"
            ]
        }
    ]
}

API Gatewayの設定

/onetime APIにはIAM認証をかけます。
apigateway1.png

それ以外のAPIに関してはAPIキーでのアクセス制限をかけます。
APIGateway2.png

CloudFront経由からの呼び出しだと署名情報が壊れてしまうのでIAMでの認証ができませんでした
(回避策ご存知の方いれば教えてください)

APIキーの払い出し

APIキー項目よりAPIキーを作成します。
作成後、APIのステージに紐付けをします。
APIキーはこの後、CloudFront側で設定します。
APIキーはAPI呼び出しに必要な認証情報なので外部に知られないようにしてください

APIGateway3.png

CloudFront側から呼び出すステージのアドレスを控えておきます。
stage.png

CloudFrontの設定

ここが一番苦労しました。(設定項目が多すぎる・・・)

WebのDistributionを作成します。
CloudFront1.png

Origin Domain Nameには先ほど控えたAPI Gatewayのアドレスを指定します。(https除外、ステージ名の前まで)
Origin Pathにはステージのパスを設定します。
Origin Protocol PolicyはCloudFront→API Gatewayはhttps経由なのでhttps Onlyにします。
Origin Custom Headersはx-api-keyヘッダーを付与し、先ほど取得したAPIキーを設定します。
CloudFront2.png

Viewer Protocol PolicyはCloudFrontへのアクセス時のプロトコルですHttps Onlyにします。
Allower HTTP Methodsは通信を許可するリクエストメソッドです。
Forward HeaderをWhitelistにし、Originを追加します。これはCloudFront経由のアクセスでもCORSを許可するためです。
(ブラウザからのリクエスト時はCloudFront経由でAPI GatewayのOPTIONSメソッドにアクセスします。)
Object CachingはCustomizeにし、
Minimum TTL、Maximum TTL、Default TTLを1にし、キャッシュを無効にします。
(キャッシュがあるとセッションなどの挙動がおかしくなるため)
Forward CookiesはAllにし、クッキー情報をロストしないようにします。
Forward Query StringsもYesにし、リクエストパラメータをロストしないようにします。
Restrict Viewer AccessをYesにし、署名付きのURL以外CloudFrontに直接アクセスできないようにします。
(署名URLは/onetime APIにて払い出しします。)
CloudFront3.png

AWS WAF Domain Namesは後でWAF側から設定します。まずはNoneでCreate Distributionします。
CloudFront4.png

WAFの設定

フィルターのかけ方は色々あるので下記参考にしてください
「AWS WAF」を導入してみた。- IP addresses編
「AWS WAF」を導入してみた。- SQL injection編
「AWS WAF」を導入してみた。- String matching編

署名付きURLの払い出しAPI実装

参考にさせてもらいました
CloudFront+S3で署名付きURLでプライベートコンテンツを配信する

ルートアカウントの管理画面右上から「Security Credential」を選択します。
accesskey1.jpg
[Your Security Credentials]画面が表示されますので、[CloudFront Key Pairs]を選択します。
accesskey2.jpg
プライベートキーをダウンロードします。
accesskey4.jpg
アクセスキーを控えます。
accesskey5.jpg

/onetime APIのLambdaは次のようにCloudFrontの署名付きURLを払い出しします。
aws-cloudfront-signモジュールを使っています。

'use strict';

var cf = require('aws-cloudfront-sign');
var options = {
    keypairId: (アクセスキー),
    privateKeyString: (プライベートキー)
};


module.exports.handler = function (event, context, cb) {


    if(event.param === void 0){
        // パラメータなしはクーロンで呼ばれた場合(常にスタンバイ状態にする)
        return cb(null, {
            "cron": "stanby call"
        });
    }

    var url = event.param.url;
    var signedUrl = cf.getSignedUrl(url, options);
    console.log('Signed URL: ' + signedUrl);

    return cb(null, {
        url: signedUrl
    });
};

クライアント側サンプルコード(Javascript、iOS、Android)

下記にアップロードしました。
https://github.com/teradonburi/CognitoCloudFrontWafAPIGateway

/onetime APIを経由しているだけで前回とあまりかわりません。
違う点だけピックアップしていきます。

Javascript

お使いの環境のIDを設定してください

login.html
 ///////////////// Config /////////////////////////
 var identityPoolId = 'us-east-1:<indentity-pool-id>';
 var userPoolId = 'us-east-1_<user-pool-id>';
 var clientId = '<user-pool-crient-id>';
 var apiGateway = 'https://<stage-id>.execute-api.us-east-1.amazonaws.com/<stage>';
 var cloudFront = 'https://<cloudfront-id>.cloudfront.net';
 var api = "<api-path>";
 //////////////////////////////////////////////////

まず/onetime APIでCloudfrontのワンタイムAPIを払い出します。
払い出したワンタイムAPIをパラメータをつけてリクエストします。
API Gateway SDKのapigClient.jsは呼び出しURLを変更できるように改良してあります。

login.html
 function callAPIGateway(api, token) {
            var apigClient = apigClientFactory.newClient({
                endpoint: apiGateway,
                accessKey: token.AccessKeyId,
                secretKey: token.SecretKey,
                sessionToken: token.SessionToken,
                region: 'us-east-1' // OPTIONAL: The region where the API is deployed, by default this parameter is set to us-east-1
            });

            var param = {};

            var body = {
                param: {
                    url: cloudFront + api
                }
            };

            apigClient.doPost('/onetime', param, body)
                .then(function (result) {
                    console.log("result:", result);



                    var cloudFrontClient = apigClientFactory.newClient({
                        endpoint: cloudFront,
                        region: 'us-east-1' // OPTIONAL: The region where the API is deployed, by default this parameter is set to us-east-1
                    });

                    var param = {};

                    // JSON送信データ
                    var body = {

                    };

                    return cloudFrontClient.doPost(api + '?' + result.data.url.split('?')[1], param, body);

                })
                .then(function (result) {
                    console.log("result:", result);
                })
                .catch(function (err) {
                    console.log("err:", err);
                });
        }

iOS

お使いの環境のIDを設定してください

/////////////////// Config ////////////////////////////////

// Cognito User Pool
let ClientId = "<user-pool-client-id>" // Cognito User Pool Client Id
let ClientSecret = "<user-pool-secret>" // Cognito User Pool Client Secret
let PoolId = "us-east-1_<user-pool-id>" // Cognito User Pool Id
// Congnito Identity Pool
let IdentityId = "us-east-1:<identity-pool-id>" // Cognito Identity Pool Id
// API Gateway
let EndPoint = "https://<apigateway-id>.execute-api.us-east-1.amazonaws.com/<stage>"
let CloudFront = "https://<cloudfront-id>.cloudfront.net"
let api = "<api path>"
///////////////////////////////////////////////////////////

認証付きで
API Gatewayでの/onetime API呼び出し
およびCloudFront用にクライアントをそれぞれ作成します。

ViewController.swift
// Cognito identityPoolIdによるAPI認可
let configuration = AWSServiceConfiguration(region: .USEast1, credentialsProvider: credentialsProvider)
ServerlessClient.registerClientWithConfiguration(configuration,forKey: "Auth",endpoint:self.EndPoint)
ServerlessClient.registerClientWithConfiguration(configuration,forKey: "CloudFront",endpoint:self.CloudFront)

self.callAPI(ServerlessClient(forKey: "CloudFront"),client: ServerlessClient(forKey: "Auth"))

/onetime APIを呼び出した後、
CloudfrontのURLにエンドポイントを設定したクライアントを呼び出します。

ViewController.swift
func callAPI(onetime:ServerlessClient,client:ServerlessClient){

        let path = CloudFront + self.api
        client.onetimeUrl(path).continueWithBlock { (task: AWSTask!) -> AnyObject! in

            var url:String
            ///////////////////////API認可されたAPIの呼び出し///////////////////////////
            if (task.error != nil) {
                print(task.error)
                return nil
            } else {
                print(task.result)
                url = (task.result as! NSDictionary)["url"] as! String
            }

            // JSON送信データ
            let param = [
                "": ""
            ]

            let queryParam = ServerlessClient.dictionaryFromQueryString(url)


            onetime.Post(self.api,param: param,queryParam:queryParam).continueWithBlock { (task: AWSTask!) -> AnyObject! in

                if (task.error != nil) {
                    print(task.error)

                } else {
                    print(task.result)

                }
                return nil
            }

            return nil
        }

}

Android

認証付きで
API Gatewayでの/onetime API呼び出し
およびCloudFront用にクライアントをそれぞれ作成します。
Cloudfront経由での呼び出しはAPI Gateway SDKが対応していないため、okhttpを利用して直接リクエストしてます。

MainActivity.java
 //////////////////////
 // Config Param
 //////////////////////
    private static final String userPoolId = "us-east-1_<userpool-id>";
    private static final String clientId = "<userpool-client-id>";
    private static final String clientSecret = "<userpool-client-secret>";
    private static final String identityPoolId = "us-east-1:<identity-pool-id>";
    private static final String EndPoint = "https://<apigateway-id>.execute-api.us-east-1.amazonaws.com/<stage>";
    private static final String CloudFront = "https://<cloudfront-id>.cloudfront.net";
    private static final String api = "<api path>";

build.gradleにて
AWS SDKおよびokhttpをインストールします。

build.gradle
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.3.0'
    compile 'com.amazonaws:aws-android-sdk-core:2.2.17+'
    compile 'com.amazonaws:aws-android-sdk-cognito:2.2.17+'
    compile 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.2.17+'
    compile 'com.amazonaws:aws-android-sdk-apigateway-core:2.2.17+'
    compile 'com.squareup.okhttp3:okhttp:3.3.1'
}

リクエストのパラメータを入れ子にするためには
インナークラスにしてgsonにてjson化します。

MainActivity.java
 ////////////////////////////////////////////

 class RequestData {

    public class Param {
            public class Data{
                String name;
                String type;
            }

            Data data = new Data();
    }

    Param param = new Param();
}

class ResponseData {
        class Data{
            String app_name;
            String app_type;
        }

        Data data;
        Integer status_cd;
}


private void callAPI(final CognitoCredentialsProvider credentialsProvider){

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                ApiClientFactory factory = new ApiClientFactory();

                if(credentialsProvider != null){
                    factory.credentialsProvider(credentialsProvider);
                }

                // Build
                final OneTimeClient client = factory.build(OneTimeClient.class);


                OneTimeRequestData onetimeRequest = new OneTimeRequestData();
                onetimeRequest.param.url = CloudFront + api;
                OneTimeResponseData onetimeUrl = client.Post(onetimeRequest);


                Gson gson = new Gson();
                RequestData apiRequest = new RequestData();
                apiRequest.param.data.name = "user";
                apiRequest.param.data.type = "primium";


                String url = onetimeUrl.url;
                String requestJSON = gson.toJson(apiRequest);
                try {
                    Request request =  new Request.Builder()
                            .url(url)
                            .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), requestJSON))
                            .build();

                    Response response = new OkHttpClient().newCall(request).execute();
                    if (response.isSuccessful()){
                        ResponseData res = gson.fromJson(response.body().string(),ResponseData.class);
                        Log.i("200", res.toString());
                    }
                }catch(IOException e){
                    Log.e("500", "error:" + e.getMessage(), e);
                }

                return null;
            }


        }.execute();

}

teradonburi
気管支喘息を患って死にかけです。 いつ死ぬかわかりません。 成人喘息は誰でもなりえるものだし、 咳喘息から気管支喘息に進行すると慢性的な死の危険があるものです。 自身や周りで数週間咳が続いてる人がいたら気をつけて・・・ Twitterフォローいただけたらフォロバします。 https://twitter.com/teradonburi
meetsmore
プロを探せる見積りプラットフォーム「ミツモア」の開発・運営
https://meetsmore.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away