こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。
Route 53 Resolver DNS Firewall を使ってみた話です。VPCのセキュリティグループや AWS WAF と比較すると話題になることが少ないサービスですが、簡単に導入でき、多層防御の手段の一つとして有効であると感じたため、使ってみた際に考えた現実的な設定方法を書き残したいと思います。
- Route 53 Resolver とは
- Route 53 Resolver DNS Firewall とは
- Route 53 Resolver DNS Firewall で防げるもの、防げないもの
- 準備:Resolver Query Log の出力
- 準備:サブスクリプションフィルターで Alert / Block ログを通知する
- 基本編:拒否リスト方式で DNS Firewall を作成
- 応用編:許可リスト方式で DNS Firewall を作成
- まとめ
Route 53 Resolver とは
公式ドキュメント: https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/resolver.html
Route 53 Resolver はVPC内から名前解決を担当する DNSリゾルバーであり、以前は Amazon Provided DNS と呼ばれていたようです。VPCのネットワークアドレスに2をプラスしたIPアドレスからアクセスできます。(VPCのIPアドレスレンジが 10.0.0.0/16 であれば 10.0.0.2)
例えば、VPC内にあるEC2インスタンスが some-domain.com への名前解決を行う際には、この Route 53 Resolver に問い合わせを行い、結果のIPアドレスによって通信がルートテーブルで制御されます。(内部的には、Route 53 Resolverから他のネームサーバーへ再帰的にルックアップが行われます)
インターネットからアクセスできるパブリックDNSホスト名だけではなく、プライベートホストゾーンやVPC内のプライベートDNSホスト名の名前解決も担当します。例えばEC2インスタンスにプライベートDNS名「ip-10-0-0-11.ap-northeast-1.compute.internal」が付与されている場合、プライベートIPアドレス「10.0.0.11」への解決もRoute 53 Resolverが担当します。
Route 53 Resolver DNS Firewall とは
公式ドキュメント: https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/resolver-dns-firewall-overview.html
DNS Firewall は、Route 53 Resolver による特定ドメインへの名前解決結果をブロックしたり、オーバーライドしたりできるDNS/UDPレイヤーのファイアウォールです。以下の構成要素を設定することで使用を開始できます。
フェールクローズとフェールオープン
VPCでDNS Firewallを有効にする場合、DNS Firewall自身が障害などにより応答しない時のことも考えておくと良いでしょう。デフォルトではフェールクローズと呼ばれ、DNS Firewallが応答しない時は名前解決が失敗するようになっています。フェールオープンに変更することでセキュリティよりも可用性が優先され、DNS Firewallが応答しない時は名前解決が成功するようにできます。
Route 53 Resolver DNS Firewall で防げるもの、防げないもの
DNS Firewall はDNSレイヤーのファイアウォールなので、特定ドメインへの名前解決をブロックし応答を返さないことで、結果的に対象サーバへの通信を防ぎます。例えばインスタンスが侵入されたりマルウェアに感染した際、悪意のあるサーバとのDNS名を介した通信をブロックすることで被害の拡大防止に役立ちます。
一方でドメイン名ではなく、直接IPアドレスによるアクセスはRoute 53 Resolverに名前解決を要求しないため、DNS Firewallによりブロックされません。そのためDNS Firewallによる防御は攻撃への根本対策ではなくあくまでも軽減策と考え、常に他の防御手段と組み合わせることを考えるようにしましょう。
準備:Resolver Query Log の出力
ここからは、実際にどのようにDNS Firewallを使っていくのが良いのかを考えてみます。
まずは準備として、Route 53 Resolverのクエリログを出力するように設定します。これにより、ルールのアクションが Alert
や Block
の時にどんなクエリが問題となったのかを見つけられるようになります。(ログはクエリが許可された場合も出力されます)
CDKで作成する場合は次のようになります。ログの出力先としてCloudWatch Logsロググループ、S3バケット、Kinesis Data Firehoseから選べますが、この例ではCloudWatch Logsロググループとしました。BlockやAlertログの通知をサブスクリプションフィルターで簡単に実装できるためです(後述)。
import * as logs from "aws-cdk-lib/aws-logs"; import * as route53resolver from "aws-cdk-lib/aws-route53resolver"; ... // CloudWatch Logsロググループを作成 const queryLogGroup = new logs.LogGroup(this, "MyResolverQueryLogGroup", { logGroupName: "my-resolver-query-log-group", retention: logs.RetentionDays.ONE_MONTH, }); // Route 53 Resolverログの設定 const queryLogConfig = new route53resolver.CfnResolverQueryLoggingConfig(this, "MyResolverQueryLoggingConfig", { name: "cloudwatch logs", destinationArn: queryLogGroup.logGroupArn, }); // Route 53 ResolverログをVPCに関連付ける new route53resolver.CfnResolverQueryLoggingConfigAssociation(this, "MyResolverQueryLoggingConfigAssociation", { resolverQueryLogConfigId: queryLogConfig.attrId, resourceId: vpc.vpcId, });
これにより、設定したVPC内で Route 53 Resolver へDNSクエリが発生する毎にログに記録されるようになります。下の図では、ログ出力のための logs.ap-northeast-1.amazonaws.com.
への名前解決が記録されています。
クエリが Alert
対象となっている場合は、次のように firewall_rule_action
、 firewall_rule_group_id
、 firewall_domain_list_id
の3つのフィールドがログエントリーに追加されます。 (Block
の場合は firewall_rule_action
が BLOCK
になります)
準備:サブスクリプションフィルターで Alert / Block ログを通知する
DNSクエリが ALERT
や BLOCK
対象となった場合、ログからそれを検知して通知する仕組みを作っておくと、DNS Firewallの設定の不足を見つけたり、攻撃に素早く反応できるようになるのでおすすめです。CloudWatch Logsから特定のログエントリーを簡単にフィルタリングする方法には、メトリクスフィルターやサブスクリプションフィルターがありますが、ログエントリー本文(どのDNSクエリが ALERT
/ BLOCK
されたのか)を通知に含めたいため、サブスクリプションフィルターで実装することにします。
CDKでの実装サンプルは以下のとおりです。
import * as kms from "aws-cdk-lib/aws-kms"; import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs"; import * as destinations from "aws-cdk-lib/aws-logs-destinations"; import * as sns from "aws-cdk-lib/aws-sns"; import * as subscription from "aws-cdk-lib/aws-sns-subscriptions"; ... // SNSトピックの作成。(任意)KMSのデフォルトキーで暗号化 const defaultKey = kms.Key.fromLookup(this, "DefaultSNSKey", { aliasName: "alias/aws/sns" }); const topic = new sns.Topic(this, "MyDnsFirewallNotificationTopic", { masterKey: defaultKey }); // 通知送信先のEメールアドレスをサブスクライブ const sub = new subscription.EmailSubscription("my-name@my-domain.com"); topic.addSubscription(sub); // サブスクリプションフィルターから受け取ったデータをSNSトピックに送信するLambda関数 const firewallLogFunction = new lambdaNodejs.NodejsFunction(this, "MyDnsFirewallLogNotificationFunction", { entry: "functions/dns-firewall-log.ts", runtime: Runtime.NODEJS_16_X, environment: { TOPIC_ARN: topic.topicArn, }, logRetention: logs.RetentionDays.ONE_MONTH, description: "Subscribe to cloudwatch logs and publish to SNS topic", }); topic.grantPublish(firewallLogFunction); // CloudWatch Logsサブスクリプションフィルター // firewall_rule_action フィールドが ALERT もしくは BLOCK の場合に // Lambda関数に送信する queryLogGroup.addSubscriptionFilter("MyDnsFirewallLogFilter", { destination: new destinations.LambdaDestination(firewallLogFunction), filterPattern: logs.FilterPattern.literal('{ $.firewall_rule_action = "ALERT" || $.firewall_rule_action = "BLOCK" }'), });
functions/dns-firewall-log.ts
に以下のようにLambda関数を作成します。
import * as zlib from "zlib"; import { SNSClient, PublishCommand } from "@aws-sdk/client-sns"; import { Handler, CloudWatchLogsEvent, CloudWatchLogsDecodedData } from "aws-lambda"; const client = new SNSClient({ region: process.env.AWS_REGION }); export const handler: Handler = async (input: CloudWatchLogsEvent) => { // ペイロードからログエントリーを取り出す // https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#LambdaFunctionExample const payload = Buffer.from(input.awslogs.data, "base64"); const result = await new Promise<string>((res, rej) => { zlib.gunzip(payload, (e, result) => { return e ? rej(e) : res(result.toString("ascii")); }); }); const parsedResult = JSON.parse(result) as CloudWatchLogsDecodedData; const message = parsedResult.logEvents.map((event) => event.message).join("\n\n"); console.log("Event Data:", message); // SNSトピックに送信 await client.send( new PublishCommand({ TopicArn: process.env.TOPIC_ARN, Subject: "DNS Firewall Log Notification", Message: message, }) ); };
基本編:拒否リスト方式で DNS Firewall を作成
いよいよ DNS Firewall 自体の設定をしていきます。一番簡単なのは、用意されているAWSマネージドのドメインリストを利用することです。これらのドメインリストの名前解決を拒否するだけで、一定の防御効果があります。AWSManagedDomainsBotnetCommandandControl
と AWSManagedDomainsMalwareDomainList
の2つのドメインリストが用意されているので、これらを BLOCK
し、それ以外のドメインは全て許可する拒否リスト方式が簡単に実装できます。
コンソールでの設定方法は公式ドキュメントを参考にすれば良いので、CDKでの実装例を掲載します。なお、AWSマネージドのドメインリストIDはリージョン毎に決まっており、コンソールから確認したものを利用しました。以下のサンプルは東京リージョンのIDを使用しています。
// ルールグループ作成 const dnsFirewallRuleGroup = new route53resolver.CfnFirewallRuleGroup(this, "MyDnsFirewallRuleGroup", { name: "My DNS Firewall rule group", firewallRules: [ // AWS管理のドメインリストをBlock { action: "BLOCK", priority: 1, blockResponse: "NODATA", firewallDomainListId: "rslvr-fdl-1a63d8549cca46e6", }, { action: "BLOCK", priority: 2, blockResponse: "NODATA", firewallDomainListId: "rslvr-fdl-dc19e97bef3c454a", }, ], }); // ルールグループをVPCに関連付け // priorityは1000以上を指定する new route53resolver.CfnFirewallRuleGroupAssociation(this, "MyDnsFirewallRuleGroupAssociation", { name: "My DNS Firewall rule group association", priority: 1000, firewallRuleGroupId: dnsFirewallRuleGroup.attrId, vpcId: vpc.vpcId, });
応用編:許可リスト方式で DNS Firewall を作成
より厳しく、名前解決するドメインを明示的に指定する許可リスト方式にすることもできます。しかし許可するドメインの一覧に漏れがあると、アプリケーションの通信に思わぬ不具合が発生する可能性があるため、慎重に設定しなければなりません。
すなわちVPC内からの全ての正当な名前解決を明示的に許可する必要があり、これには以下のような通信が含まれます。
上の2つはアプリケーションから明示的に通信先を指定する場合が多いので比較的把握しやすいと思いますが、下の2つがなかなか厄介です。
(2024/5/8追記) DNS Firewall においてドメインのリダイレクトがサポート され、設定を行うことでCNAMEを明示的に許可する必要がなくなりました。
AWSサービスなどへの内部的な通信
EC2インスタンスを立ち上げただけで、次のように様々なドメインへの名前解決要求をしていることがログからわかりました。通信先と用途を全て把握するのは難しそうです。
135.7.0.10.in-addr.arpa. does-not-exist.example.com. __cloud_init_expected_not_found__. instance-data. __cloud_init_expected_not_found__.ap-northeast-1.compute.internal. example.invalid. s3.dualstack.ap-northeast-1.amazonaws.com. amazonlinux-2-repos-ap-northeast-1.s3.dualstack.ap-northeast-1.amazonaws.com. 1.amazon.pool.ntp.org. 0.amazon.pool.ntp.org. 2.amazon.pool.ntp.org.
DNS クエリで得られる CNAME
例えば tech.isid.co.jp.
への dig
コマンドの結果は次の通りであり、 hatenablog.com.
の別名となっていることがわかります。
% dig tech.isid.co.jp ;; ANSWER SECTION: tech.isid.co.jp. 86400 IN CNAME hatenablog.com. hatenablog.com. 60 IN A 35.75.255.9 hatenablog.com. 60 IN A 54.199.90.60
これを名前解決する際には、まずは tech.isid.co.jp.
へのDNSクエリが発生し、その後 hatenablog.com.
へのクエリが発生します。両方に対してDNS Firewallのルールが評価されるため、通信を許可するには全ての CNAME に対する名前解決を許可する必要があります。特に自組織で管理しているドメインでない場合は突然CNAMEが変わる可能性もあります。
(2024/5/8追記) DNS Firewall においてドメインのリダイレクトがサポート され、設定を行うことでCNAMEを明示的に許可する必要がなくなりました。
デフォルトAlertがおすすめ
以上により、名前解決を許可したいドメインを完全に把握するのは難しいケースが多いと思いますので、許可リスト方式においてはデフォルトの挙動を Block
ではなく Alert
にするのがおすすめです。 Alert
ログを通知するように設定しているため、通知を受けたら必要な通信かどうかを都度判断し、許可するドメインリストに追加していく運用が良さそうです。
とはいえAWSマネージドのドメインリストは明示的にブロックしたいので、これらを組み合わせたルールグループのCDK実装例は次のようになります。
// 許可するドメインのリストを定義 const allowedDomainList = new route53resolver.CfnFirewallDomainList(this, "MyDnsFirewallAllowedDomains", { name: "My Dns Firewall allowed list", domains: ["*.compute.internal.", "*.amazonaws.com.", "*.ec2.internal.", "*.compute-1.internal.", "my-domain.com."], }); // デフォルトAlert用の全ドメイン const allDomains = new route53resolver.CfnFirewallDomainList(this, "DnsFirewallAllDomains", { name: "All domains", domains: ["*"], }); // ルールグループ作成 const dnsFirewallRuleGroup = new route53resolver.CfnFirewallRuleGroup(this, "MyDnsFirewallRuleGroup", { name: "My DNS Firewall rule group", firewallRules: [ // AWS管理のドメインリストは先にBlock { action: "BLOCK", priority: 1, blockResponse: "NODATA", firewallDomainListId: "rslvr-fdl-1a63d8549cca46e6", }, { action: "BLOCK", priority: 2, blockResponse: "NODATA", firewallDomainListId: "rslvr-fdl-dc19e97bef3c454a", }, // 許可するドメインリスト { action: "ALLOW", priority: 10, firewallDomainListId: allowedDomainList.attrId, }, // それ以外の全ドメインはAlertとする { action: "ALERT", priority: 1000, firewallDomainListId: allDomains.attrId, }, ], }); // ルールグループをVPCに関連付け // priorityは1000以上を指定する new route53resolver.CfnFirewallRuleGroupAssociation(this, "MyDnsFirewallRuleGroupAssociation", { name: "My DNS Firewall rule group association", priority: 1000, firewallRuleGroupId: dnsFirewallRuleGroup.attrId, vpcId: vpc.vpcId, });
まとめ
まとめると、Route 53 Resolver DNS Firewall の現実的な設定は次のようになります。
- DNS Firewallはデフォルトでフェールクローズなので、セキュリティよりも可用性を優先したい場合はフェールオープンに変更しよう
- Route 53 Resolver Query Logを出力しよう
- Query Log の Alert と Block を通知しよう
- AWSマネージドのドメインリストを利用して悪意のあるサーバとの通信を簡単にブロックしよう
- より厳しく設定したい場合、許可リスト方式でデフォルト Alert のルールグループを作成しよう
なお、DNS FirewallとQuery Logの保存先には料金が発生しますので、設定時にはご留意ください。
https://aws.amazon.com/jp/route53/pricing/
お読みいただきありがとうございました。
(2022/12/21追記)
いつの間にかAWS管理のドメインリストに AWSManagedDomainsAggregateThreatList
が追加されていました。
私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。
執筆:@kou.kinyo、レビュー:@yamashita.tsuyoshi (Shodoで執筆されました)