はじめに
この記事はコインチェック株式会社(以下、コインチェック)のアドベントカレンダー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 に対する時間料金とトラフィックによる従量課金で、そこまで高額な料金設定ではないため、導入の敷居は低いです。
検証環境の構築手順と脅威シナリオ
構築ステップ概要
- VPCとサブネット用意
- Private SubnetにEC2インスタンスを配置。EC2 インスタンスから外部への通信をおこなう。
- NFWのデプロイ
- AWS Network Firewallを設置し、VPCエンドポイント経由で Private Subnet のアウトバウンドトラフィックをNFWにルーティングします。
- NFWポリシーでallowed.example.comのみ許可し、他ドメインはデフォルトブロックとするドメインリストルールを設定します。
- 確認
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オプションを使います。
- この場合、リクエスト上は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; )
- 上記はあくまで一例で、実際にはSNIとHostの整合性を突合せる高度なルールやLuaスクリプトを用いて、「SNIと接続先IPが想定と異なる」「ホスト偽装パターン」が見られたらドロップすることが可能です。
- SuricataはL7インスペクションが可能なため、ホスト偽装による単純な回避を防ぎやすいと思います。ただし、GWLB、Suricataホスト、ルール管理といった新たな運用コスト・複雑性が発生し、Squid導入時と別観点での管理負荷が増える可能性があります。
パフォーマンス、コスト、運用性の考察
Squid Proxy に匹敵するL7精度をNFW中心で得るには、他機能を大量に組み合わせる必要があり、結果的にSquid導入と同様、またはそれ以上の複雑なアーキテクチャとなり得ることがわかります。
利用リソース |
考察 |
Squid (Fargate) |
|
NFW単体 |
|
NFW + DNS Firewall + Suricata/GWLB + GuardDuty |
|
結論と実務的な指針
本検証で分かったこと
- NFW単体でSquid Proxy 並みのドメインベース制御は困難で、ホストヘッダ偽装攻撃は簡単に成立し、DNS FirewallやGuardDutyでは本質的な対策になりづらい。
- SuricataやTLSインスペクションでNFWを補強可能だが、運用コスト増となり、L7レベルの細粒度コントロールを追加ツールで達成可能だが、手軽さは失われる。
したがって、
- L7レベルで厳密なドメイン制御が不可欠であり、ホスト偽装攻撃も確実に潰したい場合は、Squid Proxy をFargate等で運用し続けるのが現時点では無難であることがわかります。
- コスト・運用負荷軽減を優先し、要件次第である程度の妥協が可能ならNFW+DNS Firewallで基本的なドメインリスト制御を行い、未知リスクはGuardDutyやSIEMで検知・対応する戦略もありかもしれません。
いずれの選択肢においても、自社のセキュリティ要件・リスク許容度・運用リソースを踏まえたアーキテクチャ決定が鍵となります。AWSサービスは常にアップデートし続けており、今後NFWがL7機能を強化する可能性もあるかと思います (サポートへのリクエストは出させていただきました) 。現時点では、ホスト偽装を含む高度な回避手法を阻止したいなら、Squid Proxy や高度なL7プロキシ技術を当面維持することが堅実かもしれません。