auth-zone を使った権威・キャッシュ同居の Unbound ～ 同居しない例も添えて

・LAN 内のドメインを権威サーバーとして名前解決する
・それ以外のドメインへのクエリは反復問い合わせする
構成の例です。

:::note info
現代では、権威サーバーとキャッシュサーバーは分離することが推奨されています。
参考: JPRS トピックス＆コラム No.020
https://jprs.jp/related-info/guide/topics-column/no20.html
:::
## 前提
サーバー IP アドレス: `192.168.182.12`
AlmaLinux 9.5
Unbound 1.23.0
SELinux 有効
## OSの設定
通信ソケットバッファの拡大のため、以下を実行してカーネルパラメーターを変更します。
詳細については[公式のドキュメント](https://unbound.docs.nlnetlabs.nl/en/latest/topics/core/performance.html)を参照してください。
```
# sysctl -w net.core.rmem_max=4194304 >> /etc/sysctl.conf
# sysctl -w net.core.wmem_max=4194304 >> /etc/sysctl.conf
```
Unbound の実行ユーザー、グループを追加します。
```
# groupadd unbound
# useradd -g unbound -s /sbin/nologin unbound
```
## Unbound のインストール
標準のリポジトリにあるバージョンが古いため、前提となるパッケージをインストールし、ソースを入手してビルド、インストールします。
```
# dnf install gcc openssl-devel libevent-devel expat-devel systemd-devel bison flex
# cd /usr/src
# curl -O https://nlnetlabs.nl/downloads/unbound/unbound-latest.tar.gz
# tar xf unbound-latest.tar.gz
# cd unbound-1.23.0
# ./configure --enable-systemd --with-libevent
# make
# make install
```
## Unbound の初期設定
systemd への登録、Unbound で使用するディレクトリの作成と、所有者の変更を行います。
また、`unbound-control` 用の証明書と秘密鍵の生成を行います。
ルートゾーンのトラストアンカーも取得します。

:::note info
Unbound 1.23.0 は RFC5011 に対応しているため、自動的にトラストアンカーを更新します。
:::
```/usr/lib/systemd/system/unbound.service
[Unit]
Description=Validating, recursive, and caching DNS resolver
Before=nss-lookup.target
Requires=network-online.target
After=network-online.target

[Install]
WantedBy=multi-user.target

[Service]
Type=notify
ExecReload=/usr/sbin/unbound-control reload
ExecStartPre=/usr/local/sbin/unbound-checkconf
ExecStart=/usr/local/sbin/unbound -d -p
NotifyAccess=main
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectControlGroups=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=false
ProtectProc=invisible
ProtectSystem=strict
RuntimeDirectory=unbound
ConfigurationDirectory=unbound
StateDirectory=unbound
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
RestrictRealtime=true
SystemCallArchitectures=native
SystemCallFilter=~@clock @cpu-emulation @debug @keyring @module mount @obsolete @resources
RestrictNamespaces=yes
LockPersonality=yes
RestrictSUIDSGID=yes
ReadWritePaths=/usr/local/etc/unbound /var/log/unbound
```
```
# mkdir /usr/local/etc/unbound/files
# chown unbound:unbound -R /usr/local/etc/unbound/files
# unbound-control-setup
# sudo -u unbound /usr/local/sbin/unbound-anchor -a /usr/local/etc/unbound/files/root.key
```
## ログの設定
空のログファイルを作成し、所有者を変更します。
```
# mkdir /var/log/unbound
# touch /var/log/unbound/unbound.log
# chown -R unbound:unbound /var/log/unbound/
```
ログローテーションの設定を追加します。
```/etc/logrotate.d/unbound
/var/log/unbound/unbound.log
{
        daily
        rotate 7
        missingok
        create
        dateyesterday
        postrotate
                unbound-control log_reopen 
        endscript
}
```
## Unbound の設定
以下の定義ファイルを作成します。
```/usr/local/etc/unbound/unbound.conf
server:
    chroot: ""
    
    interface: 192.168.182.12
    interface: 127.0.0.1
    port: 53

    logfile: /var/log/unbound/unbound.log
    log-time-ascii: yes

    # サーバーの CPU コア数を設定する
    num-threads: 4 
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4 
    # サーバーの CPU コア数を設定する項目終了
    
    so-reuseport: yes
    outgoing-num-tcp: 1000
    incoming-num-tcp: 1000
    msg-cache-size: 128m
    rrset-cache-size: 256m
    num-queries-per-thread: 2048

    do-ip4: yes
    do-ip6: no
    do-daemonize: no

    access-control: 127.0.0.0/8 allow # ACL を設定 
    access-control: 192.168.182.0/24 allow
    access-control: 192.168.207.0/24 allow
    
    private-address: 192.168.0.0/16 # 左記を含む回答は破棄される DNS rebinding 対策
    private-address: 169.254.0.0/16
    private-address: fd00::/8
    private-address: fe80::/10
    private-domain: "example.com" # private-address を回答に含めるドメインを列挙
    
    local-zone: "182.168.192.in-addr.arpa" transparent # 逆引きゾーンの数だけ列挙
    
    auto-trust-anchor-file: "/usr/local/etc/unbound/files/root.key"

    hide-identity: yes
    hide-version: yes

remote-control:
    control-enable: yes
    control-interface: 127.0.0.1

    auth-zone:
        name: "example.com"
        fallback-enabled: yes
        zonefile: /usr/local/etc/unbound/example.com.zone

    auth-zone:
        name: "182.168.192.in-addr.arpa"
        fallback-enabled: yes
        zonefile: /usr/local/etc/unbound/182.168.192.in-addr.arpa.zone
```

:::note info
組み込み以外のルートヒントファイルを使用する場合は、定義ファイルに以下を追加します。
```diff:
server:
（中略）
+    root-hints: "<ルートヒントファイルの絶対パス>"
（後略）
```
:::
### ゾーンファイルの作成
以下のようなゾーンファイルを作成します。
``` /usr/local/etc/unbound/example.com.zone
$ORIGIN example.com.
$TTL 3600
@       IN      SOA     dns.example.com. root.example.com. (
                  1          ; Serial
                  900        ; refresh
                  600        ; retry
                  86400      ; expire
                  900        ; minimum
)

@                 IN NS        dns.example.com.
dns               IN A         192.168.182.12
dhcp              IN A         192.168.182.15
```
``` /usr/local/etc/unbound/182.168.192.in-addr.arpa.zone
$ORIGIN 182.168.192.in-addr.arpa.
$TTL 3600
@       IN      SOA     dns.example.com. root.example.com. (
                  1          ; Serial
                  900        ; refresh
                  600        ; retry
                  86400      ; expire
                  900        ; minimum
)

@                 IN NS        dns.example.com.
12                IN PTR       dns.example.com.
15                IN PTR       dhcp.example.com.
```
以下のコマンドにて設定ファイルのチェックを行います。
``` 
# unbound-checkconf
```
## Unbound の起動
DNS の着信接続を許可し、Unbound を起動します。
```
# firewall-cmd --add-service=dns --permanent
# firewall-cmd --reload
# systemctl enable unbound
# systemctl start unbound
```
## 確認
以下の通り解決できます。
```:権威ゾーンのレコード aa flag が立っている
# dig @192.168.182.12 example.com.

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 example.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45700
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;example.com.                   IN      A

;; AUTHORITY SECTION:
example.com.            900     IN      SOA     dns.example.com. root.example.com. 1 900 600 86400 900

;; Query time: 10 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Wed May 07 12:04:27 JST 2025
;; MSG SIZE  rcvd: 85

# dig @192.168.182.12 example.com. NS

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 example.com. NS
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54709
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;example.com.                   IN      NS

;; ANSWER SECTION:
example.com.            3600    IN      NS      dns.example.com.

;; ADDITIONAL SECTION:
dns.example.com.        3600    IN      A       192.168.182.12

;; Query time: 0 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Wed May 07 12:04:41 JST 2025
;; MSG SIZE  rcvd: 74

# dig @192.168.182.12 dns.example.com.

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 dns.example.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19297
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;dns.example.com.               IN      A

;; ANSWER SECTION:
dns.example.com.        3600    IN      A       192.168.182.12

;; Query time: 0 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Wed May 07 12:05:01 JST 2025
;; MSG SIZE  rcvd: 60

# dig @192.168.182.12 -x 192.168.182.15

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 -x 192.168.182.15
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 468
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;15.182.168.192.in-addr.arpa.   IN      PTR

;; ANSWER SECTION:
15.182.168.192.in-addr.arpa. 3600 IN    PTR     dhcp.example.com.

;; Query time: 0 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Wed May 07 12:05:23 JST 2025
;; MSG SIZE  rcvd: 86
```
```:反復問い合わせしたレコード aa flag がない
# dig @192.168.182.12 google.com.

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 google.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52179
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com.                    IN      A

;; ANSWER SECTION:
google.com.             300     IN      A       172.217.175.14

;; Query time: 180 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Wed May 07 12:05:49 JST 2025
;; MSG SIZE  rcvd: 55
```
DNSSEC 検証も正しく成功・失敗しています。
```:DNSSEC が正しく設定されているドメイン
# delv @192.168.182.12 jprs.co.jp. +vtrace
;; fetch: jprs.co.jp/A
;; validating jprs.co.jp/A: starting
;; validating jprs.co.jp/A: attempting positive response validation
;; fetch: jprs.co.jp/DNSKEY
;; validating jprs.co.jp/DNSKEY: starting
;; validating jprs.co.jp/DNSKEY: attempting positive response validation
;; fetch: jprs.co.jp/DS
;; validating jprs.co.jp/DS: starting
;; validating jprs.co.jp/DS: attempting positive response validation
;; fetch: jp/DNSKEY
;; validating jp/DNSKEY: starting
;; validating jp/DNSKEY: attempting positive response validation
;; fetch: jp/DS
;; validating jp/DS: starting
;; validating jp/DS: attempting positive response validation
;; fetch: ./DNSKEY
;; validating ./DNSKEY: starting
;; validating ./DNSKEY: attempting positive response validation
;; validating ./DNSKEY: verify rdataset (keyid=20326): success
;; validating ./DNSKEY: marking as secure (DS)
;; validating jp/DS: in fetch_callback_dnskey
;; validating jp/DS: keyset with trust secure
;; validating jp/DS: resuming validate
;; validating jp/DS: verify rdataset (keyid=53148): success
;; validating jp/DS: marking as secure, noqname proof not needed
;; validating jp/DNSKEY: in fetch_callback_ds
;; validating jp/DNSKEY: dsset with trust secure
;; validating jp/DNSKEY: verify rdataset (keyid=35821): success
;; validating jp/DNSKEY: marking as secure (DS)
;; validating jprs.co.jp/DS: in fetch_callback_dnskey
;; validating jprs.co.jp/DS: keyset with trust secure
;; validating jprs.co.jp/DS: resuming validate
;; validating jprs.co.jp/DS: verify rdataset (keyid=13611): success
;; validating jprs.co.jp/DS: marking as secure, noqname proof not needed
;; validating jprs.co.jp/DNSKEY: in fetch_callback_ds
;; validating jprs.co.jp/DNSKEY: dsset with trust secure
;; validating jprs.co.jp/DNSKEY: verify rdataset (keyid=63574): success
;; validating jprs.co.jp/DNSKEY: marking as secure (DS)
;; validating jprs.co.jp/A: in fetch_callback_dnskey
;; validating jprs.co.jp/A: keyset with trust secure
;; validating jprs.co.jp/A: resuming validate
;; validating jprs.co.jp/A: verify rdataset (keyid=43611): success
;; validating jprs.co.jp/A: marking as secure, noqname proof not needed
; fully validated
jprs.co.jp.             300     IN      A       117.104.133.165
jprs.co.jp.             300     IN      RRSIG   A 8 3 300 20250604023001 20250505023001 43611 jprs.co.jp. GNkmT72CLP6GLbPBoVCUNYDKJKdNQqEoT7mBgXr+L4sPVjh1K9oCttS6 eQrBrfBpwFQwSjOmJKk1VcN3aclxF2xd2s1UnOH8tmhKkj0tm16J1GY5 +oLQ+yOwI7MRDLZkevEnbdSjiYDI/EWOavyzEt707VizoD0JsaUTbdfF 2QI=
```
```:意図的に不正な設定がされているドメイン
# delv @192.168.182.12 dnssec-failed.mufj.jp. +vtrace
;; fetch: dnssec-failed.mufj.jp/A
;; resolution failed: SERVFAIL
```
## 補足（フォワーダーとして使用する場合）
フォワーダーとして使用する場合は以下のようにします。（Google Public DNS に転送する例）
```diff:/usr/local/etc/unbound/unbound.conf
（前略）
remote-control:
    control-enable: yes
    control-interface: 127.0.0.1

+   forward-zone:
+       name: "."
+       forward-addr: 8.8.8.8

    auth-zone:
        name: "example.com"
        fallback-enabled: yes
        zonefile: /usr/local/etc/unbound/example.com.zone

    auth-zone:
        name: "182.168.192.in-addr.arpa"
        fallback-enabled: yes
        zonefile: /usr/local/etc/unbound/182.168.192.in-addr.arpa.zone
```
## 補足（権威サーバーへ転送する場合）
権威サーバーにクエリを転送したい場合は、`stub-zone` を使用します。
定義ファイルは以下のようになります。
`192.168.182.10` 上の権威サーバーは以下記事同等のものとします。

https://qiita.com/ahra_/items/75056bc86d56e0bc773e

```diff:/usr/local/etc/unbound/unbound.conf
server:
    chroot: ""
    
    interface: 192.168.182.12
    interface: 127.0.0.1
    port: 53

    logfile: /var/log/unbound/unbound.log
    log-time-ascii: yes

    # サーバーの CPU コア数を設定する
    num-threads: 4 
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4 
    # サーバーの CPU コア数を設定する項目終了
    
    so-reuseport: yes
    outgoing-num-tcp: 1000
    incoming-num-tcp: 1000
    msg-cache-size: 128m
    rrset-cache-size: 256m
    num-queries-per-thread: 2048

    do-ip4: yes
    do-ip6: no
    do-daemonize: no

    access-control: 127.0.0.0/8 allow # ACL を設定 
    access-control: 192.168.182.0/24 allow
    access-control: 192.168.207.0/24 allow
    
    private-address: 192.168.0.0/16 # 左記を含む回答は破棄される DNS rebinding 対策
    private-address: 169.254.0.0/16
    private-address: fd00::/8
    private-address: fe80::/10
    private-domain: "example.com" # private-address を回答に含めるドメインを列挙

+    local-zone: "example.com" transparent
    local-zone: "182.168.192.in-addr.arpa" transparent # 逆引きゾーンの数だけ列挙
    
    auto-trust-anchor-file: "/usr/local/etc/unbound/files/root.key"

    hide-identity: yes
    hide-version: yes

+    domain-insecure: "example.com" # 対象ゾーンでの DNSSEC 検証を無効にする
+    domain-insecure: "182.168.192.in-addr.arpa"

remote-control:
    control-enable: yes
    control-interface: 127.0.0.1
    
+    stub-zone: # クエリを権威サーバーに転送
+        name: "example.com"
+        stub-addr: 192.168.182.10

+    stub-zone:
+        name: "182.168.192.in-addr.arpa"
+        stub-addr: 192.168.182.10
```
この設定では、以下のように解決されます。
```:権威を持たないため aa flag が立っていない
# dig @192.168.182.12 example.com.

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 example.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 65394
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;example.com.                   IN      A

;; AUTHORITY SECTION:
example.com.            701     IN      SOA     dns1.example.com. root.example.com. 0 900 600 86400 900

;; Query time: 0 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Mon May 19 09:49:46 JST 2025
;; MSG SIZE  rcvd: 86

# dig @192.168.182.12 dns1.example.com.

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 dns1.example.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4964
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;dns1.example.com.              IN      A

;; ANSWER SECTION:
dns1.example.com.       3517    IN      A       192.168.182.10

;; Query time: 0 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Mon May 19 09:49:49 JST 2025
;; MSG SIZE  rcvd: 61

# dig @192.168.182.12 -x 192.168.182.10

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @192.168.182.12 -x 192.168.182.10
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31862
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;10.182.168.192.in-addr.arpa.   IN      PTR

;; ANSWER SECTION:
10.182.168.192.in-addr.arpa. 3542 IN    PTR     dns1.example.com.

;; Query time: 0 msec
;; SERVER: 192.168.182.12#53(192.168.182.12) (UDP)
;; WHEN: Mon May 19 09:49:51 JST 2025
;; MSG SIZE  rcvd: 86
```