午前2時15分のルーティングループ
午前2時15分、PagerDutyのアラートでスマートフォンが鳴り響きました。社内の開発チームがステージング環境にアクセスできず、すべてのリクエストが接続タイムアウトになっています。5分ほど調査した結果、原因が判明しました。内部サーバーがステージングポータルにパブリックIPアドレスで接続しようとしていたのです。トラフィックはローカルネットワークを出てISPに到達し、ファイアウォールを介して「ヘアピン」のように戻ってこようとしていました。ファイアウォールはこれをセキュリティリスクと判断し、即座にパケットを破棄していたのです。
このようなアーキテクチャ上の悩みはよくあります。例えば portal.example.com というサービスを考えてみましょう。ロンドンの顧客には 203.0.113.10 のようなパブリックIPが見えるべきですが、オフィスの従業員には 192.168.1.10 のようなローカルIPが提供されるべきです。内部トラフィックをわざわざパブリックインターネットに出してから戻すのは非効率です。これによって40〜60msの不要なレイテンシが発生し、エッジルーターが単一障害点(SPOF)になってしまいます。
スプリットホライズンDNS(またはスプリットビューDNS)はこの問題を解決します。Linux上でBIND9を使用することで、クエリの送信元を検出し、それに応じた適切なレスポンスを返すようにDNSサーバーを構成できます。私の経験では、これを実装することで高トラフィック環境でのファイアウォールのCPU負荷を最大30%削減でき、内部トラフィックをローカル内に完結させることができます。
LinuxへのBIND9のインストール
ほとんどのプロダクションDNSサーバーは、セキュリティパッチの安定性からDebianまたはUbuntuで運用されています。以下の手順は標準的なものですが、カスタムロジックを導入するための準備として確実に行いましょう。
# パッケージリストを更新
sudo apt update
# BIND9と主要なユーティリティをインストール
sudo apt install bind9 bind9utils bind9-doc -y
# 起動時に自動開始するように設定
sudo systemctl enable bind9
sudo systemctl start bind9
systemctl status bind9 を実行して、サービスがアクティブであることを確認してください。設定ファイルを変更する前に、クリーンなベースラインを確認することは非常に重要です。
設定:Viewの実装
BIND9は「View(ビュー)」を使用してDNSネームスペースを分割します。ここで厳格なルールが1つあります。Viewを使用する場合、すべてのゾーンをViewの中で定義しなければなりません。グローバルゾーンとView内のゾーンを混在させることはできません。
1. アクセス制御リスト(ACL)の定義
まず、誰を「内部(internal)」とみなすかを定義します。/etc/bind/named.conf.options を開きます。設定を一元管理するために、ここでACLを定義するのが私の好みです。ループバックアドレスとローカルのCIDRブロックを含めます。
acl "trusted" {
127.0.0.0/8;
192.168.1.0/24;
10.0.0.0/8;
};
options {
directory "/var/cache/bind";
recursion yes;
allow-query { any; };
dnssec-validation auto;
listen-on-v6 { any; };
};
2. Viewの設定
/etc/bind/named.conf.local を編集して、Internal(内部)と External(外部)のViewを定義します。BINDはこれらを上から順に処理します。クライアントのソースIPに最初に一致したViewが適用されます。
# 内部View
view "internal" {
match-clients { "trusted"; };
recursion yes;
zone "example.com" {
type master;
file "/etc/bind/zones/db.example.com.internal";
};
include "/etc/bind/named.conf.default-zones";
};
# 外部View
view "external" {
match-clients { any; };
recursion no;
zone "example.com" {
type master;
file "/etc/bind/zones/db.example.com.external";
};
};
内部ユーザーには recursion yes を設定し、サーバーをWeb全体のフルリゾルバーとして機能させます。外部Viewに対しては、再帰問い合わせを無効(recursion no)にします。これにより、サーバーがDNSアンプ攻撃(増幅攻撃)に悪用されるのを防ぎます。
3. ゾーンファイルの作成
example.com に対して、2つの異なるバージョンのゾーンファイルが必要です。整理しやすいように専用のディレクトリを作成しましょう。
sudo mkdir /etc/bind/zones
内部ゾーンファイル
/etc/bind/zones/db.example.com.internal では、ホスト名をプライベートIPにマッピングします。内部的な変更が頻繁に予想される場合は、TTLを低め(600)に設定しておくと良いでしょう。
$TTL 600
@ IN SOA ns1.example.com. admin.example.com. (
2023101001 ; シリアル番号 (YYYYMMDDNN)
604800 ; リフレッシュ
86400 ; リトライ
2419200 ; 有効期限
604800 ) ; ネガティブキャッシュTTL
;
@ IN NS ns1.example.com.
ns1 IN A 192.168.1.5
portal IN A 192.168.1.10
web IN A 192.168.1.11
外部ゾーンファイル
/etc/bind/zones/db.example.com.external では、外部向けのWAN IPアドレスを使用します。
$TTL 86400
@ IN SOA ns1.example.com. admin.example.com. (
2023101001 ; シリアル番号
604800 ; リフレッシュ
86400 ; リトライ
2419200 ; 有効期限
604800 ) ; ネガティブキャッシュTTL
;
@ IN NS ns1.example.com.
ns1 IN A 203.0.113.5
portal IN A 203.0.113.10
web IN A 203.0.113.11
検証とモニタリング
DNSは繊細です。セミコロンが1つ欠けているだけで、ネットワーク全体の名前解決が停止してしまいます。サービスを再起動する前に、必ず構文を検証してください。
# BIND設定の構文チェック
sudo named-checkconf
# 特定のゾーンファイルのチェック
sudo named-checkzone example.com /etc/bind/zones/db.example.com.internal
sudo named-checkzone example.com /etc/bind/zones/db.example.com.external
出力にエラーがなければ、BINDを再起動します:sudo systemctl restart bind9
ロジックのテスト
dig コマンドを使用して、Viewが機能しているか確認します。DNSサーバー自体(信頼されたIP)から実行すると、192.168.x.xのアドレスが返ってくるはずです。
dig @localhost portal.example.com
外部からのリクエストをシミュレートするには、リモートVPSやモバイルホットスポットから dig を実行してください。代わりに203.0.x.xのアドレスが表示されるはずです。もし結果が逆転している場合は、ACLの範囲を再確認し、設定ファイル内で「internal」Viewが最初に記載されているか確認してください。
ログの監視
Viewの割り当てをデバッグするには、システムジャーナルを確認するのが最適です。以下のコマンドを使用して、クエリをリアルタイムで監視します。
journalctl -u bind9 -f
view internal: query という記述を含む行を探してください。これにより、BINDがローカルユーザーを正しく識別していることが確認できます。スプリットホライズンDNSはルーティングの問題を解決するだけでなく、内部のIP構成を外部から隠蔽することでセキュリティ層を追加します。これは、複雑なNATルールや手動のhostsファイル編集を必要とする問題に対する、クリーンなサーバーサイドの解決策です。

