AWS Network Firewall の Route 53 解決済み IP 検証(Resolved IP validation)を試してみた

はじめに

2026年10月21日に、AWS Network Firewall が Route 53 解決済み IP 検証(Resolved IP validation)に対応しました。Network Firewall でドメインを条件にできるのは、これまで TLS の SNI や HTTP の Host ヘッダを見られる HTTPS と HTTP だけでした。この機能を使うと、SFTP や SSH のように通信の中にホスト名が出てこないプロトコルでも、宛先をドメインで絞れるようになります。

ちょうど、オンプレミスのファイアウォールで FQDN 指定にしていた SFTP のルールを、Network Firewall にどう移すかを考えていたところでした。ドキュメントを読むだけでは挙動に自信が持てなかったので、検証用の VPC を CloudFormation で作り、SFTP の通信を実際に流して確かめています。

確かめたのは次のことです。

  • SFTP(TCP/22)の pass ルールに FQDN を書いて、その宛先に接続できるか
  • 許可していない FQDN 宛ての接続は止まるか
  • クライアント側で名前解決を書き換え、許可した FQDN を別の IP に向けたら止まるか
  • CNAME で別の名前を指しているホスト名でも許可できるか
  • ポリシーの Action を ALERT にしたままで、ルール単位の判定は効くか

結果はすべて期待どおりでした。ただ、最初のデプロイでファイアウォールが作れなかったり、止まっているはずの通信がつながったように見えたりと、すんなりとはいきませんでした。設定の中身とテスト結果に加えて、検証で押さえておくべきだと感じたポイントと、本番に入れる前に考えておきたいことをまとめました。

そもそも、オンプレのファイアウォールではなぜ SFTP を FQDN で許可できるのか

最初に「Network Firewall では SFTP を FQDN で書けない」と聞いたとき、まず疑問に思ったのは、今のファイアウォールでは書けているのはなぜか、ということでした。

SFTP の通信には接続先のホスト名が入っていないので、オンプレのファイアウォールも、通信の中身を見て名前を判定しているわけではありません。多くのファイアウォール製品では、FQDN のアドレスオブジェクトを作ると、ファイアウォール自身が、自分に設定された DNS サーバにその FQDN を定期的に問い合わせます。返ってきた IP をオブジェクトに結び付けておき、ポリシーではその IP で照合します。更新の間隔は、DNS の TTL に従うものや、最短の間隔を設定で決めるものなど、製品によって違います。1つの FQDN に結び付けられる IP の数に上限がある製品もあります。

見た目は FQDN のルールでも、中身は「ファイアウォールが自分で引いた IP のリスト」で判定しているわけです。IP で照合しているので、HTTPS でも SFTP でも同じように使えます。

この方式には弱点があります。クライアントとファイアウォールが別々に名前解決するので、両者の結果がずれることがあります。DNS のラウンドロビンで問い合わせのたびに違う IP を返すサービスや、IP が頻繁に変わるクラウドのサービスだと、クライアントが引いた IP がファイアウォールのリストにまだ入っていない瞬間があり、そのときに通信が落ちます。ファイアウォールとクライアントに同じ DNS サーバを使わせるよう勧められているのは、このずれを小さくするためです。オンプレで問題が出ていなかったとしても、相手の IP がたまたま安定していただけかもしれません。

Network Firewall には、HTTP/HTTPS 以外でファイアウォール自身が FQDN を名前解決してくれる機能がありません。「SFTP は FQDN で書けない」と言われていたのはこのためです。オンプレと同じことをしようとすると、Lambda で定期的に名前解決して VPC のマネージドプレフィックスリストを更新し、ルールグループから IP セット参照で使う、といった仕組みを自分で作ることになります。この場合、同じずれの問題もそのまま持ち込みます。

Resolved IP validation は、ここの考え方が違います。ファイアウォールは自分では名前解決せず、クライアントが Route 53 Resolver から実際に受け取った答えと照合します。

  多くのファイアウォール製品の FQDN オブジェクト Network Firewall の Resolved IP validation
名前解決するのは ファイアウォール自身 クライアント(Route 53 Resolver が結果を記録する)
判定に使う IP ファイアウォールが引いた IP のリスト クライアントが実際に受け取った IP
クライアントとのずれ 起きることがある 起きない(クライアントが Route 53 Resolver で名前解決している場合)

Resolved IP validation の仕組み

VPC 内のクライアントが Route 53 Resolver(VPC の DNS)で名前解決すると、Resolver はどのドメインにどの IP を返したかを記録します。Network Firewall は、接続の最初のパケットで、宛先 IP がその記録にあるかを照合します。記録は AZ ごとに持たれていて、判定結果はその接続が続く間ずっと使われます。

照合は2つのレベルで行われます。

  ポリシーレベル ルールレベル
見ていること 宛先 IP が、何らかのドメインの名前解決結果として記録されているか(ドメインは問わない) 宛先 IP が、特定のドメインの名前解決結果として記録されているか
設定する場所 ファイアウォールポリシーの Route53ResolvedIPValidation ステートフルルールの aws_route_53_resolved_domain キーワード、またはドメインリストのターゲット種別 AWS_ROUTE_53_RESOLVED_DOMAIN
照合に失敗したとき ポリシーの Action(ALERT / DROP / REJECT)が適用される そのルールに一致しないだけ。ほかのルールか既定のアクションで扱われる

ポリシー側の設定項目は次のとおりです。

項目 内容
Action ポリシーレベルの照合に失敗した接続の扱い。ALERT はフローログに印を付けるだけで止めない(ログの有効化が必須)。DROP は黙って止め、REJECT は TCP リセットを返す
DNSFirewallRuleGroupId どの VPC の名前解決を照合に使うかを決める、Route 53 Resolver DNS Firewall のルールグループ。ルールは空でよく、照合に使う VPC に関連付ける
DNSExpiryExceptionPolicy 記録が期限切れになったあとも同じ IP への通信が続いているときの扱い(CONTINUE / DROP / REJECT)。記録は DNS の TTL より長く保持される
SourceIPRanges ポリシーレベルの照合をかける送信元(最大100件)。指定しなければファイアウォールの VPC の CIDR
BypassDestinationValidationIPRanges ポリシーレベルの照合から外す宛先(最大100件)

ドキュメントを読んでいて設計に効いてくると感じたのは、SourceIPRanges と BypassDestinationValidationIPRanges はポリシーレベルの照合にしか効かず、キーワード付きのルールはこの範囲に関係なく評価される、という一文です。ポリシーの Action を ALERT にしておけば、ほかの通信には影響を出さずに、キーワードを書いたルールだけでドメインの判定を効かせられるはずです。これはテストで確かめました。

検証環境

東京リージョンの1つの AZ に、次の構成を CloudFormation で作りました。

[クライアントサブネット]
  EC2(Amazon Linux 2023) ──名前解決──▶ Route 53 Resolver
     │                                     ├ 空の DNS Firewall ルールグループを関連付け
     │                                     └ プライベートホストゾーン(CNAME のテスト用)
     │ デフォルトルート
     ▼
[ファイアウォールサブネット]
  Network Firewall エンドポイント
     │
     ▼
  インターネットゲートウェイ(エッジルートテーブルで、戻りの通信もファイアウォールへ)
     │
     ▼
  test.rebex.net:22 / github.com:22 / www.github.com:22

クライアントには Session Manager で入ります。SSM 用のインターフェイス VPC エンドポイント(ssm / ssmmessages / ec2messages)をクライアントサブネットに置いているので、SSM の通信は VPC 内で完結し、ファイアウォールを通りません。クライアントのセキュリティグループは、インバウンドルールなし、アウトバウンドルールは TCP 22 と 443 だけにして、宛先の絞り込みは Network Firewall に任せています。クライアントとファイアウォールエンドポイントは同じ AZ です(この機能の前提です)。

接続先には、業務とは関係のない公開サーバを使いました。

接続先 用途
test.rebex.net:22 Rebex 社が公開している SFTP のテストサーバ。デモ用のアカウントが公開されていて、読み取り専用。許可する宛先として使う
github.com:22 許可しない宛先として使う。鍵交換までで、認証はしない
www.github.com:22 公開 DNS で github.com を指す CNAME。CNAME のテストに使う

第三者のサーバなので、SFTP でログインしても ls までにして、ファイルの転送や負荷のかかる操作はしていません。

ファイアウォールポリシーは Strict order で、既定のアクションは aws:drop_established と aws:alert_established にしました。ルールに合わない通信は止めて、アラートログに記録する設定です。

  FirewallPolicy:
    Type: AWS::NetworkFirewall::FirewallPolicy
    Properties:
      FirewallPolicyName: nfw-ripoc-policy
      FirewallPolicy:
        StatelessDefaultActions: [aws:forward_to_sfe]
        StatelessFragmentDefaultActions: [aws:forward_to_sfe]
        StatefulEngineOptions:
          RuleOrder: STRICT_ORDER
        StatefulDefaultActions:
          - aws:drop_established
          - aws:alert_established
        StatefulRuleGroupReferences:
          - ResourceArn: !Ref SftpRuleGroup
            Priority: 100

ルールは1本だけです。標準のステートフルルールに、aws_route_53_resolved_domain を RuleOption として書いています。値はダブルクォートで囲んだ文字列にする必要があるので、YAML では '"..."' のように二重に囲みます。パラメータ EnableResolvedDomainRule でキーワードの有無を切り替えられるようにしてありますが、その理由は次の章で書きます。

  SftpRuleGroup:
    Type: AWS::NetworkFirewall::RuleGroup
    Properties:
      RuleGroupName: nfw-ripoc-sftp-rg
      Type: STATEFUL
      Capacity: 100
      RuleGroup:
        StatefulRuleOptions:
          RuleOrder: STRICT_ORDER
        RulesSource:
          StatefulRules:
            - Action: !If [UseResolvedDomainRule, PASS, DROP]
              Header:
                Protocol: TCP
                Source: !Ref ClientSubnetCidr
                SourcePort: ANY
                Direction: FORWARD
                Destination: ANY
                DestinationPort: '22'
              RuleOptions: !If
                - UseResolvedDomainRule
                - - Keyword: aws_route_53_resolved_domain
                    Settings:
                      - !Sub '"${AllowedDomain}"'
                  - Keyword: sid
                    Settings: ['1001']
                - - Keyword: sid
                    Settings: ['1001']

Suricata 形式で書くと、次のルールと同じ意味です。

pass tcp $CLIENT_NET any -> any 22 (msg:"SFTP allowed by resolved domain"; aws_route_53_resolved_domain:"test.rebex.net"; sid:1001;)

照合の範囲を決める DNS Firewall のルールグループは、ルールなしで作って VPC に関連付けるだけです。

  DnsFirewallRuleGroup:
    Type: AWS::Route53Resolver::FirewallRuleGroup
    Properties:
      Name: nfw-ripoc-frg

  DnsFirewallRuleGroupAssociation:
    Type: AWS::Route53Resolver::FirewallRuleGroupAssociation
    Properties:
      FirewallRuleGroupId: !GetAtt DnsFirewallRuleGroup.Id
      VpcId: !Ref Vpc
      Priority: 101

クライアントの EC2 には DependsOn でこの関連付けを指定し、関連付けより前に名前解決した結果がクライアントのキャッシュに残らないようにしました。CNAME のテスト用には、プライベートホストゾーンに sftp-alias.<ゾーン名> から test.rebex.net への CNAME(TTL 60秒)を作っています。フローログとアラートログは CloudWatch Logs に出しました。ポリシーの Action を ALERT にするにはログの有効化が必須です。

ポイント1:キーワード付きのルールを先に入れると、ファイアウォールが作れない

最初は、ルールグループに aws_route_53_resolved_domain を書いた状態でスタックを作りました。ポリシー側の設定は、デプロイのあとに CLI で入れるつもりでした。すると Firewall リソースが CREATE_FAILED になり、出てきたメッセージは次のものだけでした。

Resource handler returned message: "Error occurred during operation 'Firewall failed to create'." (HandlerErrorCode: GeneralServiceException)

これでは何が悪いのか分かりません。トラブルシューティングのページを読むと、キーワード付きのルールグループがポリシーに関連付けられているのに、ポリシー側に Resolved IP validation の設定がないと、ポリシーのエラーとして ROUTE53_RESOLVED_IP_VALIDATION_MISSING が報告され、そのポリシーを使うファイアウォールのエンドポイントが ERROR の状態になる、と書かれていました。私の作り方はまさにこれです。失敗したときにエンドポイントの状態を見ていなかったので断定はできませんが、原因はこれだと考えています。

ポリシー側も CloudFormation で書ければ話は早いのですが、検証した時点では AWS::NetworkFirewall::FirewallPolicy のドキュメントに Route53ResolvedIPValidation の記載が見当たりませんでした。そこで、テンプレートにパラメータを1つ足して、3段階で作るようにしました。

  1. EnableResolvedDomainRule=false でスタックを作る。ルールはキーワードなしの drop の仮ルール
  2. CLI でポリシーに Route53ResolvedIPValidation を追加する
  3. EnableResolvedDomainRule=true でスタックを更新する。変わるのはルールグループだけ

2のポリシーの更新は、今のポリシーを取得して、jq で設定を足してから戻す形にしました。

aws network-firewall describe-firewall-policy \
  --firewall-policy-name "$POLICY_NAME" > cur.json

jq --arg frg "$FRG_ID" --arg cidr "$CLIENT_CIDR" '
  .FirewallPolicy + {
    Route53ResolvedIPValidation: {
      Action: "ALERT",
      DNSFirewallRuleGroupId: $frg,
      DNSExpiryExceptionPolicy: "CONTINUE",
      SourceIPRanges: [ { AddressDefinition: $cidr } ]
    }
  }' cur.json > new.json

aws network-firewall update-firewall-policy \
  --firewall-policy-name "$POLICY_NAME" \
  --update-token "$(jq -r .UpdateToken cur.json)" \
  --firewall-policy file://new.json

設定後のポリシーはこうなります。

{
    "Action": "ALERT",
    "DNSFirewallRuleGroupId": "rslvr-frg-xxxxxxxxxxxxxxxx",
    "DNSExpiryExceptionPolicy": "CONTINUE",
    "SourceIPRanges": [
        { "AddressDefinition": "<クライアントサブネットの CIDR>" }
    ]
}

CloudShell の AWS CLI(2.39.3)でそのまま通りました。古い CLI では Unknown parameter in FirewallPolicy: "Route53ResolvedIPValidation" と言われるので、その場合は CLI を更新するか、コンソールのファイアウォールポリシーの画面にある「Route 53 Resolved IP validation」から設定します。更新のあと、ファイアウォールは PENDING から30秒ほどで IN_SYNC になりました。反映を待つときは次のコマンドで状態を見ています。

aws network-firewall describe-firewall --firewall-name "$FW_NAME" \
  --query '[FirewallStatus.Status,FirewallStatus.ConfigurationSyncStateSummary]' --output text
# READY  IN_SYNC になれば反映済み

3のスタック更新で、CLI で入れたポリシー側の設定が消えないかは気になっていました。ルールグループしか変わらない更新では、設定は残っていました。一方で、FirewallPolicy リソースのプロパティを変える更新をすると、CloudFormation がポリシーを上書きして設定が消える可能性があります(試してはいません)。テンプレートのコメントには「スタック更新はルールのパラメータだけにする」と書いておきました。

片付けも逆の順番が要ります。先にポリシー側の設定を外すと、キーワード付きのルールだけが残って、同じエラーの状態になります。キーワード付きのルールを仮ルールに戻し、ポリシー側の設定を del(.Route53ResolvedIPValidation) で外してから、スタックを削除しました。

ファイアウォールの作成に失敗したときは、aws cloudformation deploy に --disable-rollback を付けて作り直すと、失敗したファイアウォールが残るので状態を調べられます。エンドポイントの状態は次のコマンドで見られます。最初の失敗のときにこれをしておけば、原因を断定できていました。

aws network-firewall describe-firewall --firewall-name "$FW_NAME" \
  --query 'FirewallStatus.SyncStates' --output json
# Attachment.Status と Attachment.StatusMessage に原因が出る

テストのやり方

クライアントでは、毎回 DNS のキャッシュを消してから接続しました。判定には ssh-keyscan を使い、サーバのホスト鍵が取れたら OK としています。SSH のバナーが返ったかどうかで判定していない理由は、次の「ポイント2」で書きます。

# DNS の参照先が VPC の DNS(VPC の CIDR の先頭に2を足したアドレス)になっているか
resolvectl dns

# DNS のキャッシュを消す
flush() { resolvectl flush-caches 2>/dev/null; true; }

# ホスト鍵が取れたら OK(鍵交換まで成功)、取れなければ NG
check() {
  local host="$1" port="${2:-22}" keys
  keys=$(timeout 30 ssh-keyscan -T 10 -p "$port" "$host" 2>/dev/null | grep -v '^#')
  if [ -n "$keys" ]; then
    echo "OK  ${host}:${port}(鍵交換まで成功)"
  else
    echo "NG  ${host}:${port}(鍵交換できず)"
  fi
}

ルールに書くドメインを変えるときは、CloudShell から aws cloudformation deploy でパラメータを変えてスタックを更新し、IN_SYNC になってからさらに60秒ほど待って接続しました。

aws cloudformation deploy \
  --stack-name nfw-ripoc \
  --template-file nfw-resolved-ip-poc.yaml \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides AvailabilityZone="$AZ" \
      EnableResolvedDomainRule=true AllowedDomain=www.github.com

テストの順番にも気を使いました。「まだ一度も名前解決していない名前」を使うテストは、その名前をクライアントで引く前にやらないと、記録が残って結果が混ざります。また、一度だけ、前のテストで変えたルールのドメインを戻し忘れたまま次のテストを始めてしまい、期待と違う結果が出ました。テストの前に毎回 describe-rule-group で今のルールを表示するようにしてからは、こうした取り違えはなくなりました。

ポイント2:止まっている通信が「つながった」ように見える

最初の check 関数は、SSH のバナー(SSH-2.0-... の1行)が返ってきたら OK と判定していました。この判定で github.com に接続すると、バナーが返ってきて OK になりました。許可していない宛先なので、止まらないとおかしい結果です。

ルールを書き間違えたかと思い、ルートテーブルとポリシーを見直しましたが、問題はありませんでした。github.com の 443 番に curl すると、こちらはタイムアウトします。ファイアウォールは動いています。アラートログを見ると、github.com の 22 番宛ての通信は blocked として記録されていました。ファイアウォールはちゃんと止めていたのです。

原因は、既定のアクションの Drop established と、SSH の作りの組み合わせでした。Drop established は TCP の3ウェイハンドシェイクを通し、接続が確立したあとのパケットを止めます。SSH はサーバのほうが先にバナーを送るプロトコルなので、接続した直後にサーバが送ったバナーだけはクライアントに届きます。そのあとクライアントが送る鍵交換のパケットは止まるので、SFTP としては使えません。一度も名前解決していない IP(gitlab.com の IP を別の端末で調べて直接指定)でも、同じようにバナーだけが返ってきました。

判定を鍵交換に変えてやり直すと、github.com は NG、test.rebex.net は OK になり、アラートログとも一致しました。なお、ssh-keyscan はサーバのバナーを # host:port SSH-2.0-... というコメント行で出力することがあります。この行まで数えるとバナーだけでも OK になってしまうので、上の関数ではコメント行を除いています。

これは Resolved IP validation に限らず、Drop established を既定のアクションにしたファイアウォール全般で起きる話です。Test-NetConnection や telnet、nc -z のように TCP の接続だけを確かめるツールでは、止まる通信も「接続できた」に見えます。移行の当日にこの方法で疎通確認をしていたら、つながらない宛先を OK と報告していたかもしれません。SFTP の疎通確認は、実際にログインしてみるか、少なくとも鍵交換まで確かめる必要があります。

テストの結果

# ポリシーの Action ルールに書いたドメイン 接続のしかた 期待 結果
1 ALERT test.rebex.net test.rebex.net に名前で接続 OK OK。SFTP でログインして ls まで確認
2 ALERT test.rebex.net github.com に接続 NG NG
2′ ALERT test.rebex.net 一度も名前解決していない IP に直接接続 NG NG
3 ALERT test.rebex.net /etc/hosts で test.rebex.net を github.com の IP に向けて接続 NG NG
4 ALERT test.rebex.net 直前に名前解決した test.rebex.net の IP を直接指定 OK OK
5a ALERT sftp-alias.<プライベートゾーン> 別名で接続(CNAME で test.rebex.net を指す) 不明 OK
5c-1 ALERT www.github.com github.com として名前解決して接続(www.github.com はまだ引いていない) NG NG
5c-2 ALERT www.github.com www.github.com として名前解決して接続(公開の CNAME で github.com を指す) OK OK
F-1 DROP test.rebex.net test.rebex.net に名前で接続 OK OK
F-2 DROP test.rebex.net github.com に接続 NG NG
F-3 DROP test.rebex.net テスト3と同じなりすまし NG NG

テスト3のなりすましは、クライアントで次のように実行しました。IP はその場で調べた値を変数に入れて使っています。

flush
GH_IP=$(getent ahostsv4 github.com | awk 'NR==1{print $1}')
echo "$GH_IP test.rebex.net" >> /etc/hosts
getent hosts test.rebex.net      # github.com の IP になっていること
check test.rebex.net             # NG

# 必ず元に戻す
sed -i '/ test.rebex.net$/d' /etc/hosts

結果から分かったこと

見ているのは「その名前で解決された IP か」

テスト4では、クライアントが名前を使わずに IP を直接指定しても通りました。判定は「宛先 IP が、ルールに書いたドメインの名前解決結果として記録されているか」で行われていて、クライアントがどうやって接続先を決めたかは関係ありません。

テスト3は、/etc/hosts で許可した名前を別の IP に向けたものです。SFTP の通信にはホスト名が入っていないので、ネットワーク上はテスト2(github.com の IP への接続)とまったく同じ通信になります。それでも止まったということは、hosts ファイルや別の DNS サーバなど、クライアント側で名前解決を書き換えても許可を回避できない、ということです。ファイアウォールが信じているのは、VPC の Route 53 Resolver が実際に返した答えだけです。

SFTP でこの機能を使う価値は、IP を並べて書くルールと同じくらい宛先を絞れて、しかも相手の IP が変わっても名前解決の結果に追従してくれるところにあると思います。

CNAME は、接続に使う名前を書けばよい

テスト5aと5cで、CNAME をたどる名前でも、ルールに CNAME の元の名前(クライアントが接続に使う名前)を書けば許可できることを確かめました。プライベートホストゾーンの CNAME と、公開 DNS の CNAME の両方で同じ結果です。CNAME の先の名前をルールに書く必要はありませんでした。SaaS の SFTP エンドポイントでは、ホスト名が CNAME でサービス共通の名前を指していることがよくあり、その先の名前は事業者の都合で変わります。接続に使う名前だけをルールに書けばよいのは、運用上ありがたい挙動です。

面白かったのは5cです。www.github.com は github.com を指す CNAME なので、どちらの名前で引いても返ってくる IP は同じです。ルールを www.github.com にした状態で、github.com として名前解決して接続すると止まり、www.github.com として名前解決すると通りました。宛先の IP は同じでも、ファイアウォールは「どの名前の名前解決結果として記録された IP か」を区別しています。

ただし、IP を共有している相手には届く

裏を返すと、許可した名前で一度名前解決されれば、その IP はそのルールで許可された状態になります。5c-1 を www.github.com の名前解決より前に実施したのはこのためで、順番を逆にすると、github.com という名前で接続しても通ってしまうはずです(この順番では試していません)。

これは、同じ IP を使っている別のサービスにも、その IP 宛てなら接続できるということでもあります。クラウドのストレージサービスが提供する SFTP エンドポイントのように、複数の利用者で IP を共有しているサービスでは、許可した相手以外にも届く可能性があります。ただ、IP で許可する場合も、オンプレの次世代ファイアウォールの FQDN オブジェクトでも事情は同じです。事業者が公開している IP レンジ全体を許可するのと比べれば、許可されるのは「今その名前が使っている IP」だけなので、範囲はずっと狭くなります。

ポリシーは ALERT のままでよい

テスト1〜3はポリシーの Action を ALERT にしたまま実施し、許可したドメインだけが通りました。ルールレベルの判定は、ポリシーの Action に関係なく効いています。DROP に変えたテスト F-1〜F-3 も同じ結果でした。今回のルールはもともと名前解決した IP しか通さないので、ポリシーを DROP にしても結果が変わらないのは理屈どおりです。

本番に入れるなら、ポリシーは ALERT にしておき、ドメインで絞りたい通信だけにキーワード付きのルールを書く、という形が安全だと考えています。ポリシーを DROP にすると、SourceIPRanges の送信元から「VPC の Resolver で名前解決していない宛先」への通信が、ルールに関係なくすべて止まります。IP を直接指定して接続している社内向けの通信などがあれば、BypassDestinationValidationIPRanges に入れておく必要があります。

ログとメトリクスの見方

フローログには、照合の結果が route53_resolved_ip として出ます。CloudWatch Logs Insights では次のクエリで確認しました。フローログは通信が終わってから出力されるので、テストから5分ほど待ってから見ます。

fields @timestamp, event.src_ip, event.dest_ip, event.dest_port, event.netflow.route53_resolved_ip
| filter event.dest_port = 22
| sort @timestamp desc
| limit 50

テスト1の通信では route53_resolved_ip が true になっていて、戻りの通信もファイアウォールを通っていることが確認できました。止まった通信は、アラートログのほうで見ます。

fields @timestamp, event.src_ip, event.dest_ip, event.dest_port, event.alert.action, event.alert.signature
| filter event.dest_port = 22
| sort @timestamp desc
| limit 50

フローログを眺めていて気付いたのは、インターネットからクライアントのパブリック IP の 22 番や 443 番に来ている接続(よくあるポートスキャンです)には、route53_resolved_ip のフィールド自体が出ていなかったことです。送信元が SourceIPRanges の外なので、照合の対象になっていません。ドキュメントの説明どおりの動きでした。ちなみにこれらの接続は、既定のアクションが最初の SYN を止めないのでファイアウォールは通過しますが、セキュリティグループのインバウンドルールで許可していないので、クライアントには届いていません。

ドキュメントによると、ログの route53_resolved_ip: false だけでは、記録がなかったのか、AZ が対応していなかったのかを区別できません。区別するにはメトリクスの DnsResolvedFlows、DnsUnresolvedFlows、DnsResolutionErrorFlows、DnsAzUnavailableFlows を見ます。今回はテストが期待どおりだったので、そのまま片付けに進んでしまい、DnsAzUnavailableFlows を確認していません。次の章に書く理由から、本番ではキーワード付きのルールを入れる前に、このメトリクスが0であることを必ず確かめるつもりです。

本番に入れる前に考えておきたいこと

対応していない AZ では、キーワード付きのルールが通信を止める

Resolved IP validation は、すべての AZ で使えるわけではありません。ドキュメントによると、対応していない AZ では、キーワード付きのルールを評価できず、そのルールが対象にしている通信をすべて止めます。ポリシーの Action が ALERT でも、ルールのアクションが pass でも同じです。

たとえば「クライアントから任意の宛先の22番、キーワードは特定のドメイン」というルールなら、その AZ ではクライアントからの SFTP がすべて止まります。IP で許可している SFTP の宛先があるなら、Strict order で IP のルールを先に置き、キーワード付きのルールを後ろに置いておくと、影響をキーワード付きのルールの対象だけに抑えられます。導入の順番も、ポリシーを ALERT で有効にする、DnsAzUnavailableFlows が0であることを確かめる、キーワード付きのルールを入れる、の順にするのが安全です。

使えるのはアウトバウンド通信だけ

照合されるのは、クライアントからサーバへ向かう接続の宛先 IP です。外部のシステムからこちらに接続してくるインバウンド通信では、送信元をドメインで判定することはできません。インバウンド通信の送信元は、これまでどおり IP で書くことになります。

ここで気を付けたいのは、データの向きと接続の向きが一致しないことです。SFTP でファイルを「受け取る」場合でも、こちらのサーバが相手の SFTP サーバに接続してダウンロードしているなら、接続としてはアウトバウンドです。システム構成図ではデータの流れる向きで矢印を描くことが多いので、図だけを見てインバウンド通信だと判断すると、この機能を使える通信を見落とします。ファイアウォールのルールは、誰が接続を始めるかで考える必要があります。

Transit Gateway 経由でインターネットに出る構成で使うとき

今回は1つの VPC の中で完結する構成で試しました。各 VPC の通信を Transit Gateway で Egress VPC(インターネットへの出口をまとめた VPC)に集め、そこの Network Firewall と NAT ゲートウェイを通してインターネットに出す構成で使うなら、次の点を確かめる必要があります。

  • 記録と照合は AZ 単位なので、クライアントと同じ AZ のファイアウォールエンドポイントを通る必要がある。このような構成では、クライアントの AZ の情報を運んでくれる Transit Gateway アタッチのファイアウォールが推奨されている
  • ファイアウォールが NAT ゲートウェイより前で通信を見ている必要がある。NAT の後ろにあると、送信元が NAT の IP になり、SourceIPRanges やルールの送信元でクライアントを指定できない
  • DNS Firewall のルールグループは、クライアントが名前解決する VPC に関連付ける。ファイアウォールが別のアカウントにある場合は、ファイアウォール側のアカウントからそのルールグループを参照できる必要がある(AWS RAM での共有など。今回は試していない)
  • ルールグループに関連付けた VPC の名前解決は1つのプールとして扱われ、どれかの VPC で解決された IP は全体で解決済みになる。関係のない環境とルールグループを使い回さず、ファイアウォールごとに分ける

クライアントの DNS の経路

今回のクライアントは、VPC の Route 53 Resolver で直接名前解決していました。Active Directory に参加している Windows サーバのように、DNS をドメインコントローラーに向けている環境では、ドメインコントローラーがどの VPC、どの AZ から Route 53 Resolver に問い合わせるかによって、記録が残る場所が変わる可能性があります。ここは今回検証していないので、本番の DNS の構成で別に確かめるつもりです。

共用のファイアウォールに入れるとき

ドキュメントには、Resolved IP validation の有効化や無効化、DNS Firewall のルールグループの変更で、ステートフルエンジンの再起動が必要になることがあると書かれています。再起動のとき、既存の接続はストリーム例外ポリシーの設定に従って扱われます。Action や SourceIPRanges などの変更では再起動は起きません。ほかのシステムと共用しているファイアウォールで有効にするなら、作業の時間帯を選び、SourceIPRanges は対象のサーバだけに絞っておくのがよさそうです。

ポリシー側の設定を CloudFormation で書けないうちは、IaC の外で管理する設定が1つ増えることにもなります。誰がどの手順で入れたかを残しておかないと、あとでテンプレートからポリシーを更新したときに消えてしまう恐れがあります。

おわりに

試してみて、SFTP の宛先を FQDN で絞るという用途には十分に使える機能だと感じました。CNAME の名前もそのまま書けて、クライアント側で名前解決を書き換えても回避されません。ポリシーを ALERT のままにしておけば、ほかの通信に影響を出さずに入れられます。IP が予告なく変わる SaaS の SFTP に接続するなら、Lambda で定期的に名前解決してプレフィックスリストを更新するような仕組みを作るより、クライアントが実際に受け取った答えと照合するこの機能のほうが、結果のずれが起きない分だけ確実です。一方で、相手の IP が固定か、変更の前に通知がもらえるなら、IP で書くほうが前提が少なくて済みます。

時間がかかったのは、機能そのものより、その周りでした。ポリシーとルールを作る順番を間違えるとファイアウォールが作れないこと、Drop established の環境では止まる SSH の通信もバナーまでは返ってくること。特に後者は、この機能を使うかどうかに関係なく、ファイアウォールを入れた環境で疎通確認をするときに必ず思い出したい話です。

まだ確かめていないのは、ドメインコントローラー経由の名前解決と、Transit Gateway アタッチのファイアウォールでの動き、アカウントをまたいだ構成です。本番に近い構成では、ここが一番の不確定要素になります。次はそのあたりを試すつもりです。


検証環境

  • AWS Network Firewall(Strict order、既定のアクション drop_established / alert_established)
  • Amazon Route 53 Resolver / Route 53 Resolver DNS Firewall / Route 53 プライベートホストゾーン
  • Amazon EC2(Amazon Linux 2023)/ AWS Systems Manager Session Manager
  • AWS CloudFormation / AWS CloudShell(aws-cli 2.39.3)
  • Amazon CloudWatch Logs(Logs Insights)
  • リージョン:ap-northeast-1(1 AZ)、2026年11月時点
  • 接続先:test.rebex.net(Rebex 社の公開 SFTP テストサーバ)、github.com、www.github.com

参考

 
×
タイトルとURLをコピーしました