Rocky Linux 9 一覧へ戻る トップ / Rocky Linux 9 / Caddyリバースプロキシ
Rocky Linux 9 ・ Phase 4 ・ DMZ境界セグメント

Caddy リバースプロキシの構築

HTTPS終端/証明書自動管理/DMZ境界セグメント ― Rocky Linux 9(172.16.1.18)

対象: Rocky Linux 9(DMZ・172.16.1.0/24) 所要時間目安: 30〜45分 前提: Phase 3(Headscale)稼働中

本ページの位置づけ

Caddyは自宅ラボの「ゼロトラスト統合基盤」において、DMZ側でHTTPSを一元的に受け止める要のコンポーネントです。詳細な設計思想はAlmaLinux側の「ゼロトラスト基盤構築ガイド(v5決定版)」のPhase 4にも記載していますが、本ページはRocky Linux 9(Caddyが実際に稼働するホスト)の視点から、より詳しい手順として独立してまとめたものです。

目次

  1. 0. Phase 4の目的と全体像
  2. 1. 事前準備と確認事項
  3. 2. 作業開始:Caddyの構築手順
  4. 3. Phase 3の修正がPhase 4に与える影響
  5. 4. Phase 4完了チェックリスト

0. Phase 4 の目的と全体像を理解する

作業を始める前に、Phase 4で何を構築するのか・なぜそれが必要なのかを理解してください。手順の意味が分かると、トラブル発生時の対処が格段に楽になります。

0-1. Caddy とは何か

プロ技・解説:Caddyの役割を一言で言うと

Caddy(キャディ)は「インターネットからのHTTPS通信を受け取り、適切な内部サーバーへ振り分けるプログラム」です。具体的には以下の3つの仕事をします。

  • ① HTTPS通信の暗号を解除する(TLS終端):ブラウザとの暗号化通信(https://)をCaddyが担当。内部サーバー(Headscale/Authentik)は暗号化不要になります。
  • ② ホスト名によって振り分ける(リバースプロキシ):hs.yourdomain.jp → Headscale(192.168.2.13)/auth.yourdomain.jp → Authentik(192.168.2.16)/www.yourdomain.jp → 既存Webサーバー(172.16.1.2)
  • ③ SSL証明書を自動取得・自動更新する:Let's Encryptから証明書を自動取得し、90日ごとの更新も自動で行う(手動作業は一切不要)。

0-2. なぜCaddyをDMZ(172.16.1.18)に置くのか

設計ポイント:DMZ配置の重要性

DMZとは何か:DMZ(Demilitarized Zone:非武装地帯)とは、インターネットとSERVERセグメントの間に設ける中間ネットワークです。外部からアクセスされる可能性があるサーバーをここに置き、内部を守ります。

なぜ内部に置いてはいけないのか:もしCaddyをSERVERセグメント(192.168.2.x)に置くと、インターネット→FortiGate→SERVER(直接)となり、インターネットの生トラフィックが直接内部に入ってきます。これはPhase 1で構築したセグメント分離を無意味にします。

DMZに置くと何が良いのか:インターネット→FortiGate→DMZ:Caddy→(FW制御下)→SERVERとなり、SERVERにはCaddyからの必要最小限の通信しか届きません。万が一Caddyが攻撃されても、内部は守られます。

0-3. Phase 4 が解決する4つの問題

#解決する問題解決方法
ポート競合:Rocky Linuxが:80/:443を占有しているためHeadscaleが公開できないCaddyが:80/:443を受け取りホスト名(SNI)で振り分ける → Rocky Linuxと共存可能
証明書管理の手間:certbot + cronでの手動管理が必要CaddyがLet's Encryptから自動取得・自動更新 → 一切手動作業不要
将来の拡張性:新サービスを追加するたびに複雑な設定が必要Caddyfileに1ブロック追記するだけ → Grafana/OpenZitiも簡単に追加
セキュリティ設計:インターネットが直接内部に届くCaddyをDMZに配置 → 内部には制御された通信のみ

0-4. Phase 4 完成後の全体通信フロー

インターネット
↓ TCP 80 / 443
FortiGate(VIPで転送)
↓ TCP 80 / 443 → 172.16.1.18
Caddy 172.16.1.18(DMZ) ← ここでHTTPSを終端
↓ ホスト名で振り分け(HTTPに変換)
├─ hs.yourdomain.jp  → HTTP:8080 → Headscale 192.168.2.13(SERVER)
├─ auth.yourdomain.jp → HTTP:9000 → Authentik 192.168.2.16(SERVER)
└─ www.yourdomain.jp → HTTP:80  → Rocky Linux 172.16.1.2(DMZ)

※ Caddy→SERVERへの通信はFortiGateのポリシーで制御されています(送信元: 172.16.1.18のみ許可/ポート: 8080と9000のみ)

1. 事前準備と確認事項

作業を始める前に、以下を確認してください。Phase 3の修正点も含まれています。

1-1. Rocky Linux 9 に SSH でログインする

Phase 4の作業はすべてRocky Linux 9(DMZ: 172.16.1.2)で行います。AlmaLinux(SERVER)ではありません。作業するホストを間違えないよう注意してください。

# Windowsのコマンドプロンプトまたは PowerShell から
ssh ユーザー名@172.16.1.2

# ログイン後、ホスト名を確認する
hostname
# Rocky Linux 9 のホスト名が表示されればOK

1-2. Phase 3(Headscale)が正常稼働しているか確認する

CaddyはHeadscaleに通信を転送します。Headscaleが動いていないとCaddyを起動しても意味がありません。AlmaLinuxから確認します。

ポイント:確認場所に注意

この確認コマンドはAlmaLinux 9(SERVER)で実行します。Rocky Linux(DMZ)ではありません。別のターミナルウィンドウを開いてAlmaLinuxにSSHログインしてから実行してください。

# AlmaLinux 9(SERVER)で実行する
sudo podman ps

# 以下の3つが Up と表示されていれば Phase 3 は正常稼働中
# NAMES                STATUS
# headscale-pod-infra  Up XX minutes
# headscale-db         Up XX minutes
# headscale            Up XX minutes (healthy)  ← (healthy) が重要

注意:headscale が (healthy) になっていない場合

Phase 3のトラブルシューティング記録を参照して、問題③(noise.private_key_path)と問題④(private_key_path)の修正が適用されているか確認してください。

確認コマンド(AlmaLinuxで実行):sudo grep private_key_path /srv/containers/headscale/config/config.yaml
以下の2行が表示されれば修正済み:private_key_path: /var/lib/headscale/private.keynoise: private_key_path: /var/lib/headscale/noise_private.key

1-3. DMZ ネットワークのブリッジ名を確認する(Phase 3 修正点)

Phase 3のトラブルシューティングで判明したように、ネットワークインターフェース名は環境によって異なります。Phase 4でも同様に、作業前に必ず確認が必要です。

プロ技・解説:なぜブリッジ名を確認するのか

DMZのネットワーク定義(local_dmz.network)では、Caddyコンテナをどの物理ネットワークに接続するかを指定します。Phase 3では「SERVER用のNIC名(eth0)」が環境と一致しなかったため、ネットワークが起動できませんでした。Phase 4でも同様に、DMZ用のブリッジ名を事前に確認してから設定ファイルに記述する必要があります。

# Rocky Linux 9(DMZ)で実行する

# ── ブリッジの一覧を表示 ─────────────────────────
ip link show type bridge

# 出力例(環境により異なる):
# 5: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...  ← br0 を使う
# または
# 5: br-dmz: <BROADCAST,MULTICAST,UP,LOWER_UP> ...        ← br-dmz を使う

# ブリッジが見つからない場合は以下でも確認できる
ip link show | grep -E "^[0-9]+:"

# 表示された名前をメモしておく(後で使います)

注意:ブリッジが見つからない場合

ip link show type bridge で何も表示されない場合は、DMZ用のブリッジが作成されていない可能性があります。Phase 1(インフラ基盤構築)のDMZネットワーク設定が完了しているか確認してください。

または、以下のコマンドですべてのNICを確認し、DMZに使うNICを特定してください:ip addr show(172.16.1.xのアドレスが付いているNIC名を使います)

1-4. DDNS の確認

CaddyがLet's Encryptから証明書を取得するには、ドメイン名がサーバーの外部IPに正しく紐づいている必要があります。

# インターネットに繋がっている端末(自分のPC等)から実行する

# ドメインのIPを確認する
nslookup hs.yourdomain.mydns.jp
# または
ping hs.yourdomain.mydns.jp

# 返ってきたIPがご自宅のWAN(外部)IPと一致していればOK

2. 作業開始:Caddy の構築手順

1

ディレクトリと Caddyfile の作成(Rocky Linux 9 側)

STEP 1-1 保存用ディレクトリの作成

コンテナが削除・再作成されてもLet's Encryptの証明書やCaddyの設定が消えないよう、ホストOS(Rocky Linux 9)に保存領域を作成します。

ポイント:ディレクトリの役割

/srv/containers/caddy/Caddyfile → プロキシのルール//srv/containers/caddy/data/ → Let's Encrypt証明書のキャッシュ(再起動しても残る)//srv/containers/caddy/config/ → Caddyの内部設定キャッシュ

sudo mkdir -p /srv/containers/caddy/config
sudo mkdir -p /srv/containers/caddy/data

# SELinuxコンテキストを正しく設定する
sudo restorecon -Rv /srv/containers/caddy

# 作成できたか確認する
ls -la /srv/containers/caddy/
STEP 1-2 Caddyfile(プロキシ設定ファイル)の作成

Caddyfileは「どのドメインへのアクセスを、どのサーバーに転送するか」を定義するファイルです。Caddyの設定の心臓部です。

注意:2か所を必ず書き換えてください

yourdomain.mydns.jp → ご自身のDDNSドメイン名/172.16.1.2 → Rocky Linuxの実際のIPアドレス(確認方法: ip addr show | grep 172.16

sudo tee /srv/containers/caddy/Caddyfile << 'EOF'
# ════════════════════════════════════
# Caddyfile - Phase 4 設定
# ════════════════════════════════════

# ── Headscale(Phase 3 / SERVER: 192.168.2.13)──
hs.yourdomain.mydns.jp {
    encode gzip zstd
    reverse_proxy 192.168.2.13:8080 {
        header_up Upgrade    {http.request.header.Upgrade}
        header_up Connection {http.request.header.Connection}
    }
}

# ── 既存Webサーバー(Rocky Linux DMZ: 172.16.1.2)──
www.yourdomain.mydns.jp {
    encode gzip zstd
    reverse_proxy 172.16.1.2:80 {
        header_up X-Real-IP       {remote_host}
        header_up X-Forwarded-For {remote_host}
    }
}

# ── Authentik(Phase 5 / SERVER: 192.168.2.16)──
auth.yourdomain.mydns.jp {
    encode gzip zstd
    reverse_proxy 192.168.2.16:9000
}

# ── 将来追加予定(Phase 6: Grafana)──
# grafana.yourdomain.mydns.jp {
#     encode gzip zstd
#     reverse_proxy 192.168.2.12:3000
# }
EOF

cat /srv/containers/caddy/Caddyfile
STEP 1-3 Caddyfile の構文チェック(事前検証)

ポイント:Phase 3 トラブルシューティングで学んだこと

Phase 3では設定ファイルの不備(private_key_path未設定)が原因で何度も起動・失敗を繰り返しました。Caddyには設定ファイルの文法を事前確認できる機能があります。コンテナを起動する前に確認する習慣をつけることで、同様のトラブルを防ぐことができます。

sudo podman run --rm \
  -v /srv/containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro,Z \
  docker.io/library/caddy:<version> \
  caddy validate --config /etc/caddy/Caddyfile

# 成功時: Valid configuration
# エラー時: Error: parsing caddyfile tokens: ...
2

Quadlet ファイルの作成と起動

Phase 3と同様に、Quadlet(systemdネイティブ)でコンテナを管理します。

STEP 2-1 ネットワーク定義ファイルの作成

注意:ブリッジ名を確認してから実行してください

「1-3 DMZネットワークのブリッジ名を確認する」で確認したブリッジ名を使います。このガイドではbr0を例として使います。ご自身の環境がbr-dmzやvirbr0などの場合は置き換えてください。

sudo tee /etc/containers/systemd/local_dmz.network << 'EOF'
[Network]
# ★ 重要: bridge=br0 の "br0" は実際のブリッジ名に書き換えてください
# 確認方法: ip link show type bridge
Driver=bridge
Options=bridge=br0
Subnet=172.16.1.0/24
Gateway=172.16.1.1
EOF

cat /etc/containers/systemd/local_dmz.network
STEP 2-2 イメージのダイジェスト(ハッシュ値)を取得する

プロ技・解説:ダイジェスト固定とは何か

タグ(caddy:<version>)は人が読みやすいが同じタグで中身が変わる可能性があります。ダイジェスト(caddy@sha256:...)はイメージの内容から計算した固有の値で、同じダイジェストなら必ず同じイメージです。本番・学習環境を問わず、ダイジェスト固定は再現性の高い構成を保つためのベストプラクティスです。

sudo podman pull docker.io/library/caddy:<version>

CAD_DIGEST=$(sudo podman image inspect docker.io/library/caddy:<version> \
  --format '{{index .RepoDigests 0}}')

echo "Caddy digest: ${CAD_DIGEST}"
# 出力例: Caddy digest: docker.io/library/caddy@sha256:a1b2c3d4...

# ⚠️ この変数はシェルを閉じると消えます。Quadletファイル作成まで同じターミナルで続けてください
STEP 2-3 Caddy コンテナ定義ファイルの作成
# CAD_DIGEST 変数が設定されているか確認してから実行
echo ${CAD_DIGEST}

sudo tee /etc/containers/systemd/caddy.container << EOF
[Unit]
Description=Caddy Edge Reverse Proxy (DMZ)
After=network-online.target
[Container]
Image=${CAD_DIGEST}
ContainerName=caddy
Network=local_dmz.network
IP=172.16.1.18
Volume=/srv/containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro,Z
Volume=/srv/containers/caddy/data:/data:Z
Volume=/srv/containers/caddy/config:/config:Z
HealthCmd=caddy version || exit 1
HealthInterval=30s
[Service]
Restart=always
[Install]
WantedBy=multi-user.target
EOF

cat /etc/containers/systemd/caddy.container
STEP 2-4 Quadlet の事前確認(dryrun)
sudo /usr/libexec/podman/quadlet -dryrun 2>&1 | grep -E "caddy|error|Error"
STEP 2-5 systemd への認識と起動

プロ技・解説:Phase 3の修正点:enableではなくstartを使う

Quadletで生成されたユニットは/run/systemd/generator/に置かれる一時ファイルのため、enableコマンドは使えません。startコマンドで起動します。OS再起動後の自動起動はQuadletが自動的に処理するため、enableは不要です。

sudo systemctl daemon-reload

sudo systemctl list-unit-files | grep caddy
# caddy.service generated と表示されれば認識成功

sudo systemctl start caddy

sudo systemctl status caddy --no-pager
# Active: active (running) と表示されれば成功
STEP 2-6 起動ログで証明書取得を確認する
sudo journalctl -u caddy -f --no-pager

# 成功時のログ例:
# {"level":"info","msg":"obtained certificate","domain":"hs.yourdomain.mydns.jp"}
# {"level":"info","msg":"serving initial configuration"}

# エラーのみ抽出して確認
sudo journalctl -u caddy --no-pager | grep -i "error\|failed\|certificate" | tail -20

注意:証明書取得に失敗する主な原因

  • 原因1 FortiGateのVIP設定が未完了 → WAN:80/WAN:443が172.16.1.18に転送されていないと取得できません。STEP 3を先に完了させてから再試行してください。
  • 原因2 DDNSが更新されていない → nslookup hs.yourdomain.mydns.jpで返ってくるIPを確認してください。
  • 原因3 同じドメインで短時間に何度も試行した → Let's Encryptには失敗回数の制限があります(1時間に5回まで)。エラーが続く場合は1時間待ってから再試行してください。
3

FortiGate の設定(VIP / ファイアウォールポリシー)

FortiGateの設定はPhase 4の中で最も重要な作業です。この設定が正しくないとCaddyが証明書を取得できず、外部からのアクセスも届きません。

STEP 3-1 設定の全体像を理解する

設計ポイント:FortiGateに設定する2種類のルール

① VIP(バーチャルIP):「インターネットから来た特定のポートへの通信を、内部のどのIPに転送するか」を定義します。
WAN:443 → 172.16.1.18:443(CaddyへのHTTPS転送)/WAN:80 → 172.16.1.18:80(Let's Encrypt証明書更新用)

② ファイアウォールポリシー:「どのネットワークからどのネットワークへの通信を許可するか」を定義します。VIPを作成しただけでは通信は通りません。A. WAN→DMZ(インターネットからCaddyへの通信を許可)/B. DMZ→SERVER(CaddyからHeadscale/Authentikへの通信を許可)

STEP 3-2 VIP(バーチャルIP)の作成
VIP名(任意)外部IP(WAN)外部ポート転送先IP(DMZ)転送先ポート用途
VIP_ReverseProxy_HTTPSWAN側グローバルIPTCP 443172.16.1.18443Headscale/AuthentikのHTTPS通信
VIP_ReverseProxy_HTTPWAN側グローバルIPTCP 80172.16.1.1880Let's Encrypt証明書取得・更新用

ポイント:FortiGate GUI でのVIP作成手順

  1. Policy & Objects → Virtual IPs → Create New → Virtual IP
  2. Name: VIP_ReverseProxy_HTTPS(任意)/Interface: WAN/External IP Address: WAN側グローバルIP/Map to IPv4 Address: 172.16.1.18/Port Forwarding: 有効/Protocol: TCP/External Service Port: 443/Map to IPv4 Port: 443 を入力してOK
  3. 同様にVIP_ReverseProxy_HTTP(ポート80)も作成
STEP 3-3 WAN → DMZ ファイアウォールポリシーの作成
設定項目設定値補足説明
NamePolicy_WAN_to_Proxy任意の名称
Incoming InterfaceWAN外部インターフェース
Outgoing InterfaceDMZDMZインターフェース
Sourceall送信元は制限しない(インターネット全体)
DestinationVIP_ReverseProxy_HTTPS と VIP_ReverseProxy_HTTP作成したVIPを両方選択
ServiceHTTP, HTTPSまたはCustomでTCP 80と443を指定
ActionACCEPT通信を許可
IPSプロファイルdefault等侵入防御を有効にする(推奨)
ログすべてのセッショントラブル時の調査のため有効化
STEP 3-4 DMZ → SERVER ファイアウォールポリシーの作成(最重要)

セキュリティ:この設定がゼロトラスト境界の核心です

この設定を正しく行わないと、①CaddyからHeadscale/Authentikに通信できずリバースプロキシが動かない、②設定を緩めすぎると他のDMZ機器(Rocky Linux等)からもSERVERに直接アクセスできてしまいセキュリティが崩れる、という2つの問題が発生します。

「送信元をCaddy(172.16.1.18)のみ」「ポートを必要最小限(8080と9000のみ)」この2点を厳守してください。

設定項目設定値補足説明
NamePolicy_Proxy_to_Internal任意の名称
Incoming InterfaceDMZDMZインターフェース
Outgoing InterfaceSERVERSERVERインターフェース
Source172.16.1.18(Caddyのアドレスオブジェクト)Caddyのみ許可。Rocky Linux等は含めない
Destination192.168.2.13(Headscale)/192.168.2.16(Authentik)SERVER内の転送先2台のみ
ServiceカスタムTCP 8080(Headscale用)/9000(Authentik用)他のポートは許可しない
ActionACCEPT通信を許可
IPSプロファイル有効(推奨)内部通信もIPSで監視
ログすべてのセッション必ず有効化(セキュリティ監査のため)

ポイント:FortiGateでアドレスオブジェクトを作成する方法

1. Policy & Objects → Addresses → Create New → Address/Name: Caddy_DMZ/Type: Subnet/IP/Netmask: 172.16.1.18/32(/32で単一IPを指定)
2. 同様にHeadscale_SERVER(192.168.2.13/32)、Authentik_SERVER(192.168.2.16/32)も作成

4

動作テスト

FortiGateの設定が完了したら、外部からのアクセスと証明書取得が正しく機能しているか確認します。

STEP 4-1 Caddy の状態確認
sudo systemctl is-active caddy
sudo systemctl status caddy --no-pager
STEP 4-2 証明書取得の確認
sudo journalctl -u caddy --no-pager | grep -i "certificate"
# 3つのドメイン分(hs / auth / www)が表示されれば証明書取得完了

sudo ls -la /srv/containers/caddy/data/
# certificates/ ディレクトリが作成されていれば成功
STEP 4-3 外部からのHTTPSアクセステスト

モバイル回線から実行してください

自宅のWi-Fiからは内部通信になるため正確なテストになりません。モバイル回線(LTE/5G)に接続したスマートフォンやノートPCから実行してください。

curl -v https://hs.yourdomain.mydns.jp 2>&1 | grep -E "SSL|certificate|Connected|HTTP"

# 成功時の出力例:
# * SSL connection using TLSv1.3 ...
# * Server certificate: subject: CN=hs.yourdomain.mydns.jp
# * issuer: C=US; O=Let's Encrypt; CN=R11
# < HTTP/2 200

curl -v https://www.yourdomain.mydns.jp 2>&1 | grep -E "SSL|certificate|HTTP"

確認ポイント:SSL証明書エラーが出ない=成功

①curlの出力にSSL connection using TLSv1.3が表示される/②issuerが"Let's Encrypt"になっている/③SSL証明書エラー(ERR_CERT_NOT_TRUSTED等)が出ない。ブラウザでアクセスした場合はアドレスバーに鍵アイコンが表示されれば成功です。

STEP 4-4 Caddy から SERVER への疎通確認
curl -s http://192.168.2.13:8080/ 2>&1 | head -3
# 何らかの応答があれば疎通成功(FortiGateのDMZ→SERVERポリシーが正しく設定されている)

3. Phase 3 の修正が Phase 4 に与える影響(重要)

Phase 3のトラブルシューティングで発見した問題のうち、Phase 4およびPhase 5(Authentik)の設計に影響するものをまとめます。

3-1. OIDC の issuer URL の変更

プロ技・解説:旧設計と新設計の違い

旧設計(v3マニュアルまで):HeadscaleからAuthentikへのOIDC通信に内部IPを直接使用(issuer: "https://192.168.2.16:9443/application/o/headscale-app/")。問題点:自己署名証明書のためinsecure_skip_verify: trueが必要だった。

新設計(v5/Phase 4導入後):CaddyがLet's Encryptの正規証明書でHTTPSを終端するため、issuer: "https://auth.yourdomain.mydns.jp/application/o/headscale-app/"。メリット:正規の証明書を使うためinsecure_skip_verifyは不要。セキュリティが向上する。

3-2. Phase 5(Authentik)の config.yaml への影響

注意:Phase 5 作業時の注意

Phase 5でHeadscaleにOIDC設定を追加する際、以下の新設計に従ってください。

oidc:
  # 正しい:Caddy経由の正規HTTPS URLを使う
  issuer: "https://auth.yourdomain.mydns.jp/application/o/headscale-app/"
  client_id: "(Authentikで発行されたClient ID)"
  client_secret: "(Authentikで発行されたClient Secret)"
  scope: ["openid", "profile", "email"]
  strip_email_domain: true
  # insecure_skip_verify は不要(削除する)

# 古い設計(使わない):
#   issuer: "https://192.168.2.16:9443/..."  ← 内部IP直接指定は使わない
#   insecure_skip_verify: true               ← 不要になったため削除

4. Phase 4 完了チェックリスト

以下をすべて確認できればPhase 4は正常に完了しています。1つでも確認できない場合は、対応するステップに戻って確認してください。

◆ Caddyコンテナの動作確認

◆ 外部アクセスの確認(モバイル回線から実行)

◆ FortiGateポリシーの確認

◆ Phase 3 修正点の反映確認

Phase 4 完了

CaddyがDMZでHTTPSを一元管理し、Let's Encrypt証明書が自動取得されました。既存のRocky Linux(Web/Mail)と共存しながら、Headscaleが標準ポート443で公開されています。次はPhase 5(Authentikによる MFA追加)に進みます。

Rocky Linux 9 一覧へ戻る