AlmaLinux 9 一覧へ戻る トップAlmaLinux 9 / Phase 2
AlmaLinux 9 ・ Phase 2

内部アイデンティティ(DNS / NTP)の確立

SERVERセグメント専用の「時刻」と「名前解決」の基盤を作る

対象コンテナ: 192.168.2.11 Unbound + Chrony

目次

  1. このガイドのゴール
  2. 設計方針を理解する
  3. 構築手順
  4. トラブルシューティング
  5. フェーズ2完了時の構成図
  6. 完了チェックリスト

このガイドのゴール

コンテナ 192.168.2.11 上に、SERVERセグメント内専用の DNS(Unbound)と NTP(Chrony)を構築します。これは今後のフェーズ全部が依存する「土台」です。HeadscaleのTLS証明書検証や、各サーバーのログの時刻整合性は、ここが正確であることが前提になります。

0. まず設計方針を理解する(3分で読めます)

0-1. なぜ「Unbound」なのか

DNSサーバーには大きく2種類の役割があります。

今回2.11が担うのは後者です。Unboundは軽量・高速で、LAN内専用のキャッシュDNSとして広く使われています。

0-2. なぜDMZ(172.16.1.x)はこのサーバーに頼らないのか

これが一番大事な設計判断です。これまでの検討で、こういう結論になりました。

もしDMZが2.11に依存すると…
DMZ(172.16.1.x) →(DNS問い合わせ)→ SERVER内の2.11

↑ FortiGateに「DMZ→SERVERを許可する例外」が必要になる
→ これまで積み上げてきた「DMZ→SERVERは原則拒否」が崩れる

今回の設計:DMZは外部に直接問い合わせる
DMZ(172.16.1.x) →(DNS問い合わせ)→ 公開DNS / NICT
SERVER(192.168.2.x) →(DNS問い合わせ)→ 2.11(内専用)

→ FortiGateの例外ルールが不要。DMZとSERVERの境界がそのまま守られる

なので、2.11の設定では「SERVER(192.168.2.0/24)以外からの問い合わせは拒否する」という制限を必ず入れます。

0-3. なぜpodman runにそのままapt-get installを書かないのか

コンテナ起動時のコマンドの中でapt-get installを実行する書き方には、2つの問題があります。

  1. コンテナが再起動するたび(サーバー再起動・障害復旧時など)に、毎回インターネットから再インストールが走る。つまり「LAN内の基盤であるDNSサーバーの復旧が、外部のインターネット接続に依存してしまう」という本末転倒な状態になります。
  2. 再現性がない。同じ手順で作っても、実行するたびにバージョンが微妙に変わる可能性があります。

そこで今回は、「Dockerfile」という設計図を1回だけ作り、そこからイメージ(完成品)をビルドしておく方式にします。一度ビルドすれば、あとは何度再起動してもインターネット接続なしで即座に起動します。

0-4. なぜ「起動スクリプト(entrypoint.sh)」が必要なのか

1つのコンテナの中で、DNS(unbound)とNTP(chrony)という2つの独立したプログラムを同時に動かす必要があります。コンテナは通常「1つの代表プロセス」が動いている間だけ生き続ける仕組みなので、2つを正しく管理する小さな「指揮役」のスクリプトを用意します。これが entrypoint.sh です。

0-5. なぜ「ディレクトリ」ではなく「ファイル」単位でマウントするのか

これは実装時の重要な注意点です。仮に -v ホスト側ディレクトリ:/etc/chrony:Z のようにディレクトリ単位でマウントすると、何が起こるか見てみましょう。

ディレクトリごと上書き(マウント)すると…
マウント前の /etc/chrony/
├── chrony.conf(既定値)
└── chrony.keys(NTP認証鍵など)


コンテナ内の /etc/chrony/ は、ホスト側フォルダの中身(chrony.confのみ)で"完全に置き換え"られ、chrony.keysなど他のファイルが消えて見えなくなります(「ディレクトリ・マスキング」)

ファイル単位マウント(採用)
-v .../unbound.conf:/etc/unbound/unbound.conf:Z
-v .../chrony.conf:/etc/chrony/chrony.conf:Z

変更したいファイルだけをピンポイントでマウントし、パッケージが用意した他のファイルには触れないようにします。

今回の設定では実害が出にくいケースですが、将来的に機能を追加したときに気づきにくい不具合の原因になります。そのため、変更したいファイルだけをピンポイントでマウントする「ファイル単位マウント」を採用します。

構築手順

1

ディレクトリの準備

設定ファイルとDockerfileを置く場所を作ります。

sudo mkdir -p /srv/containers/identity/build
sudo mkdir -p /srv/containers/identity/unbound
sudo mkdir -p /srv/containers/identity/chrony
ディレクトリ用途
build/Dockerfileと起動スクリプトの置き場所(イメージのビルド専用)
unbound/Unboundの設定ファイル(コンテナを作り直しても消えない)
chrony/Chronyの設定ファイル(同上)
2

Unboundの設定ファイルを作成

sudo vi /srv/containers/identity/unbound/unbound.conf

以下の内容を貼り付けます。

server:
interface: 0.0.0.0
port: 53
do-ip4: yes
do-udp: yes
do-tcp: yes

# セキュリティ設定:SERVER内とループバック以外からの問い合わせは拒否
# ★172.16.1.0/24(DMZ)はここに書かない、というのが今回の設計判断です
access-control: 127.0.0.0/8 allow
access-control: 192.168.2.0/24 allow

# 内部ドメインの定義
local-zone: "home.local." static
local-data: "mgmt-server.home.local. IN A 192.168.2.3"

メモ:今後、他のコンテナ(Headscaleやmonitoringなど)が立ち上がるたびに、local-data: の行を1行ずつ追加していきます。書式は local-data: "ホスト名.home.local. IN A IPアドレス" です。

参考:全コンテナ追加後のlocal-data(完成形)

Phase 3〜5まで進めて全コンテナが揃うと、最終的に以下の9行になります。

local-data: "mgmt-server.home.local.     IN A 192.168.2.3"
local-data: "macv0.home.local.          IN A 192.168.2.99"
local-data: "identity-dns.home.local.    IN A 192.168.2.11"
local-data: "monitoring.home.local.      IN A 192.168.2.12"
local-data: "headscale.home.local.       IN A 192.168.2.13"
local-data: "syslog.home.local.          IN A 192.168.2.14"
local-data: "loki.home.local.            IN A 192.168.2.15"
local-data: "idp.home.local.             IN A 192.168.2.16"
local-data: "openziti.home.local.        IN A 192.168.2.17"
3

Chronyの設定ファイルを作成

sudo vi /srv/containers/identity/chrony/chrony.conf

以下の内容を貼り付けます。

# 同期先の日本標準時サーバー(NICT)
pool ntp.nict.jp iburst

# クライアントへの時刻提供を許可する範囲(SERVER内のみ)
# ★ここにも172.16.1.0/24は書きません
allow 192.168.2.0/24

# 時刻が大きくズレていた場合、起動直後3回までは一気に補正する
makestep 1.0 3
4

Dockerfile(設計図)の作成

sudo vi /srv/containers/identity/build/Dockerfile
FROM docker.io/ubuntu:24.04
RUN apt-get update && \
    apt-get install -y --no-install-recommends unbound chrony && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
COPY entrypoint.sh /entrypoint.sh
# COPY直後に、コンテナ内部でも確実に実行権限を付与する
# (ホスト側でchmod +x済みでも、ビルド環境によっては権限が引き継がれないことがあるため、
#   念のためここでも明示しておくと、どの環境でビルドしても確実に動作する)
RUN chmod +x /entrypoint.sh
EXPOSE 53/udp 53/tcp 123/udp
ENTRYPOINT ["/entrypoint.sh"]

ポイント:RUN apt-get install は「イメージのビルド時」に1回だけ実行されます。podman run のたびに実行される旧方式とは違い、ここで完成形がイメージとして固定されます。

5

起動スクリプト(entrypoint.sh)の作成

sudo vi /srv/containers/identity/build/entrypoint.sh
#!/bin/bash
set -e
echo "[entrypoint] Unbound(DNS)を起動します..."
unbound -d -c /etc/unbound/unbound.conf &
UNBOUND_PID=$!

echo "[entrypoint] Chrony(NTP)を起動します..."
# -x: コンテナにはホストの時計を直接操作する権限がないため、その操作を無効化
chronyd -d -x -f /etc/chrony/chrony.conf &
CHRONY_PID=$!

# どちらかが異常終了したら、コンテナ全体も終了させる
# (DNSだけ死んでNTPだけ動き続ける「半死に状態」を防ぐため)
wait -n "$UNBOUND_PID" "$CHRONY_PID"
EXIT_CODE=$?
echo "[entrypoint] プロセスが終了しました(exit code: $EXIT_CODE)。コンテナを終了します。"
exit "$EXIT_CODE"

実行権限を付けます(こちらはホスト側での操作。前述の通り、Dockerfile側でも二重に保証しています)。

sudo chmod +x /srv/containers/identity/build/entrypoint.sh
6

イメージのビルド

cd /srv/containers/identity/build
sudo podman build -t identity-dns-ntp:latest .
出力例(最後の数行)
COMMIT identity-dns-ntp:latest
--> xxxxxxxxxxx
Successfully tagged localhost/identity-dns-ntp:latest

Successfully tagged が出ればビルド成功です。

7

【推奨】本番起動の前に設定ファイルの文法チェック

いきなり起動して失敗の原因を調べるより、先に文法だけチェックすると効率的です。

sudo podman run --rm \
  -v /srv/containers/identity/unbound/unbound.conf:/etc/unbound/unbound.conf:Z \
  identity-dns-ntp:latest unbound-checkconf /etc/unbound/unbound.conf
期待される出力
unbound-checkconf: no errors in /etc/unbound/unbound.conf

エラーが出た場合は、ステップ2のファイルにタイプミス(インデントのズレなど、YAMLライクな書式は特にスペースの数に敏感です)がないか確認してください。

8

コンテナの起動

ステップ0-5で説明した通り、ここではディレクトリ単位ではなく、ファイル単位でマウントします。

sudo podman run -d \
  --name identity-dns-ntp \
  --network local_lan \
  --ip 192.168.2.11 \
  -v /srv/containers/identity/unbound/unbound.conf:/etc/unbound/unbound.conf:Z \
  -v /srv/containers/identity/chrony/chrony.conf:/etc/chrony/chrony.conf:Z \
  --restart always \
  identity-dns-ntp:latest
実際の実行結果(検証ログより)
sudo podman ps
CONTAINER ID  IMAGE                            STATUS         PORTS                  NAMES
b72c40a8a051  localhost/identity-dns-ntp:latest Up About a minute 53/tcp, 53/udp, 123/udp identity-dns-ntp

sudo podman logs identity-dns-ntp
[entrypoint] Unbound(DNS)を起動します...
[entrypoint] Chrony(NTP)を起動します...
chronyd version 4.5 starting ...
Selected source 133.243.238.164 (ntp.nict.jp)

実機検証済み:コンテナが正常に起動し、NICTのNTPサーバーと同期していることを確認しています。

9

動作確認(必ず実施)

9-1. コンテナが起動し続けているか確認
sudo podman ps

identity-dns-ntpUp 状態で表示されればOKです。もし数秒後に消えている場合は、起動直後にクラッシュしています。次のログ確認に進んでください。

sudo podman logs identity-dns-ntp

[entrypoint] Unbound(DNS)を起動します...[entrypoint] Chrony(NTP)を起動します... の両方が表示され、エラーが出ていなければ正常です。

9-2. Chronyが時刻同期できているか確認(コンテナ内部から)
sudo podman exec identity-dns-ntp chronyc tracking
実際の実行結果(検証ログより)
Reference ID    : 3DCD7882 (ntp-k1.nict.jp)
Stratum         : 2
System time     : 0.000677686 seconds fast of NTP time
Leap status     : Normal

Stratum 2(原子時計から2段目・高精度)、誤差1ミリ秒未満、Leap status: Normal。時刻同期は正常に機能しています。

Stratumが16のままだったり、Leap statusがNot synchronisedの場合は、まだ同期できていません。数分待ってから再確認してください(インターネット接続自体に問題がないかも合わせて確認してください)。

9-3. DNSが正しく応答するか確認(ホストOSから)

ホストにdigコマンドがない場合は先にインストールします。

sudo dnf install -y bind-utils

内部ドメインの解決テスト:

dig @192.168.2.11 mgmt-server.home.local
実際の実行結果(検証ログより)
;; ANSWER SECTION:
mgmt-server.home.local. 3600 IN A 192.168.2.3
;; SERVER: 192.168.2.11#53(192.168.2.11)

ANSWER SECTIONに正しくホストのIPが返ってきています。

外部ドメインの再帰解決テスト:

dig @192.168.2.11 google.com

こちらもANSWER SECTIONにIPアドレスが返ってくれば、再帰DNSとして正常に機能しています。

10

【任意・推奨】ホストOSのDNS参照先を2.11に切り替える

今まで作った「SERVER内専用DNSサーバー(192.168.2.11)」を、ホストOS自身も使うように設定します。これにより mgmt-server.home.local のような内部名でサーバー同士が会話できるようになります。

手順1:自分のネットワーク接続名を調べる
nmcli connection show --active

NAME の列の値(例:enp1s0)をメモしてください。次のコマンドで使います。

手順2:DNSの参照先を2.11に変更する

<接続名> を手順1で確認した名前に置き換えて実行してください:

sudo nmcli connection modify <接続名> \
  ipv4.dns "192.168.2.11" \
  ipv4.dns-search "home.local" \
  ipv4.ignore-auto-dns yes
オプション意味
ipv4.dns "192.168.2.11"DNSサーバーとして2.11を使う
ipv4.dns-search "home.local"mgmt-server と打つだけで mgmt-server.home.local として検索する
ipv4.ignore-auto-dns yesルーター(FortiGate)が自動配布するDNS設定を無視する
手順3:設定を反映させる
sudo nmcli connection up <接続名>

注意

一瞬ネットワークが切断されますが、すぐ再接続されます。SSHで作業している場合も通常は自動で戻ります。

手順4:正しく設定されたか確認する

① resolv.confの中身を確認

cat /etc/resolv.conf
実際の実行結果(検証ログより)
# Generated by NetworkManager
search home.local
nameserver 192.168.2.11
nameserver 1.1.1.1

② 実際にDNS解決できるか確認

nslookup mgmt-server.home.local
実際の実行結果(検証ログより)
Server:   192.168.2.11
Address:  192.168.2.11#53
Name:    mgmt-server.home.local
Address: 192.168.2.3

実機検証済み:ホストOSからの内部名前解決も正常に機能しています。

トラブルシューティング

症状原因の可能性対処法
podman psに表示されない(起動直後に終了)unbound.confかchrony.confの文法エラーpodman logs identity-dns-ntpでどちらのプロセスが落ちたか確認。ステップ7の文法チェックも活用
digがタイムアウトするコンテナがSERVER内から見えていない/access-controlの範囲外Phase 1のルート設定(ip route get 192.168.2.11)を再確認
Permission deniedがボリュームマウントで出るSELinuxのラベル不一致-vオプションの末尾に:Zが付いているか確認(既に付いていますが、設定ファイルを直接編集した後はrestorecon -R -v /srv/containers/identityを試す)
ChronyがいつまでもNot synchronisedコンテナからインターネットに出られないsudo podman exec identity-dns-ntp ping -c 3 ntp.nict.jpで疎通確認。FortiGate側でSERVERセグメントの外向き通信が許可されているか確認
dig @192.168.2.11 google.comは失敗するがmgmt-server.home.localは成功する内部ゾーンの応答とインターネットへの再帰解決は別経路。SERVERからインターネットへの出口がブロックされている可能性ホストOS(2.3)からping 8.8.8.8などで、コンテナ自体ではなくSERVER全体の外向き疎通を確認
ステップ10後、ホストの名前解決が全体的に不安定になったDNS参照先を2.11単独にしてしまい、2.11再起動中に解決できない瞬間があるフォールバック用DNSを2番目に追加(手順2の ipv4.dns に複数指定可能)

フェーズ2完了時の構成図

ここまでで、以下のような状態が出来上がっています。「DMZはこの基盤に依存しない」という設計思想が反映されています。

DMZ(172.16.1.x)
公開DNS / 公開NTP(NICT, 8.8.8.8等)を直接参照

SERVERの2.11は使わず、こちらを直接参照する

SERVER(192.168.2.0/24)
AlmaLinux Host(2.3)
├ macv0: 2.99
└ Podman Macvlan → Identity Server 2.11
   (Unbound DNS Cache / Chrony NTP)

↑ SERVER内のPC・サーバーのみ利用可(DMZからの問い合わせは拒否される)

フェーズ2完了チェックリスト

すべてチェックできれば、フェーズ2は完了です。

AlmaLinux 9 一覧へ戻る