AWS App RunnerのWAFを試してその裏側を予想してみた

2023.02.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

しばたです。

先日 AWS App RunnerにおいてAWS WAFとの連携がサポートされました。
基本的な機能に関しては下の記事で解説されていますのでぜひご覧ください。

私個人としてはこの更新に対して「AWS WAFはApp Runner基盤のどこに実装されているんだろうか?もう少し裏側の実装が知りたい。」という疑問があり、今日までずっと調べていたのですが明確な答えを得ることはできませんでした...

本記事ではAWS WAF連携を色々な条件で試してみた結果と本日時点での公開情報をベースにAWS WAFの実装を予想してみます。

検証環境

いつも私が使っている東京リージョンの検証環境で試しています。

この環境に別途用意したAWS WAF Web ACLを連携させています。

Web ACLの設定はシンプルに私の自宅IP以外からのアクセスをブロックするルールだけ設けています。

AWS WAF連携は一時停止中でも可能

App Runnerのほとんどの設定は稼働中の場合のみ変更可能で一時停止中は変更できないのですが、AWS WAFとの連携・連携解除はApp Runnerが一時停止中の場合でも可能です。

加えて、ドキュメントによれば、App RunnerとAWS WAFを連携させる際に

When you create a web ACL, a small amount of time passes before the web ACL fully propagates and is available to App Runner. The propagation time can be from a few seconds to a number of minutes.

と設定の伝播(数秒から数分程度)を要することと、連携解除の際は

You can disassociate AWS WAF web ACl that you no longer need by updating your App Runner service.

とApp Runnerサービスの更新は不要であることが記載されています。

プライベートアクセス時でもAWS WAFは利用可能

次に、AWS WAFとの連携はVPC Endpointを使いプライベートアクセスを行う場合でも有効でした。

プライベートアクセスの場合でもWAFのルールが評価され不正なアクセスはブロックされます。

注意 : Source IPは意図したものにならない

ただし、WAFのログを確認したところSource IPが意図したIP(VPC内部のIP)とはならず謎のIPからアクセスしている体となっていました。
このためIP制限のルールは意図した動作をしないのでご注意ください。
プライベートアクセスでIP制限をする場合はAWS WAFではなくVPC EndpointのENIにアタッチするセキュリティグループで行う様にしてください。

この謎のSource IPについて裏どりはできませんでしたが、恐らくはVPC Endpointの裏側にあるAWS Managedな内部NLBのIPだと推測されます。

一応プライベートアクセス時のWAFログはこんな感じです。

アクセス元情報

# VPC内のEC2 (10.0.11.172) からVPC Endpoint (10.0.21.76) へアクセスしたログを取得しています
$ hostname -i
10.0.11.172
$ curl -v https://meiyy25y7i.ap-northeast-1.awsapprunner.com
*   Trying 10.0.21.76:443...
* Connected to meiyy25y7i.ap-northeast-1.awsapprunner.com (10.0.21.76) port 443 (#0)

# ・・・中略・・・

* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< date: Tue, 28 Feb 2023 05:14:31 GMT
< server: envoy
< content-length: 0
<
* Connection #0 to host meiyy25y7i.ap-northeast-1.awsapprunner.com left intact

上記環境からのアクセスのためログ中のclientIp10.0.11.172を期待していたのですが、実際には10.0.3.35といった謎のIPが記録されています。
(10.0.3.35以外にも10.0.3.0/24と思しきレンジのIPが記録されていました。ただ、このIPは環境によって異なりそうです...)

プライベートアクセス時のWAFログ

{
    "timestamp": 1677561271833,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:ap-northeast-1:xxxxxxxxxxxx:regional/webacl/shiba-dev-app-runner-waf/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "terminatingRuleId": "block-from-outside-my-home",
    "terminatingRuleType": "REGULAR",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "APPRUNNER",
    "httpSourceId": "xxxxxxxxxxxx:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "ruleGroupList": [],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "10.0.3.35",
        "country": "-",
        "headers": [
            {
                "name": "Host",
                "value": "meiyy25y7i.ap-northeast-1.awsapprunner.com"
            },
            {
                "name": ":path",
                "value": "/"
            },
            {
                "name": ":method",
                "value": "GET"
            },
            {
                "name": ":scheme",
                "value": "http"
            },
            {
                "name": "User-Agent",
                "value": "curl/7.87.0"
            },
            {
                "name": "Accept",
                "value": "*/*"
            },
            {
                "name": "x-forwarded-for",
                "value": "10.0.3.35"
            },
            {
                "name": "x-forwarded-proto",
                "value": "http"
            },
            {
                "name": "x-envoy-internal",
                "value": "true"
            },
            {
                "name": "x-request-id",
                "value": "89c97e77-78f2-4d97-a36a-ee5ae2f4108d"
            }
        ],
        "uri": "/",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "GET",
        "requestId": "89c97e77-78f2-4d97-a36a-ee5ae2f4108d"
    }
}

あとはx-envoy-internalヘッダが付いており内部オリジンであることが分かるのが特徴的となっています。

比較用にパブリックアクセス時のWAFログも載せておきます。
この場合x-envoy-internalの代わりにx-envoy-external-addressヘッダが付いています。

パブリックアクセス時のWAFログ (実行環境はCloudShell)

{
    "timestamp": 1677560286847,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:ap-northeast-1:xxxxxxxxxxxx:regional/webacl/shiba-dev-app-runner-waf/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "terminatingRuleId": "block-from-outside-my-home",
    "terminatingRuleType": "REGULAR",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "APPRUNNER",
    "httpSourceId": "xxxxxxxxxxxx:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "ruleGroupList": [],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "3.113.213.242",
        "country": "JP",
        "headers": [
            {
                "name": "Host",
                "value": "meiyy25y7i.ap-northeast-1.awsapprunner.com"
            },
            {
                "name": ":path",
                "value": "/"
            },
            {
                "name": ":method",
                "value": "GET"
            },
            {
                "name": ":scheme",
                "value": "http"
            },
            {
                "name": "User-Agent",
                "value": "curl/7.87.0"
            },
            {
                "name": "Accept",
                "value": "*/*"
            },
            {
                "name": "x-forwarded-for",
                "value": "3.113.213.242"
            },
            {
                "name": "x-forwarded-proto",
                "value": "http"
            },
            {
                "name": "x-envoy-external-address",
                "value": "3.113.213.242"
            },
            {
                "name": "x-request-id",
                "value": "c6e26a44-62c0-4fa4-acfc-b3236a121de1"
            }
        ],
        "uri": "/",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "GET",
        "requestId": "c6e26a44-62c0-4fa4-acfc-b3236a121de1"
    }
}

AWS CLIからの操作

AWS CLIで操作する際はApp RunnerでなくAWS WAF側から行う様です。
App Runnerは正確にはAWS WAF v2のみ対応のためaws wafv2コマンドを使います。

# 連携
aws wafv2 associate-web-acl --web-acl-arn "Web ACLのARN" --resource-arn "App RunnerのサービスARN"

# 状況確認
aws wafv2 get-web-acl-for-resource --resource-arn "App RunnerのサービスARN"

# 連携解除
aws wafv2 disassociate-web-acl --resource-arn "App RunnerのサービスARN"

実行例)

$ aws --version
aws-cli/2.10.3 Python/3.9.11 Linux/4.14.255-291-231.527.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off

# 連携 : 特に値を返さない
$ aws wafv2 associate-web-acl --web-acl-arn arn:aws:wafv2:ap-northeast-1:xxxxxxxxxxxx:regional/webacl/shiba-dev-app-runner-waf/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
>    --resource-arn arn:aws:apprunner:ap-northeast-1:xxxxxxxxxxxx:service/my-first-app-runner/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

# 状況確認 : 設定の伝播状況は取得できない模様
$ aws wafv2 get-web-acl-for-resource --resource-arn arn:aws:apprunner:ap-northeast-1:xxxxxxxxxxxx:service/my-first-app-runner/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
{
    "WebACL": {
        "Name": "shiba-dev-app-runner-waf",
        "Id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "ARN": "arn:aws:wafv2:ap-northeast-1:xxxxxxxxxxxx:regional/webacl/shiba-dev-app-runner-waf/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "DefaultAction": {
            "Allow": {}
        },
        "Description": "",
        "Rules": [
            {
                "Name": "block-from-outside-my-home",
                "Priority": 0,
                "Statement": {
                    "NotStatement": {
                        "Statement": {
                            "IPSetReferenceStatement": {
                                "ARN": "arn:aws:wafv2:ap-northeast-1:xxxxxxxxxxxx:regional/ipset/shibata-my-home/0b030408-bd2d-4c03-a9c3-df99285c7507"
                            }
                        }
                    }
                },
                "Action": {
                    "Block": {}
                },
                "VisibilityConfig": {
                    "SampledRequestsEnabled": true,
                    "CloudWatchMetricsEnabled": true,
                    "MetricName": "allow-from-my-home"
                }
            }
        ],
        "VisibilityConfig": {
            "SampledRequestsEnabled": true,
            "CloudWatchMetricsEnabled": true,
            "MetricName": "shiba-dev-app-runner-waf"
        },
        "Capacity": 1,
        "ManagedByFirewallManager": false,
        "LabelNamespace": "awswaf:xxxxxxxxxxxx:webacl:shiba-dev-app-runner-waf:"
    }
}

# 連携解除 : 特に値を返さない
$ aws wafv2 disassociate-web-acl --resource-arn arn:aws:apprunner:ap-northeast-1:xxxxxxxxxxxx:service/my-first-app-runner/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

AWS WAF連携の実装を予想してみる

ここまでいろいろ試してきましたが、残念ながらAWS WAF連携の実装をつきとめることはできませんでした。
ここからは現在公開されている情報を元にAWS WAF連携の実装を予想していきます。

L7 Request Router

主に以下のDeep dive系の公開情報からApp Runner内部においてNLBの配下にLayer 7のRequest Routerがいることが判明しています。

(https://aws.amazon.com/jp/blogs/news/deep-dive-on-aws-app-runner-vpc-networking/ から引用)

このRequest Routerの具体的な実装や機能の詳細までは公開されていません。
現時点でわかっているのは「App Runnerサービス毎のECSタスクへのルーティングをする」点だけです。

Envoy Proxy Load Balancer

上記とは別にApp Runnerの同時実行制御にはEnvoy Proxyをロードバランサーとして使っていることが公開されています。

(https://nathanpeck.com/concurrency-compared-lambda-fargate-app-runner/files/Concurrency%20Compared.pptx から引用)

なんとなく「Envoy Proxy Load Balancer = L7 Request Router」な雰囲気はするのですが、裏どりはできず全然別物かもしれません...
Envoyが割と何でもできてしまうツールなので、もしかしたら実装としては同一のEnvoy Proxyで

  • 各テナント(サービス)へのルーティングを行う層
  • ECSタスクのロードバランスをする層

の多層構造という可能性もありそうです。
私はあまりEnvoyに詳しくないのでこの辺に詳しい方の知見が欲しい感じです。

aws-fargate-request-proxy コンテナ

以前の記事で書いたのですが、App RunnerのECSタスク内部にはaws-fargate-request-proxyと言う名前のコンテナがいることが分かっています。

このコンテナに関する詳細は一切公開されていないのですが、名前からしてネットワークプロキシでしょう。
前段にEnvoyがいるのでこいつもEnvoy Proxyのサイドカーなんじゃないかな?と勝手に予想しています。

AWS WAF連携予想図

ここまでの情報を踏まえて、私としてはAWS WAFはL7 Request RouterEnvoy Proxy Load Balancerに紐づけられているのではないかと予想しています。
プライベートアクセス時のログからNLBより内側に実装されているだろう、というのと、WAFによるレートリミット機能があることを考えるとECSタスク側よりは外側にあるだろうという点からの予想です。

ざっくり図にしてこんな感じかなぁ?と考えています。

なお、あくまでも私個人の予想図にすぎず、記載内容の正確さについては保証できませんのでご了承ください。

最後に

以上となります。

いろいろ試して調査したものの、残念ながらApp Runnerの実装をつきとめることはできませんでした。
もっとAWS公式情報が増えてくれると嬉しいですね。