= BIND9によるダイナミックDNSサーバーの運用 =
<<TableOfContents(3)>>

== 検証環境 ==
 * 以下のソフトウェアの利用を前提に検証を実施した。いずれも現時点で最新のリリースに基づいて検証しているが、ある程度古い環境、より新しい環境でも問題無いと思われる。
   * OS: FreeBSD 11.2-R
   * DNSコンテンツサーバー: BIND9 9.13.3
 * 上記以外の環境では、以下の点に相違が発生する。当該環境に応じて読み替えたし。
   * インストール方法
   * インストールされるディレクトリ
 * 逆に以下の点は参考にできる。
   * 設定パラメータとその意味
   * 運用事例

== 検証作業内容 ==
 * DNSコンテンツサーバーでは「example.jp」ゾーンを運用する。
 * 本サーバーでは外部(自身を含む)からのダイナミックアップデート要求に対し、「exmaple.jp」ゾーンに対してレコードの追加・変更・削除を実施する。

== DNSコンテンツサーバーの基本設定 ==
 * DNSサーバーは [[https://www.freshports.org/dns/bind913|ports/dns/bind913]] をインストールする。
 * ここでは特にカスタムする要素は無いので、デフォルトのままとする。
 * 説明のため、稼働に必要な最低限の設定とする。

=== /etc/rc.conf ===
以下の設定を追加する。

{{{
named_enable="YES"
named_chrootdir="/var/named"
altlog_proglist="named"
}}}

※altlog_proglist が既に設定済みの場合、「{{{ named}}}」を追加する。

=== /usr/local/etc/namedb/named.conf ===
{{{#!diff
--- /usr/local/etc/namedb/named.conf.sample     2018-09-13 22:12:15.847935000 +0900
+++ /usr/local/etc/namedb/named.conf    2018-09-13 22:46:26.048986000 +0900
@@ -16,15 +16,20 @@
        dump-file       "/var/dump/named_dump.db";
        statistics-file "/var/stats/named.stats";

+       recursion       no;
+       allow-query       { any; };
+       allow-recursion   { none; };
+       allow-query-cache { none; };
+
 // If named is being used only as a local resolver, this is a safe default.
 // For named to be accessible to the network, comment this option, specify
 // the proper IP address, or delete this option.
-       listen-on       { 127.0.0.1; };
+       listen-on       { any; };

 // If you have IPv6 enabled on this system, uncomment this option for
 // use as a local resolver.  To give access to the network, specify
 // an IPv6 address, or the keyword "any".
-//     listen-on-v6    { ::1; };
+       listen-on-v6    { any; };

 // These zones are already covered by the empty zones listed below.
 // If you remove the related empty zones below, comment these lines out.
}}}

 * DNSコンテンツサーバーとして、以下の設定を行うものとする。
   * IPv4/IPv6問わず、全てのクエリを受け付ける。
   * 再帰問い合わせには答えないものとする。
   * よって本設定が行われているゾーン情報のみクエリに対して答える(ルートゾーンを含む)。
 * DNSSEC設定/運用、ログ出力設定、ビュー設定、マスター/スレイブ構成、といった機能については説明しない。
 * なお原理上、本設定はマスターサーバーで実施するものであり、スレイブサーバーには適用しない/するものではない。

= example.jp ゾーンの定義 =

== /usr/local/etc/namedb/TSIGキー名.key ==
下記コマンドを実行してTSIGキーを発行する。

{{{
tsig-keygen -a hmac-sha256 TSIGキー名. > /usr/local/etc/namedb/TSIGキー名.key
chown bind:wheel /usr/local/etc/namedb/TSIGキー名.key
chmod 0400       /usr/local/etc/namedb/TSIGキー名.key

}}}

=== TSIG ===
TSIG(Transaction SIGnature)は、共有秘密鍵と一方向ハッシュ関数を使用してトランザクションレベルの認証をDNSプロトコルにもたらす仕組みである。

TSIGはTSIGキー名と秘密鍵(secret)からなり、以下のようなフォーマットになる(BIND9では)。

{{{
key "ns-www." {
    algorithm hmac-sha256;
    secret "PfzeGvXiOqtPOwQJY/iNFrvlD3/eKAHRZ0TbyK5GYII=";
};
}}}

 * 通常、これの発行は tsig-keygen コマンドを使用して作成する。
 * 決めないと行けないのは「TSIGキー名」(上記例では ns-www.)となる。
 * algorithm は今のところ「hmac-sha256」で十分で変える必要は無い(数年レベルで)。
 * secret は自動生成に任すに限る。

肝心のTSIGキー名についてだが、
 * [[https://ftp.isc.org/isc/bind9/cur/9.13/doc/arm/Bv9ARM.ch04.html#tsig|BIND9.13のマニュアル]]によれば「ホスト名1-ホスト名2.」を例示している。
 * ホスト1はDNSサーバー側、ホスト2はダイナミックDNSアップデート(DNS)クライアント側とし、ショートネームで指定している。
 * 例えば、DNSサーバーが ns.example.jp、ダイナミックDNSアップデートクライアント側 www.example.jp である場合、「ns-www.」とするのが妥当(ほんと?)。
 * まぁなんでもいいけど、わかりやすいようにね。 

== /usr/local/etc/namedb/named.conf ==
{{{#!diff
--- /usr/local/etc/namedb/named.conf.orig       2018-09-13 22:46:26.048986000 +0900
+++ /usr/local/etc/namedb/named.conf    2018-09-26 14:33:08.706240000 +0900
@@ -383,3 +383,11 @@
        };
 };
 */
+
+include "/usr/local/etc/namedb/TSIGキー名.key";
+
+zone "example.jp" {
+    type master;
+    file "/usr/local/etc/namedb/dynamic/example.jp";
+    allow-update { !{ !IPv4アドレス; !IPv6アドレス; any; }; key "TSIGキー名."; };
+};
}}}

== /usr/local/etc/namedb/dynamic/example.jp ==
{{{
$TTL    300

@       IN SOA ns.example.jp. domain.example.jp. (
                1          ; serial
                7200       ; refresh (2 hours)
                900        ; retry (15 minutes)
                2419200    ; expire (4 weeks)
                86400      ; minimum (1 day)
                )
        IN NS   ns
ns      IN A    IPv4アドレス
        IN AAAA IPv6アドレス
}}}

 * ここでは参考として SOA と NS レコードのみ設定するものとする。
 * また本ファイル作成後、オーナーとパーミッションを以下の通り設定すること。

{{{
chown bind:wheel /usr/local/etc/namedb/dynamic/example.jp
chmod 0644       /usr/local/etc/namedb/dynamic/example.jp
}}}

= 動作検証 =

== 動作検証環境 ==
 * 以下のソフトウェアの利用を前提に検証を実施した。いずれも現時点で最新のリリースに基づいて検証しているが、ある程度古い環境、より新しい環境でも問題無いと思われる。
   * OS: FreeBSD 11.2-R
   * DNSクライアントツール: BIND9([[https://www.freshports.org/dns/bind-tools|ports/dns/bind-tools]]) ※現時点では 9.12.2P2
 * ただし、古いFreeBSD(9.x等)標準の nsupdate コマンドは、TSIGを取り扱えない(コンパイル時無効)ため、やはり ports からインストールする必要がある。 
 * また既にBIND9サーバー(ports/dns/bind913 など)をインストールしている環境では ports/dns/bind-tools をインストールする必要は無い。
 * 実運用時の懸念点については [[#【付録】動的更新の安定性について]]を参照すること。

== 動作検証環境構築および確認 ==
 * ダイナミックDNSクライアント側に「/usr/local/etc/namedb/TSIGキー名.key」ファイルをコピーする。
 * その際オーナー・パーミッション設定に留意すること。この時、具体的な設置場所については利用するツールに応じて決める。
 * クライアントは「allow-update」で指定した「IPv4アドレス」ないし(and/or)は「IPv6アドレス」であることを確認する。
 * つまり「allow-update」による指定は、指定IPアドレス以外のダイナミックDNS更新は許可しないことに注意。
 * その代わり当該ゾーンの全てのレコードに対してアクセス(追加・変更・削除)可能である。

== 動作検証の実施 ==
以下のコマンドを実行することにより検証を行う(インタラクティブモードで検証)。
{{{
# dig +short ANY test.example.jp @ダイナミックDNSサーバーのIPアドレス
# /usr/local/bin/nsupdate -v -k TSIGキー名.key
> local allow-updateで許可したダイナミックDNSクライアントのIPアドレス
> server ダイナミックDNSサーバーのIPアドレス
> update add test.example.jp. 5 TXT "hello, world."
> send
> quit
# dig +short ANY test.example.jp @ダイナミックDNSサーバーのIPアドレス
"hello, world."
# 
}}}
 * この時正常に動作すれば「send」送信後には何もメッセージは表示されない。
 * 許可IPアドレス設定(allow-update)にミスがあれば「update failed: REFUSED」が表示される。

※検証時における「local」指定について
 * 通常の運用においてクライアント自身のIPを指定する「local」の指定は行う必要は無い。
 * 設定が正しいかをまず確認するために指定するもので、検証段階で問題があれば実運用もおぼつかないため、切り分けのために指定する。
 * この段階で問題無いことを確認したならば、「local」指定を無くし、正常に動作することを確認すること。
 * nsupdate コマンドが自動で見つけてくる「ダイナミックDNSクライアントのIPアドレス」が適切か検証できる。

= よくある質問とその答え =

== Q.「response to SOA query was unsuccessful」という見慣れないエラーが発生しました。なぜです?設定は完璧なはずです! ==
{{{
# dig +short ANY test.example.jp @ダイナミックDNSサーバーのIPアドレス
# /usr/local/bin/nsupdate -v -k TSIGキー名.key
> local allow-updateで許可したダイナミックDNSクライアントのIPアドレス
> server ダイナミックDNSサーバーのIPアドレス
> update add test.example.jp. 5 TXT "hello, hello, world!"
> send
response to SOA query was unsuccessful
# dig +short ANY test.example.jp @ダイナミックDNSサーバーのIPアドレス
# 
}}}

A.甘いな。激甘だな。完璧などとおこがましい。
ログファイル<<FootNote(named のデフォルトファシリティは daemon であること、/etc/syslog.conf を変更してない場合、デフォルトのログファイルは /var/log/messages である。)>>を見れば、
named 起動時に下記のようなメッセージを確認できるはず。
{{{
named[21358]: zone example.jp/IN: loading from master file /usr/local/etc/named/dynamic/example.jp failed: file not found
named[21358]: zone example.jp/IN: not loaded due to errors.
}}}
このケースの場合「file not found」つまりファイル名の指定に間違いがある(×etc/named/、○etc/namedb/)。精進すべし。

正しく設定した場合はログファイルには以下の通り出ている。
{{{
named[21470]: zone example.jp/IN: loaded serial 1
}}}

== Q.実環境でテストしてしまいました!どうやって削除すればいいですか?ゾーンファイル編集すれば良いですか? ==
A.ダイナミックDNSで追加したならダイナミックDNSで削除しないと。
(サービス停止して)オフラインであるならゾーンファイル編集でもかまいません。
ゾーンファイルは独特な(機械処理故の限界な)フィードバックが行われていますので編集の際、驚かないでください。

{{{
# dig +short ANY test.example.jp @ダイナミックDNSサーバーのIPアドレス
"hello, world."
# /usr/local/bin/nsupdate -v -k tsig.key
> local allow-updateで許可したダイナミックDNSクライアントのIPアドレス
> server ダイナミックDNSサーバーのIPアドレス
> update delete test.example.jp. TXT
> send
> quit
# dig +short ANY test.example.jp @ダイナミックDNSサーバーのIPアドレス
}}}

== Q.allow-update の指定が意味不明です。まずは解説を! ==
<<Include(bind9/Memo_Of_Answer_ACL)>>

== Q.1IP=1TSIGキー設定したい場合はどうすればいいのでしょうか? ==
A.君のような勘のいいガ(ry。

裸の key "TSIGキー名." では一致しなかったときに拒絶される(accept or reject)ことから、ネストで保護してやれば(accept or no match)行けます(下記例参照)。

{{{
allow-update {
    { !{ !IPv4アドレス; any; }; key "TSIGキー名1."; };
    { !{ !IPv6アドレス; any; }; key "TSIGキー名2."; };
};
}}}

== Q.あれ?これ acl 定義すれば簡単になるんじゃあ? ==
A.君のような勘のいいガ(ry。

 * 設定が大量(程度による)にある、引き継ぎのためのドキュメンテーション(ラベル付け)等を目的として、設定が必要なら、acl を定義した方がよい。
 * 別に一個だけの時でも設定してよいが、大抵の管理者は手抜きしたがるだろうから、まずはそちらで(設定が一カ所にまとまってるというメリットもある)。
 * というわけで一概に簡単になるわけではないし、手抜きは非合理なわけではない。

{{{
acl "ACL名1" {
    !{ !IPv4アドレス; any; }; key "TSIGキー名1.";
};

acl "ACL名2" {
    !{ !IPv6アドレス; any; }; key "TSIGキー名2.";
};

zone "example.jp" {
    type master;
    file "/usr/local/etc/namedb/dynamic/example.jp";
    allow-update { "ACL名1"; "ACL名2"; };
};
}}}

== Q.ゾーンに対して広範に任意のレコードの変更ができてしまうようですが、特定のレコードだけに制限する方法はありますか? ==
A.ない。正確には allow-update では解決しえない。
update-policy で設定するが、これにはトレードオフがあり、結果2つのソリューションがある(後述)。

== Q.もしかしてダイナミックDNSって設定は簡単なのですか? ==
A.Yes。簡単だ。allow-update か update-policy の2つのうちのどれかがゾーン設定にあればよい。厳密には、以下の条件となる。

 * ゾーンのオーナーであること。
   * SOAレコードを有する。
   * マスターサーバーである。
   * ゾーンファイルへの書き出しが可能であること(オーナー・パーミッションに留意)。
 * ゾーン内で allow-update または update-policy が設定されていること。

ACL設定もTSIG設定も allow-update のための設定でしかない。究極 any と設定することで動く(実際に運用しちゃダメ)。

= 更新したいレコードの制限 =
 * update-policy 設定により更新したいレコードの制限は行える。
 * TSIGキーの指定による制限は可能であるが、この設定はIPアドレス制限ができない(allow-update 設定は無視される)。
 * よって、2つの選択肢がある。
   * update-policy 設定を受け入れて、任意のIPアドレスからの更新を許す(もちろんTSIGキーの制限はある)。
   * 変更したいレコードを独立したゾーンに切り分けて allow-update で制限する。そのゾーン内において自由な変更を許す。

== update-policy による更新レコードの制限 ==
allow-update の代わりに下記のように記述する。

{{{
    update-policy { grant "TSIGキー名." NAME test.example.jp. TXT; };
}}}

上記の例では「test.example.jp.」に完全一致かつTXTレコードの場合のみアクセス(追加・変更・削除)が許可される。

複数指定したい場合は下記の通りとなる。

{{{
    update-policy {
        grant "TSIGキー名1." NAME test TXT;
        grant "TSIGキー名2." NAME test TXT;
    };
}}}

=== update-policy の指定方法 ===
先に見て通り「;」で区切って、複数の設定を行うことができる。
また複数指定した場合、上から順に評価される(条件が確定するとそこで評価を終了する)。
その指定方法については以下のパターンとなっている(update-policy local; という特殊事例は除く)。

{{{
update-policy {
    モード "アイデンティティ" ルールタイプ レコード名 タイプ;
};
}}}

 モード::
   grant と deny の2つある。通常は grant を使用することになる。
 アイデンティティ::
   TSIGキー名を指定する(厳密には SIG(0) が絡むが・・・)。
 ルールタイプ::
   NAME, SUBDOMAIN, ZONESTAB, WILDCARD, SELF, SELFWILD, MS-SELF, MS-SUBDOMAIN, KRB5-SELF, KRB5-SUBDOMAIN, TCP-SELF, 6TO4-SELF, EXTERNAL の13種類存在する(大文字小文字は区別しない)。
 レコード名::
   限定したいレコード名を指定する。
 タイプ::
   限定したいリソースタイプを指定する。これは0個以上(空白区切り)の指定が可能である(0個はほぼ全てのリソースタイプを許す※後述)。

ルールタイプとレコード名、タイプには相関(指定可能なオプション)に相違があるので、代表的なものをピックアップする。

==== ルールタイプ:NAME ====
{{{
update-policy {
    モード "アイデンティティ" NAME FQDN. [[タイプ] ...];
};
}}}

 * 指定した「FQDN.」と完全一致に対する許可/拒絶します。
 * 例えば「grant "アイデンティティ" name test.example.jp.」とした時、「test.example.jp」レコードの更新を許します。
 * 厳密にマッチするので、test.example.jp のサブドメインすら一致しません。
 * 当然、ゾーンに内包していないときは永久に一致しないことになります(文法エラーになりません)。

==== ルールタイプ:SUBDOMAIN ====
{{{#!wiki caution
色々試してよくわからない振る舞いしたので使わないでください。
たぶん自分の解釈に問題があります!
}}}
{{{
update-policy {
    モード "アイデンティティ" subdomain ワード [[タイプ] ...];
};
}}}

 * 一応、当該ゾーンのサブドメイン「ワード」と「ゾーン名」に対する更新を許可/拒絶することになってます。
 * 例えば「grant "アイデンティティ" subdomain test」とした時、「test.example.jp」と「example.jp」レコードの更新を許するはずです。
 * でもそんなの関係なく「foo.example.jp.」が登録できちゃいました。何か解釈間違ってるのかな?

== ゾーン切り分けによる更新レコードの制限 ==
 * 制限したいレコードを別レコードとして分ける。
 * ここでは example.jp ゾーンから test.example.jp レコードを独立させる。
 * いわゆる「共用DNSにおける『親子同居問題』」が存在するので、[[https://jprs.jp/tech/material/iw2012-lunch-L3-01.pdf|参考文献]]を確認しておくこと。

※ゾーンを分ける以外は、今までの話と同じであることから、上記の流れを参照すること。
ここでは再度説明しない。

=== /usr/local/etc/namedb/named.conf ===
{{{#!diff
--- /usr/local/etc/namedb/named.conf.orig       2018-09-13 22:46:26.048986000 +0900
+++ /usr/local/etc/namedb/named.conf    2018-10-08 00:53:08.437564000 +0900
@@ -383,3 +383,16 @@
        };
 };
 */
+
+include "/usr/local/etc/namedb/TSIGキー名.key";
+
+zone "example.jp" {
+    type master;
+    file "/usr/local/etc/namedb/master/example.jp";
+};
+
+zone "test.example.jp" {
+    type master;
+    file "/usr/local/etc/namedb/dynamic/test.example.jp";
+    allow-update { !{ !IPv4アドレス; !IPv6アドレス; any; }; key "TSIGキー名."; };
+};
}}}

 * example.jp ゾーンは「master」ディレクトリに設置する。
 * test.example.jp ゾーンを定義する。

=== /usr/local/etc/namedb/master/example.jp ===
{{{
$TTL    300

@       IN SOA ns.example.jp. domain.example.jp. (
                20181001   ; serial
                7200       ; refresh (2 hours)
                900        ; retry (15 minutes)
                2419200    ; expire (4 weeks)
                86400      ; minimum (1 day)
                )
        IN NS   ns
ns      IN A    IPv4アドレス
        IN AAAA IPv6アドレス
test    IN NS   ns
}}}

=== /usr/local/etc/namedb/dynamic/test.example.jp ===
{{{
$TTL    300

@       IN SOA ns.example.jp. domain.example.jp. (
                1          ; serial
                7200       ; refresh (2 hours)
                900        ; retry (15 minutes)
                2419200    ; expire (4 weeks)
                86400      ; minimum (1 day)
                )
        IN NS   ns.example.jp.
}}}

※本ファイル作成後のオーナー・パーミッションに注意すること。

{{{
chown bind:wheel /usr/local/etc/namedb/dynamic/test.example.jp
chmod 0644       /usr/local/etc/namedb/dynamic/test.example.jp

}}}

= 【付録】動的更新の安定性について =
ダイナミックDNSはDNSプロトコルに乗って更新を行うため、いくつかの不安定さがある。

 * デフォルトUDPなので、UDPパケットの「ロス」と「成功」の区別がつかない。
   * 一応構成されるパケットの大きさに合わせてUDPとTCPの選択は行われるが・・・。
   * nsupdate コマンドで「-v」オプションを付けることで、TCP接続を強制することができる。
   * これにより、少なくともダイナミックDNSサーバーへの接続性の担保は取れる(サーバーの生死程度は)。
 * 更新の確実性。
   * 「成功」の応答が無いので、確実に更新されたわからない。
   * またマスターの更新は問題無いにせよ、スレーブサーバーへの伝播まで確実にわからない。
 * 実装毎の揺らぎ。
   * BIND9のバージョンにも寄るが、既に登録済みの、同一レコード・同一タイプの同一内容の時の振る舞い(エラーか無視か)など。

よって更新(追加・変更・削除)において一定のパターンがある。それを以下に示す。

== 一レコード追加の担保方法(上書き) ==
{{{
# /usr/local/bin/nsupdate -v -k tsig.key
> local allow-updateで許可したダイナミックDNSクライアントのIPアドレス
> server ダイナミックDNSサーバーのIPアドレス
> prereq yxrrset test.example.jp. TXT
> update delete test.example.jp. TXT
> send
> prereq nxrrset test.example.jp. TXT
> update add test.example.jp. 5 TXT "THIS IS A TEST MESSAGE."
> send
> quit
}}}

 * まず prereq yxrrset で「レコードが存在する」場合に「update delete」を実行するようにする。
 * この結果、当該レコード(text.example.jp. の TXT レコード)はゼロであることが担保される。
 * 次に prereq nxdomain で「レコードが存在しない」場合に「update add」を実行する。
 * 逆に1個でも存在すれば更新されないことを意味する。
 * 上記更新(update add/delete)は「アトミック」に実施されないため、prereq [yn]xrrset で保護するものである。

== レコードの上書きをしないようにする方法 ==
{{{
# /usr/local/bin/nsupdate -v -k tsig.key
> local allow-updateで許可したダイナミックDNSクライアントのIPアドレス
> server ダイナミックDNSサーバーのIPアドレス
> prereq nxrrset test.example.jp. TXT
> update add test.example.jp. 5 TXT "THIS IS A TEST MESSAGE."
> send
> quit
}}}

 * 運用上の要請から、既存レコードを上書きしたく無いケースで実施する場合のパターン。
 * prereq nxdomain で「レコードが存在しない」場合に「update add」を実行するというものである。

== 安全な削除方法 ==
{{{
# /usr/local/bin/nsupdate -v -k tsig.key
> local allow-updateで許可したダイナミックDNSクライアントのIPアドレス
> server ダイナミックDNSサーバーのIPアドレス
> prereq nxrrset test.example.jp. TXT
> update delete test.example.jp. TXT
> send
> quit
}}}

 * 存在しないレコードを削除しようとするとエラーになる実装に対して実施する。
 * prereq nxdomain で「レコードが存在しない」場合に「update delete」を実行するというものである。

= 参考文献 =
 * [[https://jprs.jp/tech/notice/2013-04-18-fixing-bind-openresolver.html|設定ガイド:オープンリゾルバー機能を停止するには【BIND編】]]
 * [[https://kb.isc.org/docs/aa-00723|Using Access Control Lists (ACLs) with both addresses and keys]]
 * [[https://ftp.isc.org/isc/bind9/cur/9.13/doc/arm/Bv9ARM.html|BIND9(9.13) Administrator Reference Manual]]
 * [[https://ftp.isc.org/isc/bind9/cur/9.13/doc/arm/Bv9ARM.ch05.html#dynamic_update_policies|Dynamic Update Policies]]
 * [[https://ftp.isc.org/isc/bind9/cur/9.12/doc/arm/man.nsupdate.html|nsupdate(1)]]
 * [[https://dnsops.jp/event/20180627/DNSSummerDay2018_BIND9.11変更点auth.pdf|BIND9.9から9.11へ移行のポイント(権威DNSサーバー編)]]
 * [[https://jprs.jp/tech/material/rfc/RFC2845-ja.txt|RFC2845:DNSにおける秘密鍵のトランザクション認証(TSIG)]]
 * [[https://jprs.jp/tech/material/iw2012-lunch-L3-01.pdf|親の心子知らず?委任にまつわる諸問題について考える - JPRS]]
 * [[http://www.atmarkit.co.jp/ait/articles/0307/08/news001_2.html|Dynamic DNSの基礎とnsupdateコマンド (2/3)]]

= 注釈 =