Squid Proxyを用いたドメインリスト制御はAWS Network Firewallで代替可能か?

はじめに

この記事はコインチェック株式会社(以下、コインチェック)のアドベントカレンダー21日目(シリーズ1)の記事です。 

https://qiita.com/advent-calendar/2024/coincheck

 

アプリケーション基盤部 SRE Gのタカハシです。

弊社では、Coincheck サービスにおける暗号資産の送金や署名実行環境等の重要な処理を行うネットワークからインターネットへのアウトバウンド通信を厳密に管理するため、Squid Proxy等を用いてドメインリスト制限を実装してきました。以前、AWS Network Firewall(以下NFW)がドメインフィルタリングに対応していると知り、よりマネージドで運用負荷が低い形で同等のセキュリティを実現できないかと検討をおこないました。

本記事では、弊社内での検討過程を参考に、Squid Proxyを用いたドメインリスト制限をNFWへ移行できるかを実際の脅威シナリオで検証した結果を紹介します。加えて、DNS Resolver Firewall、GuardDuty、Suricataなどのツールとの組み合わせによる多層防御も考察し、どこまでSquid Proxy に近づけるかを考察します。

Squid proxyとは?

Squid Proxy はL7(アプリケーション層)のHTTP/HTTPSトラフィックを理解・フィルタリングできるフォワードプロキシです。 ECS Fargate で運用することを前提として以下のような特徴が挙げられます。

  • 詳細なL7制御能力があり、URLパス、HTTPメソッド、ヘッダベースのACL、ユーザー認証に応じたアクセス制御が可能です。
  • ドメインリスト制限の強みがあり、Host:ヘッダや実際のDNS解決結果を参照し、ホワイトリスト以外のドメインへのアクセスを確実にブロックします。ホストヘッダ偽装などの単純な回避手法に極めて強いです。
  • デメリットとして、ECS Fargateでの運用では、ログ収集、パッチ適用、スケーリング、設定変更時のリロードなど運用コストが発生します。

NFWによるドメインフィルタリングとは?

AWS NFWはL3/L4が本領ですが、HTTP HostヘッダやTLS SNIに基づいたドメインフィルタリング機能も備えています。これにより、allowed.example.com以外をブロックするシンプルなFQDNホワイトリストが実現可能です。更に NFWはマネージドなのでスケール・可用性、ポリシー管理も IaCツールで行いやすいです。コスト面も要件次第ではありますが、Subnet 単位で立てられる VPC Endpoint に対する時間料金とトラフィックによる従量課金で、そこまで高額な料金設定ではないため、導入の敷居は低いです。

検証環境の構築手順と脅威シナリオ

構築ステップ概要

  1. VPCとサブネット用意
    • Private SubnetにEC2インスタンスを配置。EC2 インスタンスから外部への通信をおこなう。
  2. NFWのデプロイ
    • AWS Network Firewallを設置し、VPCエンドポイント経由で Private Subnet のアウトバウンドトラフィックをNFWにルーティングします。
    • NFWポリシーでallowed.example.comのみ許可し、他ドメインはデフォルトブロックとするドメインリストルールを設定します。
  3. 確認
    • curl -v https://allowed.example.com → 成功
    • curl -v https://randomsite.example.org → ブロックされることを確認

ここまでは理想的な挙動です。

Cloudformation サンプル

 

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  VPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: '10.0.0.0/16'
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-vpc'

  InternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-igw'

  AttachGateway:
      Type: 'AWS::EC2::VPCGatewayAttachment'
      Properties:
        VpcId: !Ref VPC
        InternetGatewayId: !Ref InternetGateway
  EIPA:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: 'vpc'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-eip-a'
  EIPC:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: 'vpc'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-eip-c'

  NatGatewayA:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt EIPA.AllocationId
      SubnetId: !Ref SubnetPublicA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-natgateway-a'

  NatGatewayC:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt EIPC.AllocationId
      SubnetId: !Ref SubnetPublicC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-natgateway-c'

  SubnetPublicA:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '10.0.0.0/19'
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref 'AWS::Region'
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-subnet-public-a'
  SubnetPublicC:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '10.0.32.0/19'
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref 'AWS::Region'
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-subnet-public-c'
  SubnetPrivateA:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '10.0.96.0/19'
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref 'AWS::Region'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-subnet-private-a'
  SubnetPrivateC:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '10.0.128.0/19'
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref 'AWS::Region'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-subnet-private-c'
  SubnetNetworkFirewallA:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '10.0.192.0/28'
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref 'AWS::Region'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-subnet-network-firewall-a'
  SubnetNetworkFirewallC:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '10.0.192.16/28'
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref 'AWS::Region'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-subnet-network-firewall-c'
  PublicRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-rt'
  PublicRoute:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref InternetGateway
  PublicSubnetRouteTableAssociationA:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref SubnetPublicA
  PublicSubnetRouteTableAssociationC:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref SubnetPublicC
  PrivateRouteTableA:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-rt-a'
  SubnetPrivateARoute:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PrivateRouteTableA
      DestinationCidrBlock: '0.0.0.0/0'
      VpcEndpointId: !Select [ 0, !Split [ ',', !Select [ 1 , !Split [ 'a:', !Join [ ',', !GetAtt NetworkFirewall.EndpointIds ] ] ] ] ]
  SubnetPrivateARouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref SubnetPrivateA
      RouteTableId: !Ref PrivateRouteTableA

  PrivateRouteTableC:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-rt-c'
  SubnetPrivateCRoute:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PrivateRouteTableC
      DestinationCidrBlock: '0.0.0.0/0'
      VpcEndpointId: !Select [ 0, !Split [ ',', !Select [ 1 , !Split [ 'c:', !Join [ ',', !GetAtt NetworkFirewall.EndpointIds ] ] ] ] ]
  SubnetPrivateCRouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref SubnetPrivateC
      RouteTableId: !Ref PrivateRouteTableC

  NetworkFirewallRouteTableA:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-network-firewall-rt-a'
  SubnetNetworkFirewallARoute:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref NetworkFirewallRouteTableA
      DestinationCidrBlock: '0.0.0.0/0'
      NatGatewayId: !Ref NatGatewayA
  SubnetNetworkFirewallARouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref SubnetNetworkFirewallA
      RouteTableId: !Ref NetworkFirewallRouteTableA
    
  NetworkFirewallRouteTableC:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-network-firewall-rt-c'
  SubnetNetworkFirewallCRoute:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref NetworkFirewallRouteTableC
      DestinationCidrBlock: '0.0.0.0/0'
      NatGatewayId: !Ref NatGatewayC
  SubnetNetworkFirewallCRouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref SubnetNetworkFirewallC
      RouteTableId: !Ref NetworkFirewallRouteTableC
    
  S3VPCEndpoint:
    Type: 'AWS::EC2::VPCEndpoint'
    Properties:
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
      VpcId: !Ref VPC
      RouteTableIds:
        - !Ref PrivateRouteTableA
        - !Ref PrivateRouteTableC
      VpcEndpointType: Gateway

  EC2MessagesVPCEndpoint:
      Type: 'AWS::EC2::VPCEndpoint'
      Properties:
        ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ec2messages'
        VpcId: !Ref VPC
        VpcEndpointType: Interface
        SubnetIds:
          - !Ref SubnetPrivateA
          - !Ref SubnetPrivateC
        SecurityGroupIds:
          - !Ref SecurityGroupForVPCEndpoints
        PrivateDnsEnabled: true

  SessionManagerVPCEndpoint:
    Type: 'AWS::EC2::VPCEndpoint'
    Properties:
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssmmessages'
      VpcId: !Ref VPC
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref SubnetPrivateA
        - !Ref SubnetPrivateC
      SecurityGroupIds:
        - !Ref SecurityGroupForVPCEndpoints
      PrivateDnsEnabled: true

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub '${AWS::StackName}-default'
      GroupDescription: ""
      VpcId: !Ref VPC
      SecurityGroupEgress:
        - IpProtocol: "-1"
          CidrIp: "0.0.0.0/0"
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-default'

  SecurityGroupForVPCEndpoints:
      Type: 'AWS::EC2::SecurityGroup'
      Properties:
        GroupName: !Sub '${AWS::StackName}-vpc-endpoints-sg'
        GroupDescription: "Security group for VPC Endpoints"
        VpcId: !Ref VPC
        SecurityGroupIngress:
          - IpProtocol: -1
            CidrIp: 10.0.0.0/16
        SecurityGroupEgress:
          - IpProtocol: -1
            CidrIp: 0.0.0.0/0
        Tags:
          - Key: Name
            Value: !Sub '${AWS::StackName}-vpc-endpoints-sg'

  NetworkFirewall:
    Type: 'AWS::NetworkFirewall::Firewall'
    Properties: 
      FirewallName: !Sub '${AWS::StackName}-network-firewall'
      FirewallPolicyArn: !Ref FirewallPolicy
      VpcId: !Ref VPC
      SubnetMappings:
        - SubnetId: !Ref SubnetNetworkFirewallA
        - SubnetId: !Ref SubnetNetworkFirewallC
      DeleteProtection: false
      Description: 'Network Firewall'
      FirewallPolicyChangeProtection: false
      SubnetChangeProtection: false
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-network-firewall'

  FirewallPolicy:
    Type: 'AWS::NetworkFirewall::FirewallPolicy'
    Properties:
      FirewallPolicyName: !Sub '${AWS::StackName}-network-firewall-policy'
      FirewallPolicy:
        StatelessDefaultActions:
          - aws:forward_to_sfe
        StatelessFragmentDefaultActions:
          - aws:forward_to_sfe
        StatelessRuleGroupReferences: []
        StatefulEngineOptions:
          RuleOrder: STRICT_ORDER
          StreamExceptionPolicy: 'DROP'
        StatefulDefaultActions:
          - aws:drop_established
          - aws:alert_established
        StatefulRuleGroupReferences:
          - ResourceArn: !Ref DomainListRuleGroup
            Priority: 100
      Description: 'Firewall policy'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-network-firewall-policy'

  DomainListRuleGroup:
    Type: 'AWS::NetworkFirewall::RuleGroup'
    Properties:
      RuleGroupName: !Sub '${AWS::StackName}-domain-list-rule-group'
      Capacity: 1000
      RuleGroup:
        RuleVariables:
          IPSets:
            HOME_NET:
              Definition:
                - "10.0.0.0/16"
        RulesSource:
          RulesSourceList:
            Targets:
              - 'allowed.example.com'
            TargetTypes:
              - TLS_SNI
              - HTTP_HOST
            GeneratedRulesType: ALLOWLIST
        StatefulRuleOptions:
          RuleOrder: STRICT_ORDER
      Type: STATEFUL
      Description: 'Domain list rule group'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-domain-list-rule-group'

 

 

攻撃シナリオ詳細

攻撃者は、malicious.example.comという不正C2サーバへEC2インスタンスから外部通信を確立し、コマンド受信やデータ漏えいを狙います。

攻撃試行1:直接アクセスをする場合

  • curl -v https://malicious.example.com
  • NFWがSNIまたはHostヘッダからmalicious.example.comであることを検知しブロックするので攻撃が失敗します。

攻撃試行2:ホストヘッダ偽装する場合

ここで、攻撃者はHost:ヘッダをallowed.example.comに書き換えつつ、実際にはmalicious.example.comのIPへ接続します。curlでは--resolveオプションを使います。

# assumed malicious.example.com -> 203.0.113.10 というIPが判明済み 
curl --resolve allowed.example.com:443:203.0.113.10 \ 
 -H "Host: allowed.example.com" \ 
 https://allowed.example.com
  • この場合、リクエスト上はHost: allowed.example.comですが、実際の接続先は203.0.113.10(malicious.example.com) となります。
  • NFWはホストヘッダによるドメイン判定ではallowed.example.comと見なしてしまい、トラフィックを通過させてしまいます。
  • 結果として、攻撃者はC2サーバと通信可能となり、フィルタ回避が成立するため、NFW単体ではホストヘッダ偽装を容易に許してしまうことが再現できます。

対策強化策とその有効性の検証

NFW 単体ではホストヘッダ偽装をすることでフィルタ回避できることがわかったので、その他AWSのマネージドサービスで対策が可能かを検討してみます。

DNS Resolver Firewallの導入

malicious.example.comをDNSレベルでブロックするため、Route 53 Resolver DNS Firewallで該当ドメインを拒否します。

  • 攻撃者がIP不明の場合、DNSクエリでブロックされ、IP取得できず攻撃が失敗します。
  • ただし、IPが事前に分かっている場合(DNS不要)には効果がなく、偽装攻撃が成功してしまいます。

GuardDutyによる検知

GuardDutyは VPC Flow logs のパケットログのIPから既知の悪性ドメインやIPとの通信を検知しますが、

  • 新規に立ち上がったC2サーバやレピュテーション不明のIPには弱いのと、
  • あくまで事後検知であり、攻撃を未然に防ぎきれないという点が挙げられます。

ログ相関やSIEMでのパターン発見は手助けしますが、根本的なブロックにはならず、NFWの弱点を補完するに留まってしまいます。

Suricata + GWLBによるL7ディープインスペクション

NFWと併用してGateway Load Balancer (GWLB)を導入し、Suricataを挿入すると、HTTPヘッダフィールドの不整合パターンを検出することも可能です。

  • Suricataルール例
  • alert http any any -> any any ( msg:"Suspicious Host Header Mismatch"; http.host; content:"allowed.example.com"; nocase; sid:10001; flow:to_server; )
  • 上記はあくまで一例で、実際にはSNIHostの整合性を突合せる高度なルールやLuaスクリプトを用いて、「SNIと接続先IPが想定と異なる」「ホスト偽装パターン」が見られたらドロップすることが可能です。
  • SuricataはL7インスペクションが可能なため、ホスト偽装による単純な回避を防ぎやすいと思います。ただし、GWLB、Suricataホスト、ルール管理といった新たな運用コスト・複雑性が発生し、Squid導入時と別観点での管理負荷が増える可能性があります。

パフォーマンス、コスト、運用性の考察

Squid Proxy に匹敵するL7精度をNFW中心で得るには、他機能を大量に組み合わせる必要があり、結果的にSquid導入と同様、またはそれ以上の複雑なアーキテクチャとなり得ることがわかります。

利用リソース

考察

Squid (Fargate)

  • L7フル機能で高度な制御
  • 運用コスト(スケール、ログ分析、パッチ対応)
  • Fargateリソースコストが継続的に発生

NFW単体

  • マネージド、L3/L4基盤強化
  • ドメイン名ベースの簡易フィルタあり
  • ホスト偽装などL7回避に弱い
  • IP直打ちで回避可能

NFW + DNS Firewall + Suricata/GWLB + GuardDuty

  • 多層防御でホスト偽装リスク軽減
  • 未知ドメイン、DNSブロック、行動分析などカバレッジ拡大
  • 組み合わせるほどコスト・複雑性増大
  • Suricataルール管理、GWLB経由のパフォーマンス考慮が必要

結論と実務的な指針

本検証で分かったこと

  • NFW単体でSquid Proxy 並みのドメインベース制御は困難で、ホストヘッダ偽装攻撃は簡単に成立し、DNS FirewallやGuardDutyでは本質的な対策になりづらい。
  • SuricataやTLSインスペクションでNFWを補強可能だが、運用コスト増となり、L7レベルの細粒度コントロールを追加ツールで達成可能だが、手軽さは失われる。

したがって、

  1. L7レベルで厳密なドメイン制御が不可欠であり、ホスト偽装攻撃も確実に潰したい場合は、Squid Proxy をFargate等で運用し続けるのが現時点では無難であることがわかります。
  2. コスト・運用負荷軽減を優先し、要件次第である程度の妥協が可能ならNFW+DNS Firewallで基本的なドメインリスト制御を行い、未知リスクはGuardDutyやSIEMで検知・対応する戦略もありかもしれません。

いずれの選択肢においても、自社のセキュリティ要件・リスク許容度・運用リソースを踏まえたアーキテクチャ決定が鍵となります。AWSサービスは常にアップデートし続けており、今後NFWがL7機能を強化する可能性もあるかと思います (サポートへのリクエストは出させていただきました) 。現時点では、ホスト偽装を含む高度な回避手法を阻止したいなら、Squid Proxy や高度なL7プロキシ技術を当面維持することが堅実かもしれません。

参考