LinuxでCoreDNSによるDoHおよびDoTを構築:内部DNSクエリの盗聴・改ざんを防止する

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

クイックスタート:5分で暗号化CoreDNSを立ち上げる

ポート53を使用する平文のDNSは、すべてのクエリを暗号化せずに送信します。内部ネットワークへのアクセス権を持つ攻撃者や、VPC内で侵害されたコンテナが存在する場合、クエリの盗聴や偽装された応答の注入(スプーフィング)、さらにはDNSトンネリングによる不正通信のリスクが生じます。内部DNSトラフィックを暗号化することで、このセキュリティギャップを解消できます。CoreDNSは、外部依存関係なしで標準機能としてDNS over TLS(DoT)とDNS over HTTPS(DoH)の両方に対応しています。

以下は、Ubuntu 22.04またはDebian 12上で暗号化リゾルバーを数分で稼働させるための、実践的なセットアップ手順です。

1. CoreDNSのダウンロードとインストール

# Linux amd64バイナリ(v1.11.3)を取得
curl -LO https://github.com/coredns/coredns/releases/download/v1.11.3/coredns_1.11.3_linux_amd64.tgz
tar -zxvf coredns_1.11.3_linux_amd64.tgz
sudo mv coredns /usr/local/bin/
sudo chmod +x /usr/local/bin/coredns

# 設定ディレクトリと証明書ディレクトリを作成
sudo mkdir -p /etc/coredns /etc/coredns/certs

2. ローカルTLS証明書の生成

内部インフラ向けに、RSA秘密鍵と自己署名証明書を生成します(または組織の内部CAで署名したものを使用します):

sudo openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout /etc/coredns/certs/dns.key \
  -out /etc/coredns/certs/dns.crt \
  -days 365 -subj "/CN=dns.internal.lan"

3. Corefileの記述

/etc/coredns/Corefile を作成し、DoT(ポート853)とDoH(ポート443)をリッスンさせつつ、未解決のクエリをアップストリームのプライバシー保護リゾルバーへプロキシするように設定します:

# DNS over TLS (DoT)
tls://.:853 {
    tls /etc/coredns/certs/dns.crt /etc/coredns/certs/dns.key
    forward . 1.1.1.1 8.8.8.8
    cache 300
    errors
    log
}

# DNS over HTTPS (DoH)
https://.:443 {
    tls /etc/coredns/certs/dns.crt /etc/coredns/certs/dns.key
    forward . 1.1.1.1 8.8.8.8
    cache 300
    errors
    log
}

4. サーバーの起動とテスト

# ターミナルで直接CoreDNSを起動
sudo /usr/local/bin/coredns -conf /etc/coredns/Corefile

別のターミナルを開き、kdig(knot-dnsutils に含まれるツール)を使用してDoTリスナーの動作を確認します:

kdig @127.0.0.1 -p 853 +tls +tls-ca=/etc/coredns/certs/dns.crt example.com

アーキテクチャ解説:本番環境におけるDoT vs DoH

DoTとDoHのどちらを選択するかは、クライアントが配置されている環境やネットワークファイアウォールの管理権限によって完全に決まります。

DoT (RFC 7858) vs DoH (RFC 8484)

  • DoT(DNS over TLS)は、バイナリ形式のDNSワイヤーフォーマットをTCPポート853のTLSセッションに直接カプセル化します。プロトコルのオーバーヘッドが極めて小さく、通常TLSセッションハンドシェイクあたり1KB未満です。サーバー間通信、Kubernetesノードのリゾルバー、サイト間VPNなどに最適です。また、セキュリティチームにとっても、ポート853は境界ファイアウォールでの監視やフィルタリングが容易であるというメリットがあります。
  • DoH(DNS over HTTPS)は、DNSクエリをTCPポート443のHTTP/2またはHTTP/3ストリーム内にパッケージ化します。標準的なHTTPSトラフィックと同化するため、移動の多い開発者のノートPC、リモートオフィス、制限の厳しいプロキシ配下のネットワークに最適です。その代償として、メモリ使用量がわずかに増加し(DoT比で約15〜20%増)、HTTPフレーミングのオーバーヘッドが発生します。

CoreDNSにおける暗号化クエリの処理フロー

CoreDNSは、受信したパケットごとにプラグインを順番に実行します:

  1. TLS終端:CoreDNSが設定された証明書を使用してハンドシェイクを処理します。
  2. ゾーン解決:hosts や file などのローカルプラグインが静的レコードを最初に評価します。
  3. インメモリキャッシュ:cache プラグインが重複する参照をインターセプトし、キャッシュヒット時には1ミリ秒未満で応答を返します。
  4. アップストリーム転送:キャッシュミスしたリクエストは forward プラグインに渡され、暗号化チャネル経由で外部リゾルバーへ転送されます。

120個のマイクロサービスレプリカが稼働するクラスター環境において、アイドル時45MBのRAMを消費するCoreDNSの単一インスタンスで毎秒18,000クエリを容易に処理でき、キャッシュ済みエントリに対して3ミリ秒未満のレイテンシを維持します(本番導入時の負荷検証にはDNSパフォーマンスのベンチマークツールを活用するのが効果的です)。

本番環境セットアップ:systemdサービスの堅牢化とアップストリーム暗号化

バイナリを対話型で直接実行するのは動作検証には適していますが、本番環境のワークロードではプロセスの分離、自動再起動、アップストリーム通信の暗号化が不可欠です。

1. DoT転送による安全なアップストリーム問い合わせ

ISPやアップストリームのリゾルバーに平文でクエリを転送してはいけません。厳格なSNIホスト名検証を適用したDoT経由でアップストリームトラフィックをルーティングします:

# /etc/coredns/Corefile
tls://.:853 https://.:443 {
    tls /etc/coredns/certs/dns.crt /etc/coredns/certs/dns.key

    # ローカルのスプリットホライズンエントリ
    hosts {
        10.10.0.15 db-primary.internal.lan
        10.10.0.16 db-replica.internal.lan
        fallthrough
    }

    # TLSホスト名検証付きのアップストリームDoT
    forward . tls://1.1.1.1 tls://1.0.0.1 {
        tls_servername cloudflare-dns.com
        health_check 5s
    }

    cache 3600 {
        success 4096
        denial 1024
    }
    prometheus :9153
    errors
    log
}

2. 非特権systemdサービスの設定

専用のシステムユーザーを作成します。Linuxケーパビリティを利用して、root権限なしでバイナリがポート443および853にバインドできるように設定します:

sudo useradd -r -d /var/lib/coredns -s /usr/sbin/nologin coredns
sudo chown -R coredns:coredns /etc/coredns
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/coredns

/etc/systemd/system/coredns.service にsystemdサービスユニットを記述します:

[Unit]
Description=CoreDNS 暗号化DNSサーバー
Documentation=https://coredns.io/manual/toc/
After=network.target

[Service]
Type=simple
User=coredns
Group=coredns
ExecStart=/usr/local/bin/coredns -conf /etc/coredns/Corefile
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

systemdをリロードし、デーモンを起動します:

sudo systemctl daemon-reload
sudo systemctl enable --now coredns
sudo systemctl status coredns

動作検証とトラブルシューティング

curlを使用したDoHクエリのテスト

Base64URLエンコードされたDNSワイヤー形式のクエリを送信し、curl で直接DoHエンドポイントに問い合わせを行います:

curl -k -H "accept: application/dns-message" \
  --resolve dns.internal.lan:443:127.0.0.1 \
  "https://dns.internal.lan/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB" \
  --output - | xxd

よくある落とし穴と注意点

  • ポート53のバインド競合:Ubuntuでは systemd-resolved が 127.0.0.53:53 で動作しています。CoreDNSで暗号化されていないポート53のフォールバッククエリも処理させたい場合は、スタブリスナーを無効化してください。/etc/systemd/resolved.conf で DNSStubListener=no を設定し、/etc/resolv.conf を /run/systemd/resolve/resolv.conf へのシンボリックリンクに変更した上で、systemd-resolved を再起動します。
  • 証明書の自動更新:CoreDNSは証明書ファイルを監視し、変更時に自動で再読み込みします。CertbotやVaultなどで更新を自動化している場合は、tls ディレクティブのパスを対象のシンボリックリンクに向けておけば、サービスの再起動は不要です。
  • クライアントのトラストストア:クライアントマシンがエラーなしでDoH/DoTエンドポイントに問い合わせを行うには、ルートCAを信頼している必要があります。各Linuxクライアントの /usr/local/share/ca-certificates/internal-ca.crt にルートCAをコピーし、sudo update-ca-certificates を実行してください。
Share: