AWS App RunnerのWAFを試してその裏側を予想してみた
しばたです。
先日 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
上記環境からのアクセスのためログ中のclientIp
は10.0.11.172
を期待していたのですが、実際には10.0.3.35
といった謎のIPが記録されています。
(10.0.3.35
以外にも10.0.3.0/24
と思しきレンジのIPが記録されていました。ただ、このIPは環境によって異なりそうです...)
{
"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
ヘッダが付いています。
{
"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がいることが判明しています。
- AWS App Runner の VPC ネットワーキングに Dive Deep する
- 詳解: AWS App Runner プライベートサービス
- AWS re:Invent 2022 - A close look at AWS Fargate and AWS App Runner (CON406)
(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 Router
かEnvoy Proxy Load Balancer
に紐づけられているのではないかと予想しています。
プライベートアクセス時のログからNLBより内側に実装されているだろう、というのと、WAFによるレートリミット機能があることを考えるとECSタスク側よりは外側にあるだろうという点からの予想です。
ざっくり図にしてこんな感じかなぁ?と考えています。
なお、あくまでも私個人の予想図にすぎず、記載内容の正確さについては保証できませんのでご了承ください。
最後に
以上となります。
いろいろ試して調査したものの、残念ながらApp Runnerの実装をつきとめることはできませんでした。
もっとAWS公式情報が増えてくれると嬉しいですね。