本文へ移動
購読
GUIDE#infra更新 2026.09.24

Unboundで自宅用キャッシュDNSサーバーを構築する:DNS over TLSとDNSSEC検証

UnboundでLAN用のキャッシュDNSサーバーを構築。DNS over TLSでの転送、DNSSEC検証、home.arpaの名前、REFUSEDやSERVFAILの切り分けまでをdigの結果で確かめます。

level
中級
time
約30分
stack
Unbound 1.19 / Ubuntu 24.04 / dig
author
@ryoma_t
// 前提条件
Ubuntu 24.04のサーバーとsudo権限 / ルーターのDHCPでDNSサーバーを変更できること
unbound-checkconfで設定を検証し、DNSSEC検証の成功と失敗、ローカルゾーンの応答、キャッシュ統計をdigとunbound-controlで確認したターミナル
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

Unboundとは、オランダのNLnet Labsが開発しているオープンソースのキャッシュDNSサーバーで、LAN内の端末から名前解決の問い合わせを受けて答えを探し、その結果をキャッシュして返すソフトウェアです。DNSSECの署名検証とDNS over TLS(DoT)での問い合わせ転送を標準で備え、設定ファイル1つで自宅や小さなオフィスのDNSサーバーを構築できます。

ここではUbuntu 24.04のパッケージ(Unbound 1.19.2、執筆時点)を使い、上位の問い合わせをDoTで暗号化し、DNSSECを検証し、nas.home.arpa のようなLAN内の名前も返すキャッシュDNSサーバーを作ります。設定はすべてコンテナの中で実際に起動し、digの結果で確かめています。

この記事でわかること

  • 権威DNSとキャッシュDNSの違いと、BINDではなくUnboundを選ぶ理由
  • ルートから自分で辿る「フルリゾルバー」とDoTでの転送のどちらにするかの判断基準
  • Ubuntu 24.04のパッケージ構成と、そのまま使える設定ファイル一式
  • DNSSEC検証とLAN内の名前(home.arpa)を確かめるdigの読み方
  • REFUSED、SERVFAIL、bindエラーが出たときの原因と直し方

Unboundとは:権威DNSサーバーとキャッシュDNSサーバーの違い

DNSサーバーには役割の違う2種類があります。自分が管理するドメイン(ゾーン)について答えるのが権威DNSサーバー、端末の代わりにあちこちの権威サーバーへ問い合わせて答えを集め、キャッシュするのがキャッシュDNSサーバー(フルリゾルバー)です。このソフトは後者専用で、自分のゾーンを外部に公開する機能は持ちません。

種類役割代表的なソフト自宅・小規模オフィスでの置き方
権威DNSサーバー自分のゾーンについて答えるNSD、Knot DNS、BIND、PowerDNS公開ドメインはレジストラやDNS事業者に任せる
キャッシュDNSサーバー端末の代わりに答えを探し、キャッシュするUnbound、BIND、PowerDNS RecursorLANに1台置き、DHCPで配る
フォワーダー受けた問い合わせを別のリゾルバーに渡すdnsmasq、systemd-resolved、家庭用ルーター端末やルーターに最初から入っている

BINDは権威とキャッシュの両方を1つのプロセスで扱えますが、そのぶん設定項目が多く、両方の役割を同じサーバーに載せると、片方の設定ミスや脆弱性がもう片方に波及します。LANのキャッシュDNSとして使うだけなら、機能を絞ったUnboundの方が設定が短く、Ubuntuのパッケージでは最初からDNSSEC検証が有効になっています。逆に、動的更新を受け付ける社内ゾーンを本格的に運用したい場合(Active Directoryの名前解決など)は、Unboundの local-data では足りないので、別の製品を選びます。

2つの動かし方:フルリゾルバーとDNS over TLSでの転送

Unboundは、ルートサーバーから順に権威サーバーを辿る「フルリゾルバー」としても、決めた公開リゾルバーへ問い合わせを丸ごと渡す「フォワーダー」としても動きます。フォワーダーにする場合は、転送をDNS over TLS(TCP 853番のTLS)で暗号化できます。

観点フルリゾルバーDoTで転送
問い合わせ先ルートから順に各ドメインの権威サーバー指定した公開リゾルバー(例:1.1.1.1)
経路の暗号化なし(53番の平文)あり(853番のTLS)
問い合わせの履歴が見える相手経路上のISPと、各権威サーバー(それぞれ一部)転送先の事業者(すべて)
DNSSECの検証Unboundが行うUnboundが行う(転送先の判定を鵜呑みにしない)
必要な通信UDP/TCP 53番の外向きTCP 853番の外向き

応答速度はどちらを選んでも大差ありません。この検証環境で同じ3つの名前を引いたところ、キャッシュのない初回はどちらの方式も14〜160ミリ秒の範囲でばらつき、2回目以降はすべて0ミリ秒でした。決め手は速さではなく、問い合わせ履歴を誰に見せるかです。ISPに平文の問い合わせを見せたくないならDoT転送、特定の事業者に履歴を集約させたくないならフルリゾルバーを選びます。迷ったらDoT転送から始め、あとで forward-zone を消せばフルリゾルバーに切り替えられます。

インストールとファイル構成(Ubuntu 24.04)

terminal
sudo apt update
sudo apt install unbound dnsutils
unbound -V | head -1
# Version 1.19.2   ← 執筆時点のUbuntu 24.04の版

Ubuntuのパッケージでは、/etc/unbound/unbound.conf には include-toplevel: "/etc/unbound/unbound.conf.d/*.conf" の1行しかなく、実際の設定は unbound.conf.d/ に分かれています。最初から入っているのは2つで、remote-control.conf は管理コマンド unbound-control をUNIXソケット(/run/unbound.ctl)経由で有効にし、root-auto-trust-anchor-file.conf はDNSSECの起点となるルートの鍵を /var/lib/unbound/root.key から読み、自動で更新するよう指定しています。systemdのユニットは起動前に unbound-helper root_trust_anchor_update を実行し、このファイルを用意します。

もう1つ気をつけるのが、127.0.0.53 と 127.0.0.54 の53番で待ち受けているsystemd-resolvedとの関係です。127.0.0.1 とLAN側のIPアドレスのように具体的なアドレスで待ち受けさせれば共存できますが、0.0.0.0 を指定するとポートが衝突して起動しません。この記事の設定は具体的なアドレスで書きます。

キャッシュDNSサーバーを構築する:設定ファイルを書く

自分の設定は unbound.conf.d/ に新しいファイルとして置きます。サーバーのLAN側アドレスを 192.168.10.2、LANを 192.168.10.0/24 とした例です。

/etc/unbound/unbound.conf.d/home.conf
server:
    interface: 127.0.0.1
    interface: 192.168.10.2
    ip-freebind: yes
    port: 53
    access-control: 127.0.0.0/8 allow
    access-control: 192.168.10.0/24 allow

    hide-identity: yes
    hide-version: yes
    prefetch: yes
    serve-expired: yes
    serve-expired-ttl: 86400
    msg-cache-size: 32m
    rrset-cache-size: 64m
    log-servfail: yes

    private-address: 10.0.0.0/8
    private-address: 172.16.0.0/12
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: fd00::/8
    private-address: fe80::/10
    private-domain: "home.arpa"

    local-zone: "home.arpa." static
    local-data: "nas.home.arpa. IN A 192.168.10.20"
    local-data: "pve.home.arpa. IN A 192.168.10.11"
    local-data-ptr: "192.168.10.20 nas.home.arpa"

    tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt

forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 1.1.1.1@853#cloudflare-dns.com
    forward-addr: 1.0.0.1@853#cloudflare-dns.com

上から順に意味を説明します。interface は待ち受けるアドレスで、ip-freebind: yes は、起動時にまだそのアドレスがインターフェースに付いていなくても待ち受けを始める設定です。ネットワークの準備より先にUnboundが起動して失敗する、という起動順の問題を避けられます。access-control に書いていない送信元からの問い合わせは拒否(REFUSED)されるので、LANの範囲を忘れずに書きます。

prefetch は、よく引かれる名前のTTLが切れる少し前に先回りして取り直す設定、serve-expired と serve-expired-ttl: 86400 は、上位に届かないときにTTL切れのキャッシュを最大1日分返し続ける設定です。回線が一時的に落ちても、普段使っているサイトの名前解決は止まりません。キャッシュサイズは、rrset-cache-size を msg-cache-size の2倍程度にするのが釣り合いの取れた割合です。log-servfail: yes は、SERVFAILを返した理由を1行でログに残します。

private-address は、外部のドメイン名の答えにプライベートIPが含まれていたら捨てる設定で、DNSリバインディング攻撃(外部のWebページからLAN内の機器を操作させる手口)を防ぎます。自分のドメインでLAN内のアドレスを返すように運用している場合は、そのドメインを private-domain に足して例外にします。CaddyでDNS-01を使ってLAN専用サーバーをHTTPS化する構成では、home.example.com をここに加えます。

最後の forward-zone がDoT転送です。1.1.1.1@853#cloudflare-dns.com は「1.1.1.1の853番にTLSで接続し、証明書が cloudflare-dns.com のものか検証する」という意味で、tls-cert-bundle が検証に使うCA証明書の束です。この forward-zone ブロックを丸ごと消せば、フルリゾルバーとして動きます。

このパッケージの版は起動時に warning: subnetcache: prefetch is set but not working for data originating from the subnet module cache. という警告を出します。サブネット情報付きの問い合わせ(ECS)を扱うモジュール向けの注意で、この構成の動作には影響しません。

起動とDNSSEC検証の確認

terminal
sudo unbound-checkconf
sudo systemctl restart unbound
dig @127.0.0.1 jprs.jp +dnssec | grep -E 'status|flags:'
dig @127.0.0.1 dnssec-failed.org | grep status
dig @127.0.0.1 dnssec-failed.org +cd +short

unbound-checkconf が no errors in /etc/unbound/unbound.conf を返してから再起動します。interface や port を変えたときは、reloadではなくrestartが必要です。検証中に interface を1行足して unbound-control reload したところ、応答は ok なのに新しいアドレスでは待ち受けておらず、digは connection refused になりました。

検証の結果は、digの flags: 行で読みます。署名付きの jprs.jp を引くと flags: qr rd ra ad と、検証済みを示す ad(Authenticated Data)が付きます。署名をわざと壊してある試験用ドメイン dnssec-failed.org は status: SERVFAIL になり、検証を無効にする +cd(Checking Disabled)を付けたときだけアドレスが返ります。この2つが揃えば、Unboundは改ざんされた応答を端末に渡さずに止めています。

署名には有効期間があるため、サーバーの時計が大きくずれていると、正しい署名まで期限切れと判定され、すべての名前がSERVFAILになります。RTC(時計用の電池)のないRaspberry Piは、起動直後にNTPで時刻が合うまでこの状態になりがちです。timedatectl で System clock synchronized: yes を確かめてから切り分けを始めてください。

LAN内の名前を返す:local-zoneの種類と使い分け

LAN内の機器に名前を付けるなら、ドメインは home.arpa を使います。家庭内ネットワーク用にRFC 8375で予約された名前で、インターネット上の名前と衝突しません。.local はmDNS(Bonjour)が使うので、ここで使うとmacOSやiPhoneで引けなくなることがあります。

local-zoneの種類動き使いどころ
staticlocal-data に書いた名前だけ答え、それ以外はNXDOMAINhome.arpa のような内部専用のゾーン
transparentlocal-data に書いた名前は答え、それ以外は通常どおり外へ問い合わせる公開ドメインの一部の名前だけLAN内のIPに向けたい場合
redirectゾーン配下のすべての名前に同じ答えを返すワイルドカードで1台のリバースプロキシに向ける
always_nxdomain存在しない扱いにする特定ドメインの遮断

公開ドメイン example.com を持っていて、nas.home.example.com だけLANのアドレスに向けたい場合(スプリットDNS)は、local-zone: "home.example.com." transparent と local-data: "nas.home.example.com. IN A 192.168.10.20" を書き、あわせて private-domain: "home.example.com" を足します。広告ドメインの遮断も always_nxdomain で書けますが、リストの自動更新やクライアントごとの設定まで考えると、AdGuard Homeを前段に置いて役割を分ける方が管理が楽です。

クライアントから使う:ルーターのDHCPとsystemd-resolved

LAN全体で使うには、ルーターのDHCPサーバー設定でDNSサーバーのアドレスを 192.168.10.2 にします。端末はDHCPのリースを更新した時点で新しいDNSを使い始めます。VLANでネットワークを分けている場合は、各VLANのDHCPで同じアドレスを配り、access-control にそのVLANのサブネットを足します。VLAN間のファイアウォールで53番を通すことも忘れないでください(設計はVLANでネットワークを分離する手順を参照)。

サーバー自身の名前解決も、systemd-resolved経由でUnboundに向けます。

/etc/systemd/resolved.conf.d/unbound.conf
[Resolve]
DNS=127.0.0.1
Domains=~.
terminal
sudo systemctl restart systemd-resolved
resolvectl status | grep -A2 'Global'

広告ブロックも行いたい場合は、AdGuard Homeで家庭内の広告をDNSでブロックする構成の上位サーバーにUnboundを指定します。端末からの53番はAdGuard Homeが受け、Unboundは port: 5053 のように別のポートで待ち受けます。AdGuard HomeをDockerのブリッジネットワークで動かしている場合、コンテナから見た 127.0.0.1 はコンテナ自身なので、Unboundはホストのアドレス(例:192.168.10.2@5053)で待ち受けさせ、access-control にDockerのネットワーク(例:172.16.0.0/12)も加えます。

運用:統計とキャッシュ操作

terminal
sudo unbound-control stats_noreset | grep -E '^total.num.(queries|cachehits|cachemiss)='
sudo unbound-control list_forwards
sudo unbound-control flush www.example.com
sudo unbound-control flush_zone example.com

stats_noreset はカウンターを消さずに統計を表示します(stats は表示と同時にゼロに戻します)。cachehits と cachemiss の比率を見れば、キャッシュがどの程度効いているかがわかります。list_forwards は転送先の一覧で、この記事の設定では . IN forward 1.1.1.1 1.0.0.1 と表示されます。Webサイトのサーバー移転直後に古いアドレスが返り続けるときは、flush で1つの名前を、flush_zone でドメイン配下をまとめてキャッシュから消します。

問い合わせの内容をすべて記録する log-queries: yes もありますが、家族全員の閲覧先がログに残ることになります。トラブルの調査中だけ有効にし、終わったら戻すのが無難です。

つまずきポイント

LAN内の端末からだけstatus: REFUSEDになる

サーバー上の dig @127.0.0.1 は通るのに、他の端末からはREFUSEDになる場合は、access-control にその端末のサブネットがありません。検証環境でも、許可していないアドレスから問い合わせると status: REFUSED が返りました。VLANを増やしたときに書き足し忘れるのが典型です。

起動しない:bind: address already in use

error: bind: address already in use と fatal error: could not open ports が出る場合は、同じアドレスの53番を別のプログラムが使っています。sudo ss -lunp 'sport = :53' で相手を確認します。dnsmasqやAdGuard Homeが同じサーバーで動いているか、Unboundに 0.0.0.0 を指定してsystemd-resolvedと衝突していることがほとんどです。

しばらく待たされたあとSERVFAILになる

DoTの転送先に届いていません。853番の外向き通信が塞がれていると、Unboundは転送先の応答を待ち続け、数十秒後にSERVFAILを返します。log-servfail: yes にしておくと、SERVFAIL <www.example.net. A IN>: all the configured stub or forward servers failed, at zone . from 192.0.2.1 upstream server timeout のように、どの転送先で失敗したかがログに出ます(到達できないアドレスを転送先にして再現した例です)。ルーターやUTMで853番を許可するか、forward-zone を外してフルリゾルバーにします。

動作確認

設定が意図どおり動いているかは、次の5つのコマンドでまとめて確かめられます。

terminal
$ unbound-checkconf
unbound-checkconf: no errors in /etc/unbound/unbound.conf

$ dig @127.0.0.1 jprs.jp +dnssec | grep -E 'status|flags:'
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51799
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags: do; udp: 1232

$ dig @127.0.0.1 dnssec-failed.org | grep status
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 10216

$ dig @127.0.0.1 nas.home.arpa +short
192.168.10.20

$ sudo unbound-control stats_noreset | grep -E '^total.num.(queries|cachehits|cachemiss)='
total.num.queries=4
total.num.cachehits=1
total.num.cachemiss=3

ad フラグ付きのNOERROR、試験用ドメインのSERVFAIL、home.arpa の答え、そして統計のカウンターが問い合わせのたびに増えることの4点が確認できれば完成です。EDNS の行の flags: do は、digが +dnssec で「署名も返してほしい」と要求したことを示すもので、検証の成否とは別の表示です。端末側では、Windowsなら nslookup nas.home.arpa、macOSやLinuxなら dig nas.home.arpa で、応答したサーバーが 192.168.10.2 になっているかも見ておきます。

よくある質問

BINDとUnboundの違いは何ですか?

BINDは権威DNSとキャッシュDNSの両方を1つのソフトで扱える多機能な実装で、Unboundはキャッシュ(フルリゾルバー)専用です。LANの名前解決だけなら、機能を絞ったUnboundの方が設定が短く、役割が混ざらないぶん安全に運用できます。自分のゾーンを外部に公開するなら、権威DNS側の製品を別に選びます。

Unboundの設定方法は?

/etc/unbound/unbound.conf.d/ に .conf ファイルを作り、server: の下に待ち受けアドレス(interface)と許可する送信元(access-control)を書くのが最低限です。DoTで転送するなら forward-zone を足し、unbound-checkconf で検査してから再起動します。

Unboundとはどういう意味ですか?

英語の unbound は「縛られていない」という意味の形容詞です。DNSの文脈では、オランダの非営利団体NLnet Labsが開発し、BSDライセンスで公開しているキャッシュDNSサーバーの名前を指します。日本語ではそのまま「アンバウンド」と読みます。

UbuntuでDNSの設定は?

Ubuntu 24.04では、systemd-resolvedが 127.0.0.53 で問い合わせを受けて実際のDNSサーバーへ転送します。DNSサーバーはnetplanの nameservers: か、resolved.conf.d/ の DNS= で指定し、resolvectl status で確認します。/etc/resolv.conf の直接編集は上書きされます。

まとめ

Unboundは、interface と access-control で受け付ける範囲を決め、DoTの forward-zone を付けるかどうかでプライバシーの考え方を選び、local-zone でLAN内の名前を足す、という3つの判断で組み上がります。DNSSECが効いているかは jprs.jp の ad フラグと dnssec-failed.org のSERVFAILで確かめられ、log-servfail を入れておけば障害時の切り分けも1行のログから始められます。DNSはLANのすべての通信の入口なので、この1台が止まったときに備え、DHCPで配るDNSを2台目に切り替える手順も決めておいてください。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。