MENU

DNSSEC検証 DNSSEC破壊・検証編

目次

はじめに

何をしたいか

DNSSECの信頼の連鎖を実際に構築し、DNS応答がどのように署名・検証されるのかを確認する。また、DNSSECに関する設定や署名を意図的に破壊し、検証失敗時にフルリゾルバやクライアントからどのように見えるのかを確認する。
前回の「DNSSEC構築編」では、Parent DNSとChild DNSにDNSSECを導入し、UnboundにTrust Anchorを設定することで、lab.test から child.lab.test までの信頼の連鎖を構築した。
本記事では、構築したDNSSEC環境を意図的に破壊し、署名の不整合や信頼の連鎖の破損が発生した際に、UnboundによるDNSSEC検証およびClientからの名前解決がどのような結果になるのかを確認する。

準備・DNS構築編 https://cybermemo.blog/dnssec-lab-setup
DNSSEC構築編 https://cybermemo.blog/dnssec-lab-configuration
DNSSEC破壊・検証編 本記事

本記事で検証する構成

前回構築したDNSSEC環境を使用し、DNSSECに関する設定やDNSデータを意図的に変更して検証を行う。
正常な状態では、Unboundに設定した lab.test のTrust Anchorを起点として、Parent DNSのDNSKEY、Child DNSのDS・DNSKEY、各RRsetのRRSIGへと信頼の連鎖が成立している。
本記事では、この正常な状態を基準としてDNSSECを意図的に破壊し、正常時と異常時の応答を比較する。

              ┌─────────────┐
              │ Parent DNS  │ 192.168.56.10/24
              │    BIND9    │ 権威ゾーン: lab.test
              │             │ DNSKEY / RRSIG
              └──────┬──────┘
                     │
            child.lab.test を委任
              + DSレコード
                     │
                     ▼
              ┌─────────────┐ 192.168.56.20/24
              │  Child DNS  │ 権威ゾーン: child.lab.test
              │    BIND9    │ DNSKEY / RRSIG
              │             │ └─ www.child.lab.test → 192.168.56.100
              └─────────────┘
                     ▲
                     │ DNS問い合わせ
                     │ + DNSSEC署名情報
                     │
              ┌─────────────┐
              │  Resolver   │ 192.168.56.30/24
              │   Unbound   │ DNSSEC検証
              │             │ Trust Anchor: lab.test
              └──────┬──────┘
                     ▲
                     │ DNS問い合わせ
                     │
              ┌─────────────┐
              │   Client    │ 192.168.56.40/24
              └─────────────┘
【正常時のDNSSECの信頼の連鎖】

Trust Anchor
(lab.test KSK / DNSKEY)
        │
        ▼
lab.test DNSKEY RRset
        │
        │ ParentのDSによってChild KSKを信頼
        ▼
child.lab.test DNSKEY RRset
        │
        │ Child ZSKの公開鍵でRRSIGを検証
        ▼
www.child.lab.test A

検証環境

仮想化ソフトウェア:Oracle VirtualBox
ゲストOS:Ubuntu Server 26.04.1 LTS(amd64)
権威DNS:BIND 9.20.24
フルリゾルバ:Unbound 1.24.2
DNS確認ツール:DiG 9.20.24
内部ネットワーク:192.168.56.0/24
検証ドメイン:lab.test / child.lab.test

検証

検証1:署名済みAレコードを改ざんし、DNSSEC検証失敗を確認する

本検証では、攻撃者などによって署名済みDNSデータが改ざんされた状況を模擬するため、Child DNSのAレコードを変更し、A RRsetに対応するRRSIGは更新しない。これにより、RRsetとRRSIGが不整合となった際にUnboundがどのように検知するかを確認する。

署名済みファイルのAレコードを直接変更

Child DNS で作業。

# www.child.lab.test. 3600 IN A 192.168.56.100 → www.child.lab.test. 3600 IN A 192.168.56.200
# Serialを変更
$ sudo vim /etc/bind/db.child.lab.test.signed
※一部抜粋
; File written on Wed Sep  9 13:25:36 2026
; dnssec-signzone version 9.20.24-1ubuntu0.3-Ubuntu
child.lab.test.         3600    IN SOA  ns.child.lab.test. dnsmaster.child.lab.test. (
                                        2026091001 ; serial
(中略)
www.child.lab.test.     3600    IN A    192.168.56.200
                        3600    RRSIG   A 13 4 3600 (
                                        20261009122536 20260909122536 7661 child.lab.test.
                                        XX27GC+TFLVYuz+Jd8Ox7SyC5yxL1ifF+cuS
                                        mi+J8jcqgW8NQhR+Fi8sFaHbAEZuw691gEoZ
                                        22rXmWlCL5sbOQ== )

今回の検証の構成は下記。

署名時
www.child.lab.test A 192.168.56.100
              │
              └── ZSKで署名
                       ↓
                    RRSIG A

                ↓ Aレコードを改ざん

www.child.lab.test A 192.168.56.200
                    +
以前の192.168.56.100に対するRRSIG
                    ↓
                 不一致

BINDに改ざん後のゾーンを読み込ませる

# reload
$ sudo rndc reload child.lab.test

# Serialを確認
$ sudo rndc zonestatus child.lab.test
name: child.lab.test
type: primary
files: /etc/bind/db.child.lab.test.signed
serial: 2026091001
nodes: 3
last loaded: Thu, 10 Sep 2026 13:24:27 GMT
secure: yes
inline signing: no
key maintenance: none
next resign node: ns.child.lab.test/NSEC
next resign time: Fri, 02 Oct 2026 12:25:36 GMT
dynamic: no
reconfigurable via modzone: no

# Childが改ざん後のデータを配信していることを確認
$ dig +dnssec @192.168.56.20 www.child.lab.test A

; <<>> DiG 9.20.24-1ubuntu0.3-Ubuntu <<>> +dnssec @192.168.56.20 www.child.lab.test A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 65390
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; COOKIE: 194a0690a20c4f2e010000006aa2b07b18a3ea84a17f5fad (good)
;; QUESTION SECTION:
;www.child.lab.test.            IN      A

;; ANSWER SECTION:
www.child.lab.test.     3600    IN      A       192.168.56.200
www.child.lab.test.     3600    IN      RRSIG   A 13 4 3600 20261009122536 20260909122536 7661 child.lab.test. XX27GC+TFLVYuz+Jd8Ox7SyC5yxL1ifF+cuSmi+J8jcqgW8NQhR+Fi8s FaHbAEZuw691gEoZ22rXmWlCL5sbOQ==

;; Query time: 0 msec
;; SERVER: 192.168.56.20#53(192.168.56.20) (UDP)
;; WHEN: Thu Sep 10 13:28:27 UTC 2026
;; MSG SIZE  rcvd: 201

Client → Unbound で確認

# Resolverで作業。キャッシュ削除
$ sudo unbound-control flush_zone child.lab.test

# Clientで作業。
$ dig +dnssec @192.168.56.30 www.child.lab.test A

; <<>> DiG 9.20.24-1ubuntu0.3-Ubuntu <<>> +dnssec @192.168.56.30 www.child.lab.test A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 32814  # status: SERVFAIL, ANSWER: 0 = 問い合わせ失敗
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1  # 正常時に存在していた ad フラグなし

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;www.child.lab.test.            IN      A

;; Query time: 21 msec
;; SERVER: 192.168.56.30#53(192.168.56.30) (UDP)
;; WHEN: Thu Sep 10 13:32:32 UTC 2026
;; MSG SIZE  rcvd: 47

# ジャーナル確認
$ sudo journalctl -u unbound --since "2 minutes ago" > log
$ grep -E "validated DS|validated DNSKEY|signature mismatch|failed ANSWER|bad rrsets" log
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validated DS child.lab.test. DS IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validated DNSKEY child.lab.test. DNSKEY IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] debug: verify: signature mismatch
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validator: response has failed ANSWER rrset: www.child.lab.test. A IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: Validate: message contains bad rrsets
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] debug: verify: signature mismatch
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validator: response has failed ANSWER rrset: www.child.lab.test. A IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: Validate: message contains bad rrsets
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] debug: verify: signature mismatch
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validator: response has failed ANSWER rrset: www.child.lab.test. A IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: Validate: message contains bad rrsets
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] debug: verify: signature mismatch
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validator: response has failed ANSWER rrset: www.child.lab.test. A IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: Validate: message contains bad rrsets
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] debug: verify: signature mismatch
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validator: response has failed ANSWER rrset: www.child.lab.test. A IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: Validate: message contains bad rrsets
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] debug: verify: signature mismatch
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: validator: response has failed ANSWER rrset: www.child.lab.test. A IN
Sep 10 13:44:58 resolver unbound[1538]: [1538:0] info: Validate: message contains bad rrsets

下記の流れでエラー発生。

Client
   │
   ▼
Unbound
   │
   │ www.child.lab.test A = 192.168.56.200
   │ RRSIG A = 以前の署名
   │
   ▼
Child ZSKの公開鍵(DNSKEY)でRRSIGを検証
   │
   └── ✕ 署名が一致しない
          │
          ▼
        Bogus
          │
          ▼
       SERVFAIL

ジャーナルから読み取れる挙動は下記。

validated DS
    ↓
ChildのDSは正常

validated DNSKEY
    ↓
ChildのDNSKEYも正常

signature mismatch
    ↓
AレコードとRRSIGが一致しない

failed ANSWER rrset
    ↓
www.child.lab.test のA RRsetを検証できない

bad rrsets
    ↓
DNSSEC検証失敗

検証結果

Resolverのキャッシュを削除した後、ClientからUnboundを経由して www.child.lab.test のAレコードを問い合わせた。結果は SERVFAIL となり、ANSWER SECTIONは0件だった。また、正常時に存在していた ad フラグも付与されていない。これにより、AレコードとRRSIGが不整合な状態では、UnboundがDNSSEC検証を正常に完了できないことを確認した。

検証2:DSレコードを不整合にする

本検証では、Parent DNSに登録されている child.lab.test のDSレコードを意図的に変更し、Child DNSのKSKと不整合な状態を作成する。これにより、ParentからChildへの信頼の連鎖が成立しなくなった際に、Unboundがどのように検知するかを確認する。

Parent DNSのDSレコードを変更

Parent DNS で作業。

# 元ゾーンのDSを確認
$ grep " IN DS " /etc/bind/db.lab.test
child       IN DS 26145 13 2 C0F99483CDF9CE53B7EFBCE8607A73DCAA7B6FA728F24ECFEB01F6532742BF5D

# Serialを変更
# digestの最後の1文字だけ変更 D → E
$ sudo vim /etc/bind/db.lab.test
2026091201 ; Serial
child       IN DS 26145 13 2 C0F99483CDF9CE53B7EFBCE8607A73DCAA7B6FA728F24ECFEB01F6532742BF5E

# ファイル確認
$ cat /etc/bind/db.lab.test
$TTL 3600

@   IN  SOA ns dnsmaster (
        2026091201 ; Serial
        3600       ; Refresh
        900        ; Retry
        604800     ; Expire
        86400      ; Negative Cache TTL
)

    IN  NS  ns.lab.test.

ns  IN  A   192.168.56.10

child       IN NS ns.child
ns.child    IN A  192.168.56.20
child       IN DS 26145 13 2 C0F99483CDF9CE53B7EFBCE8607A73DCAA7B6FA728F24ECFEB01F6532742BF5E
; This is a key-signing key, keyid 25095, for lab.test.
; Created: 20260910122739 (Thu Sep 10 12:27:39 2026)
; Publish: 20260910122739 (Thu Sep 10 12:27:39 2026)
; Activate: 20260910122739 (Thu Sep 10 12:27:39 2026)
lab.test. IN DNSKEY 257 3 13 nzPV3+LMX633npZfI3Seu2cR8SrTegHIi1sCK2OtNTQyRZg/v6yVkRDl nmr52oumop7xFxsx6vcZk3emfTsFbQ==
; This is a zone-signing key, keyid 42600, for lab.test.
; Created: 20260910122712 (Thu Sep 10 12:27:12 2026)
; Publish: 20260910122712 (Thu Sep 10 12:27:12 2026)
; Activate: 20260910122712 (Thu Sep 10 12:27:12 2026)
lab.test. IN DNSKEY 256 3 13 XGx8XP7w7bqg8k9cBXoPvDLsZQFUOAuUbdr8Bd8ZSH25ip1Bq75BGFYO yZhxy0DNqEsEPPpDt2qHAW/kdlJs3g==

ここで作った状態は下記。

Child KSK
    │
    └─ 本来のSHA-256 digest
             ↓
        ...2742BF5D

Parentに登録したDS
             ↓
        ...2742BF5E
             ↑
          不一致

変更後のDSレコードを含むParentゾーンを再署名

# ゾーンファイルを確認
$ sudo named-checkzone lab.test /etc/bind/db.lab.test

# Parentゾーンを再署名
$ cd /etc/bind/keys/lab.test
$ sudo dnssec-signzone \
  -S \
  -o lab.test \
  -f /etc/bind/db.lab.test.signed \
  /etc/bind/db.lab.test
Verifying the zone using the following algorithms:
- ECDSAP256SHA256
Zone fully signed:
Algorithm: ECDSAP256SHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                            ZSKs: 1 active, 0 stand-by, 0 revoked
/etc/bind/db.lab.test.signed

改ざん後のDSに対して新しいRRSIG DSが生成される。

偽DS
  │
  └─ Parent ZSKで正常に署名
             ↓
       RRSIG DSは正常

しかし

偽DS
  ↕ 一致しない      
Child KSK

BINDに再署名後のゾーンを読み込ませる

$ sudo rndc reload lab.test
$ sudo rndc zonestatus lab.test
name: lab.test
type: primary
files: /etc/bind/db.lab.test.signed
serial: 2026091201
nodes: 4
last loaded: Sat, 12 Sep 2026 10:49:13 GMT
secure: yes
inline signing: no
key maintenance: none
next resign node: lab.test/DNSKEY
next resign time: Mon, 05 Oct 2026 09:49:13 GMT
dynamic: no
reconfigurable via modzone: no

# 変更後のDSレコードとRRSIGを確認
$ dig +dnssec +norecurse @192.168.56.10 child.lab.test DS

; <<>> DiG 9.20.24-1ubuntu0.3-Ubuntu <<>> +dnssec +norecurse @192.168.56.10 child.lab.test DS
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7197
;; flags: qr aa ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; COOKIE: c5bb3449b7fa9da2010000006aa53041466bb4c96a8f1c6c (good)
;; QUESTION SECTION:
;child.lab.test.                        IN      DS

;; ANSWER SECTION:
child.lab.test.         3600    IN      DS      26145 13 2 C0F99483CDF9CE53B7EFBCE8607A73DCAA7B6FA728F24ECFEB01F653 2742BF5E  # D → E に変更されている
child.lab.test.         3600    IN      RRSIG   DS 13 3 3600 20261012094913 20260912094913 42600 lab.test. BI3iajICF5rg6gHFcItF2XATrIZuHwPiGrBnNN/Bde02gBYnc8n62dI0 N8K3+/7d6wECpKNOjFcsUwBcnfhkQA==

;; Query time: 0 msec
;; SERVER: 192.168.56.10#53(192.168.56.10) (UDP)
;; WHEN: Sat Sep 12 10:58:09 UTC 2026
;; MSG SIZE  rcvd: 223

Child KSKから生成されるDSとの不一致を確認する

Child DNS で作業。

# Child KSK側の設定を確認
$ cd /etc/bind/keys/child.lab.test
$ dnssec-dsfromkey -2 Kchild.lab.test.+013+26145.key
child.lab.test. IN DS 26145 13 2 C0F99483CDF9CE53B7EFBCE8607A73DCAA7B6FA728F24ECFEB01F6532742BF5D

Client → Unbound で確認

# キャッシュ削除(Resolverで作業)
$ sudo unbound-control flush_zone lab.test

# Clientから確認
$ dig +dnssec @192.168.56.30 www.child.lab.test A

; <<>> DiG 9.20.24-1ubuntu0.3-Ubuntu <<>> +dnssec @192.168.56.30 www.child.lab.test A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 62414
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;www.child.lab.test.            IN      A

;; Query time: 17 msec
;; SERVER: 192.168.56.30#53(192.168.56.30) (UDP)
;; WHEN: Sat Sep 12 11:06:35 UTC 2026
;; MSG SIZE  rcvd: 47

# ジャーナル確認 ※ログは一部抜粋
$ sudo journalctl -u unbound --since "2 minutes ago" > log
$ grep -Ei "DS|DNSKEY|bogus|fail|mismatch|validate|bad|key" log
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] info: adding trusted key lab.test. DNSKEY IN
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] info: validator operate: query . DNSKEY IN
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] info: resolving . DNSKEY IN
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] debug: request has exceeded the maximum number of sends with 33
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] debug: return error response SERVFAIL
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] info: iterator operate: query . DNSKEY IN
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] info: processQueryTargets: . DNSKEY IN
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] debug: Failed to get a delegation, giving up
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] debug: return error response SERVFAIL
Sep 12 11:11:01 resolver unbound[1661]: [1661:0] info: validator operate: query . DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: generate keytag query _ta-6207.lab.test. NULL IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: validator operate: query lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: resolving lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: resolving (init part 2):  lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: resolving (init part 3):  lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: processQueryTargets: lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: sending query: lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: iterator operate: query lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: response for lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: finishing processing for lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: validator operate: query lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: validate keys with anchor(DS): sec_status_secure
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: Successfully primed trust anchor lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: validated DS child.lab.test. DS IN
(中略)
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: validator operate: query child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: resolving child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: resolving (init part 2):  child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: resolving (init part 3):  child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: processQueryTargets: child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: sending query: child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: iterator operate: query child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: response for child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: finishing processing for child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: validator operate: query child.lab.test. DNSKEY IN
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] debug: DS fail: digest is different
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] debug: Failed to match any usable DS to a DNSKEY.
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: Did not match a DS to a DNSKEY, thus bogus.
Sep 12 11:11:05 resolver unbound[1661]: [1661:0] info: Could not establish a chain of trust to keys for child.lab.test. DNSKEY IN

下記の流れでエラー発生。

Unbound
  |
  | ParentのDSを取得
  v
DS RRset + RRSIG DS
  |
  +-- ○ Parent側の署名検証は成功
  |
  v
Child DNSKEYを取得
  |
  | DNSKEYからdigestを計算
  v
Parent DS     Child KSK由来
...BF5E       ...BF5D
     \         /
      \       /
       × 不一致
          |
          v
Failed to match any usable DS to a DNSKEY
          |
          v
Bogus
          |
          v
SERVFAIL

ジャーナルから読み取れる挙動は下記。

validated DS
    ↓
ParentのDS RRset自体の署名検証は成功

DS fail: digest is different
    ↓
DSのdigestとChild DNSKEYから算出したdigestが一致しない

Failed to match any usable DS to a DNSKEY.
    ↓
DSに対応するChild DNSKEYを確認できない

Did not match a DS to a DNSKEY, thus bogus.
    ↓
DNSSEC検証でBogusと判定

Could not establish a chain of trust to keys
    ↓
ParentからChildへの信頼の連鎖を構築できない

検証結果

Resolverのキャッシュを削除した後、ClientからUnboundを経由して www.child.lab.test のAレコードを問い合わせた。結果は SERVFAIL となり、ANSWER SECTIONは0件だった。また、正常時に存在していた ad フラグも付与されていない。これにより、Parent DNSのDSレコードとChild KSKが不整合な状態では、UnboundがDNSSEC検証を正常に完了できないことを確認した。

検証3:DSレコードを削除し、Insecureになることを確認する

本検証では、Parent DNSに登録されている child.lab.test のDSレコードを削除し、ParentからChildへのDNSSECの信頼の連鎖が存在しない状態を作成する。DSレコードが存在するもののChild DNSKEYと不整合だった検証2とは異なり、DSレコードが正しく存在しない場合に、UnboundがChildゾーンをどのように扱うかを確認する。

Parent DNSからDSレコードを削除する

Parent DNS で作業。

# Serialを変更
# 元ゾーンファイルのDSレコードを削除
$ sudo vim /etc/bind/db.lab.test
$TTL 3600

@   IN  SOA ns dnsmaster (
        2026091201 ; Serial
        3600       ; Refresh
        900        ; Retry
        604800     ; Expire
        86400      ; Negative Cache TTL
)

    IN  NS  ns.lab.test.

ns  IN  A   192.168.56.10

child       IN NS ns.child
ns.child    IN A  192.168.56.20
; This is a key-signing key, keyid 25095, for lab.test.
; Created: 20260910122739 (Thu Sep 10 12:27:39 2026)
; Publish: 20260910122739 (Thu Sep 10 12:27:39 2026)
; Activate: 20260910122739 (Thu Sep 10 12:27:39 2026)
lab.test. IN DNSKEY 257 3 13 nzPV3+LMX633npZfI3Seu2cR8SrTegHIi1sCK2OtNTQyRZg/v6yVkRDl nmr52oumop7xFxsx6vcZk3emfTsFbQ==
; This is a zone-signing key, keyid 42600, for lab.test.
; Created: 20260910122712 (Thu Sep 10 12:27:12 2026)
; Publish: 20260910122712 (Thu Sep 10 12:27:12 2026)
; Activate: 20260910122712 (Thu Sep 10 12:27:12 2026)
lab.test. IN DNSKEY 256 3 13 XGx8XP7w7bqg8k9cBXoPvDLsZQFUOAuUbdr8Bd8ZSH25ip1Bq75BGFYO yZhxy0DNqEsEPPpDt2qHAW/kdlJs3g==

DS削除後のParentゾーンを再署名する

# ゾーンファイルを確認
$ sudo named-checkzone lab.test /etc/bind/db.lab.test

# Parentゾーンを再署名
$ cd /etc/bind/keys/lab.test
$ sudo dnssec-signzone \
  -S \
  -o lab.test \
  -f /etc/bind/db.lab.test.signed \
  /etc/bind/db.lab.test
Verifying the zone using the following algorithms:
- ECDSAP256SHA256
Zone fully signed:
Algorithm: ECDSAP256SHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                            ZSKs: 1 active, 0 stand-by, 0 revoked
/etc/bind/db.lab.test.signed

BINDに再署名後のゾーンを読み込ませる

$ sudo rndc reload lab.test
$ sudo rndc zonestatus lab.test
name: lab.test
type: primary
files: /etc/bind/db.lab.test.signed
serial: 2026091201
nodes: 4
last loaded: Sat, 12 Sep 2026 11:42:06 GMT
secure: yes
inline signing: no
key maintenance: none
next resign node: lab.test/DNSKEY
next resign time: Mon, 05 Oct 2026 10:42:06 GMT
dynamic: no
reconfigurable via modzone: no

# DSがANSWER SECTIONに返らないことと、DNSSECによる不在証明に関係する情報が返ってくることを確認
$ dig +dnssec +norecurse @192.168.56.10 child.lab.test DS

; <<>> DiG 9.20.24-1ubuntu0.3-Ubuntu <<>> +dnssec +norecurse @192.168.56.10 child.lab.test DS
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10626
;; flags: qr aa ra; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; COOKIE: 1a9ae6c7bc8bf2dc010000006aa53b4245473b3175ed38ac (good)
;; QUESTION SECTION:
;child.lab.test.                        IN      DS

;; AUTHORITY SECTION:
lab.test.               3600    IN      SOA     ns.lab.test. dnsmaster.lab.test. 2026091201 3600 900 604800 86400
lab.test.               3600    IN      RRSIG   SOA 13 2 3600 20261012104206 20260912104206 42600 lab.test. 3+/zAO/gAc+/0RLsZz5+9bxCTlKo9CM8It0vjtxFVgNTl89h//QS0bs7 O7JqjCRdIkMLGTxKMyoygiKlvMEq3A==
child.lab.test.         3600    IN      NSEC    ns.lab.test. NS RRSIG NSEC
child.lab.test.         3600    IN      RRSIG   NSEC 13 3 3600 20261012104206 20260912104206 42600 lab.test. JLf3igQqIgx0gh+uwOY/axOws/jSS+95NhlsvegrziN3Qn1bobeVLGcO iw66jM2zqD4ihBqS5IR4ga/reXh8/Q==

;; Query time: 0 msec
;; SERVER: 192.168.56.10#53(192.168.56.10) (UDP)
;; WHEN: Sat Sep 12 11:45:06 UTC 2026
;; MSG SIZE  rcvd: 361

NSECとそれに付いたRRSIGによって、「child.lab.test にはDSレコードが存在しない」ことがDNSSECで認証されている。

Unbound
   |
   | child.lab.test のDSは?
   v
Parent DNS
   |
   | DSは存在しない
   v
NSEC
「child.lab.testには
 NS / RRSIG / NSEC はあるが
 DSはない」
   |
   | Parent ZSK 42600によるRRSIG
   v
認証されたDSの不在

Client → Unbound で確認

# キャッシュ削除(Resolverで作業)
$ sudo unbound-control flush_zone lab.test

# Clientから確認
$ dig +dnssec @192.168.56.30 www.child.lab.test A

; <<>> DiG 9.20.24-1ubuntu0.3-Ubuntu <<>> +dnssec @192.168.56.30 www.child.lab.test A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20363
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;www.child.lab.test.            IN      A

;; ANSWER SECTION:
www.child.lab.test.     3600    IN      A       192.168.56.100
www.child.lab.test.     3600    IN      RRSIG   A 13 4 3600 20261009122536 20260909122536 7661 child.lab.test. XX27GC+TFLVYuz+Jd8Ox7SyC5yxL1ifF+cuSmi+J8jcqgW8NQhR+Fi8s FaHbAEZuw691gEoZ22rXmWlCL5sbOQ==

;; Query time: 10 msec
;; SERVER: 192.168.56.30#53(192.168.56.30) (UDP)
;; WHEN: Sat Sep 12 11:54:26 UTC 2026
;; MSG SIZE  rcvd: 173

# ジャーナル確認 
$ sudo journalctl -u unbound --since "2 minutes ago" > log
$ grep -Ei "DS|DNSKEY|secure|insecure|bogus|validate|NSEC|trust" log
Sep 12 11:54:17 resolver unbound[1611]: [1611:0] info: adding trusted key lab.test. DNSKEY IN
Sep 12 11:54:17 resolver unbound[1611]: [1611:0] info: validator operate: query . DNSKEY IN
Sep 12 11:54:17 resolver unbound[1611]: [1611:0] info: resolving . DNSKEY IN
Sep 12 11:54:17 resolver unbound[1611]: [1611:0] debug: request has exceeded the maximum number of sends with 33
Sep 12 11:54:17 resolver unbound[1611]: [1611:0] info: iterator operate: query . DNSKEY IN
Sep 12 11:54:17 resolver unbound[1611]: [1611:0] info: processQueryTargets: . DNSKEY IN
Sep 12 11:54:17 resolver unbound[1611]: [1611:0] info: validator operate: query . DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: prime trust anchor
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: validator operate: query lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: resolving lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: resolving (init part 2):  lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: resolving (init part 3):  lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: processQueryTargets: lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: sending query: lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: iterator operate: query lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: response for lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: finishing processing for lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: validator operate: query lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: validate keys with anchor(DS): sec_status_secure
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: Successfully primed trust anchor lab.test. DNSKEY IN
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: NSEC RRset for the referral proved no DS.
Sep 12 11:54:26 resolver unbound[1611]: [1611:0] info: Verified that response is INSECURE

本検証の流れは下記。

Parent
  |
  | DSなし
  | NSEC + RRSIG NSECで不在を証明
  v
Unbound
  |
  | 「このChildはDNSSECの信頼の連鎖に接続されていない」
  v
Insecure
  |
  | 名前解決は継続
  v
A = 192.168.56.100 を返す
  |
  v
adフラグは付かない

ジャーナルから読み取れる挙動は下記。

validate keys with anchor(DS): sec_status_secure
    ↓
lab.test のTrust Anchorは正常

Successfully primed trust anchor
    ↓
Parent側の信頼の起点を正常に確立

NSEC RRset for the referral proved no DS.
    ↓
child.lab.test にDSが存在しないことをNSECで確認

Verified that response is INSECURE
    ↓
ChildゾーンをInsecureとして扱う

検証結果

Resolverのキャッシュを削除した後、ClientからUnboundを経由して www.child.lab.test のAレコードを問い合わせた。結果は NOERROR となり、Aレコード 192.168.56.100 が正常に返された。一方、正常時に存在していた ad フラグは付与されていない。Unboundのログでは、NSECによってDSレコードが存在しないことが確認され、INSECURE と判定されていた。これにより、Parent DNSにDSレコードが存在しない場合、Childゾーンは Bogus ではなく Insecure として扱われ、DNSSECによる検証済みとはならないものの、名前解決自体は正常に行われることを確認した。

疑問に思ったこと

「DNSSECによる検証ができていないのに、名前解決を成功させてしまってよいのか?」という疑問を持ったので調べてみた。

なぜDNSSECが設定されていなくても名前解決ができるのか

DNSSECは、DNSSECを利用していないドメインの名前解決を拒否する仕組みではない。
インターネット上にはDNSSECを利用していないドメインも存在するため、DNSSECによる信頼の連鎖が存在しないという理由だけで名前解決を拒否してしまうと、DNSSECを利用していないドメインを名前解決できなくなってしまう。

攻撃者がDSレコードを削除してDNSSECを回避できるのではないか

攻撃者がDNS応答からDSレコードを単純に削除しただけでは、Insecure にすることはできない。
DNSSECでは、DSレコードが存在しない場合、NSEC とその RRSIG によって「DSレコードが存在しないこと」自体を認証できている。つまりUnboundは、単に「DSが返ってこなかったから Insecure」と判断したのではなく、ParentゾーンのDNSSEC署名によってDSの不在が認証されたことを確認したうえで Insecure と判定している。

実務との関連

今回の検証はDNSSECに関する攻撃そのものを再現したものではなく、攻撃や設定ミスによって発生し得るDNSSECの異常状態を意図的に作り、その際の挙動を確認したものである。

検証1:DNSデータの改ざん

検証1では、署名済みのAレコードを変更し、A RRsetとRRSIGの不整合を発生させた。
実環境では、DNSキャッシュポイズニングや通信経路上でのDNS応答改ざんなどによって、攻撃者が正規のDNSデータとは異なる情報を返そうとする状況が考えられる。
DNSSECでは、攻撃者が正しい秘密鍵を持っていなければ改ざん後のデータに対する有効なRRSIGを生成できない。そのため、検証リゾルバはRRsetとRRSIGの不整合を検知し、改ざんされたDNSデータを正当な応答として扱わない。
今回の検証では、この状態がUnboundの signature mismatch として確認でき、最終的に SERVFAIL となった。

DNSデータの改ざん
        ↓
RRsetとRRSIGが不一致
        ↓
DNSSEC検証失敗
        ↓
Bogus
        ↓
SERVFAIL

検証2:DNSSECの設定・鍵管理の不整合

検証2では、Parent DNSに登録されたDSレコードのdigestを変更し、Child KSKと対応しない状態を作成した。
実環境では、KSKロールオーバー時のDS更新ミスなどにより、Parent側のDSとChild側のDNSKEYが対応しなくなる可能性がある。
この状態では、DSレコード自体の署名検証には成功しても、DSとChild DNSKEYを結び付けることができない。そのため、ParentからChildへの信頼の連鎖を構築できず、DNSSEC検証は失敗する。
今回の検証では、Unboundのログから DS fail: digest is different および Could not establish a chain of trust を確認し、最終的に SERVFAIL となった。

DSとChild DNSKEYの不整合
        ↓
DSに対応するDNSKEYを確認できない
        ↓
信頼の連鎖を構築できない
        ↓
Bogus
        ↓
SERVFAIL

検証3:DSレコードが存在しない状態

検証3では、Parent DNSからChildのDSレコードを削除し、ParentからChildへのDNSSECの信頼の連鎖が存在しない状態を作成した。
検証2との重要な違いは、「存在するDSとDNSKEYが不整合」なのか、「そもそもDSが存在しない」のかという点にある。
DSが存在するにもかかわらず対応するDNSKEYを確認できない場合は Bogus となり、名前解決は SERVFAIL となる。一方、DSが存在しないことをNSECとRRSIGによって正当に確認できる場合、そのChildゾーンは Insecure として扱われ、DNSSECによる検証済みではないものの、名前解決自体は継続される。
実環境では、DNSSECを廃止してDSレコードを削除した場合や、レジストラ側でDSレコードが削除された場合などに、このような状態が発生し得る。

ParentにDSが存在しない
        ↓
NSEC + RRSIGによって
DSの不在を正当に確認
        ↓
DNSSECの信頼の連鎖なし
        ↓
Insecure
        ↓
名前解決は可能
ただしDNSSECによる真正性の保証はない

なお、第三者がDNS応答から単純にDSレコードを除去するだけで Insecure にできるわけではない。DSレコードが存在しないこと自体もNSECとRRSIGによって認証されるためである。ただし、本来DNSSECで保護されるべきドメインから正規のDSレコードが削除された場合は、名前解決が継続していてもDNSSECによる保護が失われるため、運用上は注意が必要となる。

まとめ

今回の検証では、DNSSECの正常な環境を意図的に破壊し、Unboundの挙動を確認した。AレコードとRRSIGが不整合な場合、およびParentのDSとChild DNSKEYが不整合な場合はBogusとなり、名前解決はSERVFAILとなった。一方、DSレコードが存在しないことをDNSSECによって正当に確認できる場合はInsecureとなり、DNSSECによる真正性は保証されないものの、名前解決自体は継続された。
今回の検証から、DNSSECでは単に「名前解決に成功するか、失敗するか」だけではなく、Secure、Bogus、Insecureの状態を区別し、信頼の連鎖のどの段階で問題が発生しているのかを確認することが重要だと分かった。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

勉強中のセキュリティエンジニアです。
初心者の目線で学んだことをまとめています。

コメント

コメントする

CAPTCHA


目次