BIND9のResponse Policy Zones(RPZ)によるDNS Firewall構築ガイド:マルウェアとフィッシングを効果的にブロック

Network tutorial - IT technology blog
Network tutorial - IT technology blog

従来のファイアウォールが突破されるとき

約6か月前、従業員50名ほどの自社オフィスで肝を冷やすインシデントが発生しました。ある社員がメールに記載されていた銀行を騙るフィッシングリンクを誤ってクリックしてしまったのです。直後、感染端末上のマルウェアはセカンダリペイロードをダウンロードしようと、見慣れないダイナミックDNSドメインに対してDNSクエリを大量に送信し始めました。境界ファイアウォールではIDS/IPSを有効化していましたが、トラフィック全体がTLS/HTTPSで暗号化されていたため検知・処理が追いつかず、即座に遮断することができませんでした。

このような事態において最も効果的な対策は、ドメイン名前解決の段階(内部DNS Resolver)で遮断することです。高額な次世代ファイアウォール(NGFW)に何万ドルも投資する必要はありませんし、社員の各端末に重いエージェントをインストールする必要もありません。BIND9にはResponse Policy Zones(RPZ)という非常に強力な機能が標準で備わっています。この機能を有効化すれば、一般的なDNSサーバーを堅牢なDNS Firewallへと進化させ、マルウェアがIPアドレスを問い合わせる最初のステップで通信を断ち切ることができます。

Response Policy Zones(RPZ)の動作メカニズム

分かりやすく言えば、RPZは再帰的な名前解決(recursive DNS)プロセスにおける中間フィルター層として機能します。クライアントからクエリを受け取ると、BIND9は通常通りインターネット上のルートサーバーや権威DNSサーバーへ問い合わせを行います。しかし、クライアントに応答を返す直前に、ローカルに設定された「ポリシールール」と結果を照合します。

RPZでよく使われるポリシーアクション(Policy Actions)

  • NXDOMAIN: ドメインが存在しないことを示すエラーコード(Name Error)を返します。悪意のあるドメインは内部ネットワークから実質的に消滅し、マルウェアの通信は即座に遮断されます。
  • NODATA: ドメイン自体は存在するものの、対応するA/AAAAレコードが存在しないと応答します。
  • Redirect / Sinkhole (Local Data): クエリを内部WebサーバーのIP(例: 10.10.10.254)に転送します。このページでユーザーにセキュリティ警告を表示したり、リクエストを記録して感染端末を特定・隔離したりできます。
  • PASSTHRU: DNSクエリを通常通り通過させます。誤検知によってブロックされた業務ドメインを即座にホワイトリスト化する際に使用します。
  • DROP: クエリパケットを破棄し、クライアント側をタイムアウトさせます(クライアント側アプリがハングアップしやすいため、あまり多用されません)。

BIND9 RPZによるDNS Firewall構築 7つのステップ

ステップ1: BIND9でresponse-policy機能を有効化する

まず、BIND9のメイン設定ファイルを開きます(Ubuntu/Debianの場合は/etc/bind/named.conf.options、CentOS/RHELの場合は/etc/named.conf)。続いて、optionsディレクティブ内にresponse-policyブロックを追加します。

sudo nano /etc/bind/named.conf.options
options {
    directory "/var/cache/bind";

    // LANネットワーク向けの再帰的DNS解決を許可
    recursion yes;
    allow-query { 127.0.0.1; 192.168.1.0/24; 10.10.0.0/16; };
    allow-recursion { 127.0.0.1; 192.168.1.0/24; 10.10.0.0/16; };

    // RPZゾーンリストの定義
    response-policy {
        zone "rpz.whitelist.local" policy passthru;
        zone "rpz.malware.local"   policy given;
    } qname-wait-recurse no;

    dnssec-validation auto;
    listen-on-v6 { any; };
};

注意: 宣言する順序が優先順位を決定します。BIND9は上から下へとルールを評価します。業務上必要なドメインが誤って遮断されないよう、rpz.whitelist.localゾーンは必ず先頭に配置してください。

ステップ2: named.conf.localでRPZゾーンを宣言する

/etc/bind/named.conf.localファイル内で、通常のDNSゾーンと同様にゾーン定義を宣言します。

sudo nano /etc/bind/named.conf.local
// ホワイトリスト用RPZゾーン
zone "rpz.whitelist.local" {
    type master;
    file "/etc/bind/rpz/db.rpz.whitelist";
    allow-query { localhost; };
    allow-transfer { none; };
};

// マルウェア・フィッシング遮断用ブラックリストRPZゾーン
zone "rpz.malware.local" {
    type master;
    file "/etc/bind/rpz/db.rpz.malware";
    allow-query { localhost; };
    allow-transfer { none; };
};

ステップ3: ゾーンファイルの作成とフィルタリングルールの定義

RPZファイルを管理するための専用ディレクトリを作成し、サービス実行ユーザーに権限を付与します。

sudo mkdir -p /etc/bind/rpz
sudo chown -R bind:bind /etc/bind/rpz

ホワイトリストファイル /etc/bind/rpz/db.rpz.whitelist は安全なドメインをバイパスするために使用します。

$TTL 300
@       IN      SOA     localhost. root.localhost. (
                        2024040101      ; Serial
                        3600            ; Refresh
                        1800            ; Retry
                        604800          ; Expire
                        300 )           ; Minimum
        IN      NS      localhost.

; ブラックリストに含まれていても通常通り名前解決を許可する
github.com              IN      CNAME   rpz-passthru.
*.github.com            IN      CNAME   rpz-passthru.

マルウェア遮断ファイル /etc/bind/rpz/db.rpz.malware に処理ルールを定義します。

$TTL 300
@       IN      SOA     localhost. root.localhost. (
                        2024040101      ; Serial
                        3600            ; Refresh
                        1800            ; Retry
                        604800          ; Expire
                        300 )           ; Minimum
        IN      NS      localhost.

; 1. NXDOMAINによるフィッシングドメインの遮断(ドットはルートヌルを表す)
bad-phishing-site.xyz   IN      CNAME   .
*.bad-phishing-site.xyz IN      CNAME   .

; 2. NODATAによるC2サーバーの遮断(アスタリスク+ドット)
malware-c2-server.top   IN      CNAME   *.
*.malware-c2-server.top IN      CNAME   *.

; 3. 内部警告ページへのリダイレクト(シンクホール)
trojan-download.online  IN      A       10.10.10.254
*.trojan-download.online IN     A       10.10.10.254

ステップ4: RPZ専用のログチャンネルを設定する

クライアント端末が悪意のあるドメインへクエリを送信した際、端末を迅速に隔離するためには該当IPを正確に把握する必要があります。/etc/bind/named.conf.log で rpz カテゴリを独立したログファイルに分離しましょう。

sudo nano /etc/bind/named.conf.log
logging {
    channel rpz_log {
        file "/var/log/named/rpz.log" versions 5 size 20m;
        severity info;
        print-time yes;
        print-category yes;
        print-severity yes;
    };
    category rpz {
        rpz_log;
    };
};

ログ保存用ディレクトリを作成し、DNSデーモンへの書き込み権限を付与することを忘れないでください。

sudo mkdir -p /var/log/named
sudo chown -R bind:bind /var/log/named

ステップ5: 構文チェックとサービスのリロード

ネットワーク全体のDNS名前解決が中断するのを防ぐため、BIND9をリロードする前に構文チェックを念入りに行います。

# メイン設定ファイルの構文チェック
sudo named-checkconf

# 各ゾーンファイルの構文チェック
sudo named-checkzone rpz.malware.local /etc/bind/rpz/db.rpz.malware

# クライアントの接続を切断せずに安全に設定を適用
sudo rndc reload

ステップ6: クライアント端末からの実機テスト

LAN内のクライアント端末(例: 192.168.1.45)から、digコマンドを使って悪意のあるドメインへテストクエリを送信します。

dig @192.168.1.2 bad-phishing-site.xyz

応答結果として ANSWER SECTION は返されず、即座に NXDOMAIN ステータスが返されます。

;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 48215
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

DNSサーバー上のログファイルを開いて詳細を確認します。

sudo tail -f /var/log/named/rpz.log
01-Apr-2024 14:22:10.104 rpz: info: client @0x7f88a0 192.168.1.45#51234 (bad-phishing-site.xyz): rpz QNAME NXDOMAIN rewrite bad-phishing-site.xyz via bad-phishing-site.xyz.rpz.malware.local

ログ行から端末 192.168.1.45 がフィッシングドメインにアクセスしたことが一目瞭然です。これをもとに影響範囲を特定し、迅速なインシデント対応を行えます。

ステップ7: 脅威フィードの自動更新

1件ずつ手動でドメインを追加するのは現実的ではありません。本番環境では、cronジョブを用いて2時間ごとにURLhausやMalwareDomainListから最新フィードを取得・更新するBashスクリプトを運用しています。

#!/bin/bash
# スクリプト: /usr/local/bin/update-rpz.sh
set -euo pipefail

FEED_URL="https://urlhaus.abuse.ch/downloads/rpz/"
DEST_FILE="/etc/bind/rpz/db.rpz.urlhaus"
TMP_FILE="/tmp/urlhaus.rpz"

# 最新フィードをダウンロード
if curl -s -f -L "$FEED_URL" -o "$TMP_FILE"; then
    # 上書き前にゾーンファイルの妥当性を検証
    if named-checkzone rpz.urlhaus.local "$TMP_FILE" > /dev/null 2>&1; then
        mv "$TMP_FILE" "$DEST_FILE"
        chown bind:bind "$DEST_FILE"
        rndc reload rpz.urlhaus.local
    else
        rm -f "$TMP_FILE"
    fi
fi

6か月間の運用から得た3つの教訓

  1. 常に$TTLを低く(300秒)設定する: 万が一重要な取引先のサイトを誤ってブロックしてしまっても、ルールを解除すればクライアント端末は最大5分で変更を検知します。半日近く業務が停滞するような事態を防ぐことができます。
  2. ワイルドカードの扱いに注意する: *.domain.com IN CNAME . という構文はすべてのサブドメインを遮断します。もしフィードに *.github.io や *.pages.dev などの共有ドメインが含まれていた場合、チームが参照する技術ドキュメント全体が閲覧不能になってしまいます。そのため、上位に優先されるホワイトリストゾーンの設定が不可欠です。
  3. リソース消費が極めて軽量: 10万件以上の悪性ドメインを含むゾーンを読み込んでも、BIND9サーバーで消費される追加RAMは約70MB程度です。DNSの応答時間の増加も1ms未満であり、エンドユーザーが遅延を感じることはまったくありません。

BIND9にわずか数行の設定を追加するだけで、社内ネットワークにプロアクティブな防御層を構築し、ボットネットやフィッシングの脅威をネットワークの入り口で確実に遮断できます。

Share: