電通総研 テックブログ

電通総研が運営する技術ブログ

Route 53 Resolver DNS Firewall の現実的な設定を考える

こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。

Route 53 Resolver DNS Firewall を使ってみた話です。VPCのセキュリティグループや AWS WAF と比較すると話題になることが少ないサービスですが、簡単に導入でき、多層防御の手段の一つとして有効であると感じたため、使ってみた際に考えた現実的な設定方法を書き残したいと思います。

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アドレスからアクセスできます。(VPCIPアドレスレンジが 10.0.0.0/16 であれば 10.0.0.2)
例えば、VPC内にあるEC2インスタンスが some-domain.com への名前解決を行う際には、この Route 53 Resolver に問い合わせを行い、結果のIPアドレスによって通信がルートテーブルで制御されます。(内部的には、Route 53 Resolverから他のネームサーバーへ再帰的にルックアップが行われます)

パブリックDNS名の名前解決

インターネットからアクセスできるパブリックDNSホスト名だけではなく、プライベートホストゾーンやVPC内のプライベートDNSホスト名の名前解決も担当します。例えばEC2インスタンスにプライベートDNS名「ip-10-0-0-11.ap-northeast-1.compute.internal」が付与されている場合、プライベートIPアドレス「10.0.0.11」への解決もRoute 53 Resolverが担当します。

プライベートDNS名の名前解決

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レイヤーのファイアウォールです。以下の構成要素を設定することで使用を開始できます。

フェールクローズとフェールオープン

VPCDNS Firewallを有効にする場合、DNS Firewall自身が障害などにより応答しない時のことも考えておくと良いでしょう。デフォルトではフェールクローズと呼ばれ、DNS Firewallが応答しない時は名前解決が失敗するようになっています。フェールオープンに変更することでセキュリティよりも可用性が優先され、DNS Firewallが応答しない時は名前解決が成功するようにできます。

Route 53 Resolver DNS Firewall で防げるもの、防げないもの

DNS FirewallDNSレイヤーのファイアウォールなので、特定ドメインへの名前解決をブロックし応答を返さないことで、結果的に対象サーバへの通信を防ぎます。例えばインスタンスが侵入されたりマルウェアに感染した際、悪意のあるサーバとのDNS名を介した通信をブロックすることで被害の拡大防止に役立ちます。

DNS Firewallによるブロック

一方でドメイン名ではなく、直接IPアドレスによるアクセスはRoute 53 Resolverに名前解決を要求しないため、DNS Firewallによりブロックされません。そのためDNS Firewallによる防御は攻撃への根本対策ではなくあくまでも軽減策と考え、常に他の防御手段と組み合わせることを考えるようにしましょう。

IPアドレスによるアクセス

準備:Resolver Query Log の出力

ここからは、実際にどのようにDNS Firewallを使っていくのが良いのかを考えてみます。
まずは準備として、Route 53 Resolverのクエリログを出力するように設定します。これにより、ルールのアクションが AlertBlock の時にどんなクエリが問題となったのかを見つけられるようになります。(ログはクエリが許可された場合も出力されます)

Query Logの出力

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_actionfirewall_rule_group_idfirewall_domain_list_id の3つのフィールドがログエントリーに追加されます。 (Block の場合は firewall_rule_actionBLOCK になります)

アラートログ

準備:サブスクリプションフィルターで Alert / Block ログを通知する

DNSクエリが ALERTBLOCK 対象となった場合、ログからそれを検知して通知する仕組みを作っておくと、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マネージドのドメインリストを利用することです。これらのドメインリストの名前解決を拒否するだけで、一定の防御効果があります。AWSManagedDomainsBotnetCommandandControlAWSManagedDomainsMalwareDomainList の2つのドメインリストが用意されているので、これらを BLOCK し、それ以外のドメインは全て許可する拒否リスト方式が簡単に実装できます。

AWSマネージドのドメインリスト

コンソールでの設定方法は公式ドキュメントを参考にすれば良いので、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内からの全ての正当な名前解決を明示的に許可する必要があり、これには以下のような通信が含まれます。

  • インターネット上の公開ドメインへの通信
  • 利用しているプライベートホストゾーンのドメインへの通信
  • AWSサービスなどへの内部的な通信
  • DNS クエリで得られる CNAME

上の2つはアプリケーションから明示的に通信先を指定する場合が多いので比較的把握しやすいと思いますが、下の2つがなかなか厄介です。

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が変わる可能性もあります。

デフォルト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.tsuyoshiShodoで執筆されました