午前2時のインシデント:正規クエリが偽りを返すとき
火曜日の午前2時15分、オンコールのアラートが一斉に鳴り響きました。監視ダッシュボードには、決済用の3つのサブドメインで突如としてSSL証明書の不一致エラーが発生していることが示されていました。正規の顧客トラフィックが、別のリージョンにある見知らぬIPアドレス(203.0.113.84)へと誘導されていたのです。Webサーバー自体は正常に稼働しており、ファイアウォール設定にも変更はなく、認証ログにも不審なSSHログインの形跡は一切ありませんでした。
外部から簡単な調査を実施したところ、問題はすぐに判明しました。
dig @8.8.8.8 api.example.com +short
このクエリは、本来のエッジプロキシである198.51.100.25ではなく、203.0.113.84を返したのです。パブリックな再帰リゾルバーのキャッシュが汚染(キャッシュポイズニング)されていました。標準のDNSは、認証機構を持たないUDPポート53上で動作します。
従来のDNS名前解決は推測が容易な16ビットのトランザクションIDに依存しているため、攻撃者は偽装した応答パケットをリゾルバーへ大量に送りつけることが可能です。権威DNSサーバーからの正規応答よりも先に偽装パケットが到達すると、リゾルバーはその偽のレコードをキャッシュしてしまいます。その結果、その後の3,600秒間にわたり、該当リゾルバーを経由するすべての訪問者が攻撃者の管理下にあるサーバーへリダイレクトされることになります。この根本的な解決策となるのが、DNSSEC(Domain Name System Security Extensions)です。
基本概念:暗号署名がDNSを保護する仕組み
標準的なDNSは「盲目的な信頼」の上に成り立っています。リゾルバーはクエリを送信し、返ってきた応答を受け取り、パケットが途中で改ざんされていないものと前提して処理を進めます。これに対し、DNSSECはセキュリティの焦点をネットワーク転送から暗号検証へと移行させます。権威サーバーがあらゆるレコードセットに対して暗号署名を生成し、検証リゾルバーが公開されている信頼の連鎖(Chain of Trust)を用いてそれらの署名を検証します。
主要なアーキテクチャコンポーネント
- Zone Signing Key(ZSK / ゾーン署名鍵): 日常的なリソースレコード(A、AAAA、MXなど)に署名します。ゾーン管理者はこの鍵を頻繁にローテーションし、通常は30〜90日ごとに更新します。
- Key Signing Key(KSK / 鍵署名鍵): DNSKEYレコードセットのみに署名します。KSKの更新には親ゾーン(レジストラ)側の更新が必要となるため、ローテーションは低頻度(通常1〜2年に1回)で行われます。
- RRSIG(Resource Record Signature / リソースレコード署名): 各レコードセットに付与される電子署名です。リプレイ攻撃を防ぐため、明示的な有効期限タイムスタンプ(通常14〜30日間)が含まれます。
- DNSKEY: ゾーン内で公開される公開鍵レコードです。リゾルバーはこれを取得してRRSIGレコードを検証します。フラグ256はZSKを示し、フラグ257はKSKを示します。
- DS(Delegation Signer / 委任署名者): 親ゾーン(例:
.com)に登録される、KSKのSHA-256フィンガープリント(ダイジェスト)です。レジストラとネームサーバーの間を暗号的に橋渡しする役割を果たします。 - NSEC / NSEC3: レコードが存在しないことを暗号的に証明するレコードです。NSEC3では、攻撃者がゾーンウォーキングによってゾーンファイル全体を網羅的に収集(スクレイピング)するのを防ぐため、ソルトと複数回の反復処理を用いて名前をハッシュ化します。
実践手順:BIND9でゾーンに署名する
以下のセットアップでは、BIND 9.18以降が稼働しているDebian 12 / Ubuntu 22.04 LTS環境を想定しています。ネームサーバーでは、すでにexample.comの権威ゾーンが設定・稼働しているものとします。
ステップ1:BINDでDNSSECディレクティブを有効化する
BINDのメイン設定ファイルを開きます。
sudo nano /etc/bind/named.conf.options
options { ... };ブロック内に以下の設定が存在することを確認します。
options {
directory "/var/cache/bind";
dnssec-validation auto;
auth-nxdomain no;
listen-on-v6 { any; };
};
設定ファイルの構文エラーをチェックします。
sudo named-checkconf
コマンドが何も出力しなければ、構文は正常です。
ステップ2:ZSKとKSKを生成する
暗号鍵を格納するための専用ディレクトリを作成し、適切なアクセス権限を設定します。
sudo mkdir -p /etc/bind/keys
cd /etc/bind/keys
sudo chown -R bind:bind /etc/bind/keys
sudo chmod 750 /etc/bind/keys
暗号化インフラを展開する前に、管理者資格情報のセキュリティを確認してください。新しい管理コンソールやデプロイ用アカウントに強力なパスワードが必要な場合は、ローカル環境で生成することをお勧めします。toolcraft.app/ja/tools/security/password-generator のオフラインユーティリティを使用すれば、ネットワーク経由で値を送信することなく、ブラウザ内でエントロピーの高い安全なパスワードを生成できます。
アルゴリズム13(ECDSA P-256 with SHA-256)を使用して、ゾーン署名鍵(ZSK)を生成します。
sudo dnssec-keygen -a ECDSAP256SHA256 -n ZONE example.com
アルゴリズム13で生成される署名はわずか64バイトです。これに対し、2048ビットRSAでは256バイトの署名サイズになります。署名サイズを小さく抑えることで、一般的なEDNS0のバッファサイズ上限である1,232バイトを十分に下回り、UDPパケットの断片化(フラグメンテーション)を回避できます。
続いて、-f KSKフラグを指定して鍵署名鍵(KSK)を生成します。
sudo dnssec-keygen -a ECDSAP256SHA256 -f KSK -n ZONE example.com
これにより、公開鍵を含む2つの.keyファイルと、秘密鍵データを保持する2つの.privateファイルの計4つのファイルが生成されます。
ステップ3:自動インライン署名を設定する
dnssec-signzoneによる手動の再署名は、レコードを変更するたびに整合性が崩れやすくなります。代わりに、BINDのinline-signing機能を使って署名を自動管理させましょう。/etc/bind/named.conf.localのゾーン定義を以下のように更新します。
zone "example.com" {
type master;
file "/var/lib/bind/db.example.com";
key-directory "/etc/bind/keys";
auto-dnssec maintain;
inline-signing yes;
};
ゾーンファイルを/var/lib/bind/にコピーします。動的な.signedゾーンファイルやジャーナルファイルを書き出すため、BINDデーモンにこのディレクトリへの書き込み権限が必要です。
sudo cp /etc/bind/db.example.com /var/lib/bind/db.example.com
sudo chown -R bind:bind /var/lib/bind
sudo rndc reload
BINDは元のゾーンファイルを変更することなく、バックグラウンドですぐにdb.example.com.signedを作成します。
ステップ4:レジストラ登録用のDSレコードを抽出する
これでサーバーローカルでのレコード署名は完了しました。ただし、ドメインの親ゾーン(レジストラ側)で権威委任が設定されるまで、パブリックリゾルバーはDNSSEC検証を適用しません。フラグ257が含まれている鍵ファイルを特定し、KSKを確認します。
grep "257" /etc/bind/keys/Kexample.com.*.key
特定したKSKファイルを指定して、委任署名者(DS)レコードを生成します。
cd /etc/bind/keys
sudo dnssec-dsfromkey Kexample.com.+013+02371.key
レジストラへの登録に適したフォーマットで出力が表示されます。
example.com. IN DS 2371 13 2 4B79A63D78E0E84F99881829D64A12B268B96464019AA1D389920DF120489AB1
ドメインレジストラのDNSSEC管理画面を開き、以下の4つのフィールドを入力します。
- Key Tag:
2371 - Algorithm:
13(ECDSA Curve P-256 with SHA-256) - Digest Type:
2(SHA-256) - Digest:
4B79A63D78E0E84F99881829D64A12B268B96464019AA1D389920DF120489AB1
ステップ5:信頼の連鎖を検証する
レジストラでの更新がルートサーバーへ伝播するまでには、通常15〜60分程度かかります。まず、ローカルホスト上で署名が正しく生成されているかを検証します。
dig @localhost example.com A +dnssec +multiline
Aレコードの直下にRRSIGレコードが表示されているはずです。次に、delv(DNS Lookup and Validation)コマンドを使用して、Cloudflareの1.1.1.1リゾルバー経由でエンドツーエンドの検証をテストします。
delv @1.1.1.1 example.com A
親ゾーンの委任とローカル鍵が正しく一致している場合、delvは以下のように出力します。
; fully validated(完全検証済み)
example.com. 300 IN A 198.51.100.25
example.com. 300 IN RRSIG A 13 2 300 ...
もしvalidation failure(検証失敗)と表示された場合は、ローカルのBINDログを調査する前に、レジストラ側に登録したDSダイジェストの値を再確認してください。
運用上のまとめ
DNSSECが導入されていない場合、エッジのファイアウォールやTLS証明書があっても、上流のリゾルバーによる偽造IPアドレスのキャッシュを防ぐことはできません。アルゴリズム13のECDSA鍵とBIND9のインライン署名を組み合わせることで、この問題をスマートに解決できます。署名は自動的に最新状態に保たれ、パケットサイズも小さく維持され、クライアントはルートサーバーから個々のホストレコードに至るまで完全な検証を行うことが可能になります。

