Files
maestro/docs/operations/caddy-cloudflare-tls.html
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

525 lines
37 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Caddy + Cloudflare でワイルドカード証明書 — MAESTRO</title>
<link rel="stylesheet" href="guide.css">
</head>
<body>
<div class="wrap">
<header class="doc">
<div class="crumbs"><a href="index.html">運用ガイド</a> / Caddy + Cloudflare でワイルドカード証明書</div>
<h1>Caddy を前に置いて HTTPS 化する <span class="badge done">完成</span></h1>
<p class="meta">対象: 手元やイントラの環境で MAESTRO を正式な証明書つきの HTTPS で使いたい人 / 更新日: 2026-08-10 / 由来: issue #1094</p>
</header>
<h2>このガイドで作るもの</h2>
<p>
Caddy をリバースプロキシとして MAESTRO の前に置き、Cloudflare の DNS を使って
Let's Encrypt からワイルドカード証明書を取ります。証明書の取得も 60 日ごとの更新も Caddy が自分でやるので、
運用中に手を入れる作業は残りません。
</p>
<table>
<thead><tr><th>項目</th><th>内容</th></tr></thead>
<tbody>
<tr><td>証明書の取り方</td><td>Let's Encrypt の DNS-01 チャレンジ。Cloudflare の API で一時的な TXT レコードを置いて検証する</td></tr>
<tr><td>ポート 80 の要否</td><td>発行には不要。80 番が塞がれている回線でも取れる(ここが HTTP-01 との違い)</td></tr>
<tr><td>ワイルドカード</td><td>取れる。DNS-01 でしか取れない</td></tr>
<tr><td>更新</td><td>Caddy が自動で行い、自分で差し替える。再起動は不要</td></tr>
<tr><td>MAESTRO 側の証明書</td><td>持たせない。TLS は Caddy で終端する</td></tr>
</tbody>
</table>
<div class="note">
<b>MAESTRO 自身に証明書を持たせる道もあります</b><br>
<code>server.tls.cert_file</code> / <code>key_file</code> に運用者の証明書を指すやり方です。ただし MAESTRO は<b>起動時に 1 回しか読みません</b>。
自動更新した証明書は再起動するまで反映されず、期限が来るとそこで繋がらなくなります。
プロキシを前に置けば MAESTRO は証明書を持たなくなるので、この問題ごと消えます。
</div>
<h2>前提</h2>
<ul>
<li>Docker Compose で MAESTRO を動かしていること。Compose は <b>v2.24 以上</b>(<code>docker compose version</code> で確認)。</li>
<li>使いたいドメインの DNS を <b>Cloudflare が管理していること</b>(ネームサーバーが Cloudflare を向いている)。</li>
<li>そのドメインの名前が<b>公開 DNS から引けること</b>。手元の DNS サーバーだけで解決している名前では発行できません。</li>
</ul>
<p>
証明書はドメイン単位で取ります。手元の機械が外から見えている必要はありません。
<code>maestro.example.com</code> の A レコードを <code>10.0.0.10</code> のような内側のアドレスに向ける使い方で問題ありません。
</p>
<h2>ステップ 1 — Cloudflare の API トークンを作る</h2>
<p>
Cloudflare のダッシュボードで <b>My Profile → API Tokens → Create Token</b> を開き、
<b>Custom token</b> を選んで次のとおり作ります。
</p>
<table>
<thead><tr><th>設定</th><th>値</th></tr></thead>
<tbody>
<tr><td>Permissions(1つ目)</td><td>Zone / Zone / <b>Read</b></td></tr>
<tr><td>Permissions(2つ目)</td><td>Zone / DNS / <b>Edit</b></td></tr>
<tr><td>Zone Resources</td><td>Include / Specific zone / <b>対象のゾーン 1 つだけ</b></td></tr>
</tbody>
</table>
<div class="warn">
<b>Global API Key は使わないこと</b><br>
アカウント全体を操作できる資格情報です。上のトークンは「そのゾーンの DNS を読み書きする」だけに限られており、漏れたときの被害範囲がまるで違います。
<br><br>
権限が <b>2 つ</b>必要な点に注意してください。DNS の編集だけでは足りず、ゾーンの読み取りも要ります(Caddy 側のプラグインがゾーン ID を引くため)。
</div>
<h2>ステップ 2 — DNS レコードを置く</h2>
<p>Cloudflare の DNS 設定に、MAESTRO を出したいホスト名の A レコードを追加します。</p>
<table>
<thead><tr><th>Type</th><th>Name</th><th>Content</th><th>Proxy status</th></tr></thead>
<tbody>
<tr><td>A</td><td>maestro</td><td>Caddy を動かす機械のアドレス</td><td><b>DNS only</b>(オレンジの雲を消す)</td></tr>
</tbody>
</table>
<div class="note">
<b>Proxied(オレンジの雲)にしない</b><br>
内側のアドレスを指す場合、Cloudflare のプロキシを通すと届きません。証明書の発行自体は TXT レコードで行うので、A レコードは DNS only のままで足ります。
</div>
<h2>ステップ 3 — 設定ファイルを用意する</h2>
<p>リポジトリのルートで見本をコピーし、値を埋めます。</p>
<pre><code>cp .env.caddy.example .env.caddy</code></pre>
<table>
<thead><tr><th>キー</th><th>意味</th><th>例</th></tr></thead>
<tbody>
<tr><td><code>CLOUDFLARE_API_TOKEN</code></td><td>ステップ1で作ったトークン</td><td>—</td></tr>
<tr><td><code>MAESTRO_BASE_DOMAIN</code></td><td>ワイルドカード証明書の対象。<code>*.</code> を付けた形で発行される。<b>必ず書き換える</b></td><td><code>example.com</code></td></tr>
<tr><td><code>MAESTRO_HOSTNAME</code></td><td>MAESTRO を出すホスト名。上の 1 つ下の階層にすること。<b>必ず書き換える</b></td><td><code>maestro.example.com</code></td></tr>
<tr><td><code>GATEWAY_HOSTNAME</code></td><td>ゲートウェイも出す場合だけ(後述)。使わないなら<b>行ごとコメントアウトのまま</b></td><td><code>gateway.example.com</code></td></tr>
<tr><td><code>GATEWAY_UPSTREAM_PORT</code></td><td>ゲートウェイの待ち受けポートを既定から変えている場合だけ。既定のままなら<b>行ごとコメントアウトのまま</b></td><td><code>4000</code></td></tr>
</tbody>
</table>
<div class="warn">
<b>使わないキーは「行ごとコメントアウトしたまま」にすること</b><br>
<code>GATEWAY_HOSTNAME=</code> のように<b>等号だけ書いた空の行を残さないでください</b>。
見本ファイルは該当行を <code>#</code> で始めた状態で配っているので、コピーしただけなら問題ありません。
<br><br>
Caddy の設定で使っている既定値つきの置換(<code>{$NAME:既定値}</code>)は、<b>その変数が未設定のときだけ</b>既定値に落ちます。
空文字を設定すると空文字がそのまま入り、次のようになります。
<ul>
<li><code>GATEWAY_HOSTNAME=</code> → Caddy が <code>module value cannot be null</code> で起動しない(スタック全体が上がらない)</li>
<li><code>GATEWAY_UPSTREAM_PORT=</code> → <b>エラーも警告も出ないまま</b> <code>maestro:80</code> へ中継され、502 になる</li>
<li><code>MAESTRO_BASE_DOMAIN=</code> → 証明書の対象が <code>*.</code> という成立しない名前になり、<b>黙って</b>発行を要求し続ける</li>
</ul>
見本の <code>CHANGE-ME.example</code> は穴埋め用の値です。書き換えずに起動すると、存在しないドメインで発行を試み続けます。
</div>
<div class="warn">
<b>Cloudflare のトークンは <code>.env</code> に書かないこと</b><br>
<code>.env</code> は MAESTRO のコンテナの環境変数として読み込まれます。エージェントは Bash ツールを持っているので、
そこに置いたトークンは<b>エージェントから読めます</b>。
プロキシ向けの値だけを <code>.env.caddy</code> に分けてあるのはこのためで、こちらは Caddy のコンテナしか読み込みません。
</div>
<p>
ワイルドカードが覆うのは 1 階層分だけです。<code>maestro.home.example.com</code> を使いたいなら
<code>MAESTRO_BASE_DOMAIN=home.example.com</code>(証明書は <code>*.home.example.com</code>)にします。
</p>
<h2>ステップ 4 — <code>config.yaml</code> をホスト側に置く(起動より先)</h2>
<p>
次のステップで <code>config.yaml</code> を書き換えます。その前に、<b>設定ファイルがホスト側に残る形にしておく必要があります</b>。
Docker の既定では <code>config.yaml</code> はイメージの中にあり、どのボリュームにも入っていません。
重ねがけを始めた最初の起動で MAESTRO のコンテナが作り直されるので、<b>コンテナの中で書いた設定はそこで消えます</b>。
</p>
<div class="warn">
<b>OAuth(Google / Gitea)でログインしている環境は、必ずここを先にやること</b><br>
<code>config.yaml</code> が初期状態に戻ると、<b>認証の設定ごと消えます</b>。
自動で復旧する仕組み(<code>src/bridge/initial-admin.ts</code>)はパスワードを持つローカル管理者がいる前提なので、
<b>OAuth だけで運用している環境では働きません</b>。認証設定が空のまま「意図的に認証なし」と解釈され、
<b>認証がかかっていない状態で立ち上がります</b>。
<br><br>
そのとき Caddy はもう HTTPS の口を開けています。<b>過去のデータをすべて抱えたまま、誰でも入れる状態</b>になり、
エージェントは Bash ツールを持っているので、実質的に遠隔からの任意コード実行になります。
<b>重ねる前に、必ず設定ファイルをホストへ退避してください。</b>
<br><br>
<b>この先の 4-2 を飛ばした場合も、行き着く先は同じです。</b>ホスト側にファイルを置かずに起動すると
Docker が同名のディレクトリを作り、MAESTRO はそれを読もうとして失敗しますが、
<b>その失敗は起動を止めません</b>(設定を読めなかったという警告が 1 行出るだけで、設定が空のまま先へ進みます)。
OAuth だけの環境では、そこから上と同じ経路に落ちます。
</div>
<h3>4-1. 上書きファイル側の指定(確認だけ・作業なし)</h3>
<p>
<code>docker-compose.caddy.yml</code> には、設定ファイルをホストに置くための指定がすでに入っています。書き足す作業はありません。
</p>
<pre><code>services:
maestro:
volumes:
- ./config.yaml:/app/config.yaml</code></pre>
<p>
リポジトリのルートの <code>config.yaml</code> がコンテナの中の設定ファイルになります。
以降は設定画面からの保存も、手で書いた変更も、コンテナを作り直しても残ります。
<b><code>:ro</code> は付けないでください</b>(設定画面からの保存が書き込むため)。
</p>
<div class="warn">
<b>ベースの <code>docker-compose.yml</code> で設定ファイルを別のパスから渡している場合は、その指定が捨てられます</b><br>
受け渡しはコンテナ側のパス(<code>/app/config.yaml</code>)を鍵に合成されるため、
ホスト側に <code>./config-prod.yaml</code> のような別名を書いていると、<b>上書きファイル側の <code>./config.yaml</code> が勝ちます</b>。
警告もエラーも出ないまま、<b>いままで使っていた設定が読まれなくなります</b>。
自分のパスに揃えるか、<code>docker-compose.caddy.yml</code> の該当行を自分のパスに書き換えてください。
そのまま重ねると、標準の名前のファイルがホストに無い限り、下の 4-2 の警告と同じ結末になります。
</div>
<h3>4-2. ホスト側の <code>config.yaml</code> を用意する</h3>
<div class="warn">
<b>起動より先に、実体のファイルを置くこと</b><br>
ホストに <code>./config.yaml</code> が無いまま起動すると、Docker が <b><code>config.yaml</code> という名前のディレクトリを作ります</b>。
MAESTRO はそれを設定ファイルとして読もうとして失敗しますが、<b>この失敗では止まりません</b>。
設定は<b>認証設定を持たない状態で読み込まれます</b>。
<br><br>
そこから先は環境で割れます。<b>パスワードを持つローカル管理者がいる環境と、新規インストールは起動を拒否します</b>
(認証を書き戻そうとしてディレクトリに書けず、その旨を名指しして止まります)。
しかし <b>OAuth(Google / Gitea)だけで運用している環境は、認証なしで起動してしまいます</b>。
書き戻しの分岐がどれも成立せず、空の設定が「意図的に認証なし」と解釈されるためです。
<b>この節をやってから起動してください。</b>やり直しの手当てではなく、これが唯一の防ぎ方です。
</div>
<p><b>すでに MAESTRO を動かしている場合</b> — 重ねる前に、動いているコンテナから取り出します。</p>
<pre><code>docker cp maestro:/app/config.yaml ./config.yaml</code></pre>
<p><b>これから始める場合</b> — イメージが同梱する初期状態と同じ 1 行だけのファイルを作ります。</p>
<pre><code>printf 'config_version: 2\n' &gt; ./config.yaml</code></pre>
<p>
<b>持ち主と書き込み権限</b> — コンテナの中の MAESTRO は root ではなく <code>node</code>(UID 1000)で動くので、
ホスト側のファイルは<b>その UID から書ける状態</b>にしておきます。上の 2 通りはどちらも、
実行した人の持ち物になります。ホストの利用者が UID 1000 でないなら、所有者を合わせてください。
</p>
<pre><code>sudo chown 1000:1000 ./config.yaml</code></pre>
<p>
食い違ったままだと、<b>すでに動いている環境では設定画面からの保存が静かに失敗します</b>(起動は通るので気づきにくい)。
新規インストールでは起動を拒否し、書き込めない旨を名指しするメッセージが出ます。
</p>
<p>
<b>配布用のドキュメントとの食い違いについて</b> — 配布版の <code>docs/docker.md</code>(この repo では <code>oss/overlay/docs/docker.md</code>)には
<code>cp config.yaml.example config.yaml</code> で作る案内がありますが、そちらは完成済みの見本なので、
<b>インストール済みと判定されてセットアップウィザードが出ません</b>(さらに、動いていない localhost の LLM を指した状態になります)。
イメージが同梱する初期状態は 1 行だけのファイルなので、<b>ここに書いたほうに従ってください</b>。
</p>
<p>
どちらの場合も、この先の編集はすべて<b>この、ホスト側の <code>config.yaml</code></b> に対して行います。
コンテナの中に入って書く必要はありません。
</p>
<h2>ステップ 5 — MAESTRO 側の設定を 2 か所直す</h2>
<p>
ステップ4で用意した<b>ホスト側の <code>config.yaml</code></b> を編集します。
<b>この 2 つを外すと、動かないか、危ない状態になります。</b>
</p>
<h3>5-1. MAESTRO 自身の TLS は切る</h3>
<p>
Caddy が TLS を終端するので、MAESTRO 側でも終端すると二重になって噛み合いません。
<code>config.yaml</code> の <code>server.tls</code> ブロックを<b>書かない</b>か、<code>enabled: false</code> にします。
</p>
<pre><code># config.yaml — プロキシ構成では server ブロックごと省くのが確実
# server:
# tls:
# enabled: false</code></pre>
<p>
Docker のイメージが同梱する初期 <code>config.yaml</code> には <code>server</code> ブロックが入っていないため、
Docker で始めた環境は既定で TLS が切れています。自分で有効にした覚えがなければ、この項目の作業は不要です。
</p>
<h3>5-2. 安全なクッキーを有効にする</h3>
<p>
<code>auth.secure_cookie: true</code> にします。効き目は 2 つです。
</p>
<pre><code>auth:
secure_cookie: true</code></pre>
<table>
<thead><tr><th>入れると起きること</th><th>入れないとどうなるか</th></tr></thead>
<tbody>
<tr>
<td>セッションのクッキーに <b>secure 属性</b>が付く</td>
<td>クッキーが平文の HTTP でも送られる状態のままになります</td>
</tr>
<tr>
<td><b>転送元の情報を信頼する設定(trust proxy)が連動して入り</b>、接続元アドレスの判定が転送ヘッダを見るようになる</td>
<td>
<b>これが本題です。</b>Caddy を挟むと、MAESTRO からは<b>全利用者が同じ接続元アドレスに見えます</b>。
ログイン試行の制限は接続元アドレスごとに数えているので、全員が<b>ひとつの枠を共有</b>することになり、
<b>全員合わせて 15 分間に 10 回失敗すると、15 分間まとめて締め出されます</b>。
1 人が 10 回間違える必要はありません。別々の 10 人が 1 回ずつ打ち間違えても同じ枠に積まれます。
<b>発動する前なら</b>、誰か 1 人がログインに成功した時点で積んだ回数は消えます。
発動したあとは成功では戻らず(締め出しの判定はパスワードの照合より手前で打ち切ります)、15 分が明けるのを待つことになります。
</td>
</tr>
</tbody>
</table>
<p>
この締め出しは、メールとパスワードでログインする構成のときに起きます(OAuth だけの環境には、この制限そのものがありません)。
なお <b>この設定が無くても HTTPS 越しのログイン自体は通ります</b>。プロトコルによって行き先が変わるリダイレクトは MAESTRO のコードにありません。
「入れないと繋がらない」ではなく「入れないと、全員が道連れで締め出される」と考えてください。
</p>
<div class="warn">
<b>この設定を入れると、Caddy を通さない直接アクセスではログインできなくなります</b><br>
secure 属性の付いたクッキーは平文の HTTP では送られません。切り分けのために
<code>http://&lt;host&gt;:9876</code> へ直接繋いで確かめる、という使い方はできなくなります(そもそもステップ6でポートを閉じます)。
</div>
<h2>ステップ 6 — 起動する</h2>
<h3>6-1. 2 つのファイルを既定にする(推奨)</h3>
<p>
先に <code>.env</code>(<code>.env.caddy</code> ではありません)に次の 1 行を足してください。
以降は <code>docker compose</code> のどのコマンドでも 2 つのファイルが読まれます。
</p>
<pre><code>COMPOSE_FILE=docker-compose.yml:docker-compose.caddy.yml</code></pre>
<div class="warn">
<b>これを設定しないと、<code>-f</code> を付け忘れた起動が 9876 番を開き直します</b><br>
重ねがけを始めたあとに、習慣で <code>docker compose up -d</code> とだけ打つと、Compose は<b>ベースのファイルだけ</b>を見ます。
MAESTRO のコンテナが作り直され、<b>平文の 9876 番がホストに再び公開されます</b>。
Caddy のコンテナは孤児として検出されるだけで止まらないので、<b>HTTPS の口と平文の口が同時に開いた状態</b>になります。
ブラウザから見ると何も変わっていないので、気づきません。
</div>
<p>
<code>COMPOSE_FILE</code> は <code>.env</code> に書いて構いません。ステップ3で「トークンは <code>.env</code> に書くな」と書いたのは、
<code>.env</code> が MAESTRO のコンテナの環境変数として読み込まれ、エージェントから読めてしまうからです。
<code>COMPOSE_FILE</code> は秘密ではなく、Compose 自身がどのファイルを読むかの指定なので、読まれても実害がありません。
<br>
<small>(<code>COMPOSE_FILE</code> は Compose の実行時に解釈されるため、<code>.env</code> に置く必要があります。<code>.env.caddy</code> に書いても効きません。)</small>
</p>
<h3>6-2. 起動する</h3>
<pre><code>docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d --build</code></pre>
<p>(6-1 を設定済みなら <code>docker compose up -d --build</code> だけで同じです。)</p>
<p>
初回は Caddy のイメージをビルドします。公式の配布物には Cloudflare の DNS 対応が入っていないため、
<code>xcaddy</code> で組み込んだものを作ります(<code>deploy/caddy/Dockerfile</code>)。数分かかりますが、初回だけです。
</p>
<p>ログで発行を確認します。</p>
<pre><code>docker compose -f docker-compose.yml -f docker-compose.caddy.yml logs -f caddy</code></pre>
<p>
<code>certificate obtained successfully</code> が出れば取れています。
あとは <code>https://maestro.example.com</code> を開くだけです。
</p>
<div class="tip">
<b>初回は検証用の発行先で試し撃ちしておくと安全です</b><br>
Let's Encrypt は<b>発行の失敗にも回数制限</b>があり、設定を直したあとしばらく取れない状態に入ることがあります。
<code>deploy/caddy/Caddyfile</code> の先頭に置く global options block に
<code>acme_ca https://acme-staging-v02.api.letsencrypt.org/directory</code> を 1 行足すと、検証用の発行先に向きます。
通ることを確かめたらこの行を消して起動し直せば、本番の発行先から取り直します(検証用の証明書はブラウザに信頼されないので、そのままでは使えません)。
検証用と本番の証明書は <code>caddy-data</code> の中で別々に置かれるので、<b>ボリュームを消す必要はありません</b>。
</div>
<h2>何が変わったか</h2>
<table>
<thead><tr><th></th><th>前</th><th>後</th></tr></thead>
<tbody>
<tr><td>外に出ているポート</td><td>MAESTRO の 9876(全ネットワーク面)</td><td>Caddy の 80 / 443 のみ</td></tr>
<tr><td>MAESTRO の 9876</td><td>ホストに公開</td><td><b>公開しない</b>。Docker の内側から Caddy だけが届く</td></tr>
<tr><td>TLS の終端</td><td>MAESTRO 自身(自己署名)または平文</td><td>Caddy(正式な証明書)</td></tr>
<tr><td><code>config.yaml</code></td><td>イメージの中。コンテナを作り直すと初期状態に戻る</td><td>ホスト側の <code>./config.yaml</code>。作り直しても残る</td></tr>
</tbody>
</table>
<p>
ポートを閉じているのは <code>docker-compose.caddy.yml</code> の <code>ports: !reset []</code> という 1 行です。
Compose の <code>profiles</code> ではこれができません。<code>ports</code> は
<code>{ip, target, published, protocol}</code> を一意キーとして<b>追記</b>されるだけで、
元のファイルが書いた公開を取り消す手段が無いためです。
重ねたファイルから元の値を消すには <code>!reset</code> を使います(<code>!override []</code> と書いても同じ結果になります)。
</p>
<div class="note">
<b>Compose の版について</b><br>
<code>!reset</code> 自体は v2.24 より前から使えます。<b>v2.24 以上</b>を要求しているのは
ベースの <code>docker-compose.yml</code> が使っている <code>env_file</code> の新しい書き方(<code>path:</code> / <code>required:</code>)のほうです。
そのため v2.24 未満の環境では、重ねる以前にベースのファイルが読めません。<b>Compose を上げてください。</b>
</div>
<h2>ゲートウェイ(AAO Gateway)も HTTPS で出す場合</h2>
<p>
<b>ゲートウェイを使っていないなら、この節は読み飛ばして構いません。</b>
</p>
<p>
ゲートウェイ(この MAESTRO 自身を OpenAI 互換の中継口として公開する仕組み)は、
<b>MAESTRO 本体とは別のポートで待ち受けます</b>。同じプロセスで動かしていても口は別です(既定 4000)。
プロキシを通すなら、そちら向けの設定も要ります。
</p>
<ol class="steps">
<li>Cloudflare に <code>gateway</code> の A レコードを追加する(ステップ2と同じ要領。同じアドレスでよい)。</li>
<li><code>.env.caddy</code> の <code>GATEWAY_HOSTNAME</code> のコメントを外して値を入れる。<b>コメントを外したら必ず値を書くこと</b>(等号だけの空の行は、Caddy を起動しなくします)。</li>
<li><code>config.yaml</code> の <code>gateway.listen_port</code> を既定(4000)から変えている場合<b>だけ</b>、<code>GATEWAY_UPSTREAM_PORT</code> のコメントを外して同じ値を書く。既定のままなら<b>コメントアウトのまま</b>にする(外して空にすると、黙って 80 番へ中継されます)。</li>
<li>再起動する(<code>up -d</code> をもう一度)。</li>
</ol>
<p>証明書は本体と同じワイルドカード 1 枚で覆われるので、追加の発行はありません。</p>
<div class="warn">
<b>ゲートウェイのホスト名も <code>MAESTRO_BASE_DOMAIN</code> の 1 つ下の階層にすること</b><br>
別のドメインにしたり、2 階層下(<code>gw.sub.example.com</code> のような形)にしたりすると、
ワイルドカード証明書もサイトの照合も覆いません。そのホスト名は<b>無反応になり、Caddy は何も言いません</b>。
本体のホスト名と同じ制約です。
</div>
<table>
<thead><tr><th>気にしなくてよいこと</th><th>理由</th></tr></thead>
<tbody>
<tr>
<td>応答の溜め込み</td>
<td>ゲートウェイは生成されたトークンを 1 つずつ流します。Caddy は中継する応答を溜め込まないので、そのまま届きます(nginx は既定で溜め込むため、そちらを選んだ場合は明示的に切る必要がありました)。</td>
</tr>
<tr>
<td>長い生成の時間切れ</td>
<td>ゲートウェイは 1 リクエストに最大 600 秒(<code>gateway.request_timeout_sec</code>)を認めています。Caddy は中継に時間制限を設けないので、途中で切られません。</td>
</tr>
<tr>
<td>流量制限が一緒くたになる</td>
<td>
ゲートウェイの制限は<b>発行した鍵ごと</b>に数えており、接続元のアドレスは見ていません。プロキシを挟んで全員が同じアドレスに見えても、混ざりません。
<br><b>これはゲートウェイの流量制限の話です。ログイン試行の制限は別物で、そちらは接続元のアドレスで数えます</b>(ステップ5-2 の安全なクッキーを入れること)。
</td>
</tr>
</tbody>
</table>
<div class="warn">
<b>ゲートウェイのポートを自分でホストに公開していた場合</b><br>
<code>docker-compose.yml</code> に <code>4000:4000</code> のような行を足していたなら、その行は消してください。
Caddy 経由にしたのにポートも出したままだと、平文の口が残ります。
</div>
<h2>よくある詰まり</h2>
<table>
<thead><tr><th>症状</th><th>原因 / 対処</th></tr></thead>
<tbody>
<tr>
<td><code>env file .env.caddy not found</code> で起動しない</td>
<td>見本をコピーしていない。<code>cp .env.caddy.example .env.caddy</code> を実行する。<b>意図的に失敗させています</b>(トークン無しで黙って起動しても証明書は取れないため)。</td>
</tr>
<tr>
<td><code>API token ... appears invalid</code></td>
<td>
<code>CLOUDFLARE_API_TOKEN</code> の<b>値の形式が合っていない</b>——空、引用符や波括弧で囲んでいる、貼り付けが途中で欠けている、など。値だけを裸で書く。
<b>この検査は設定の読み込みの早い段階で走るので、これが出ている間はほかの設定の誤りは表示されません。</b>
トークンを直して初めて、下の行のようなエラーが出てくることがあります。
</td>
</tr>
<tr>
<td><code>module value cannot be null</code> で Caddy が起動しない</td>
<td>
<code>MAESTRO_HOSTNAME</code> / <code>GATEWAY_HOSTNAME</code> のどれかが<b>空文字</b>になっている。
とくに <code>GATEWAY_HOSTNAME=</code> と等号だけ書いた行が原因になりがちです(使わないなら<b>行ごとコメントアウト</b>に戻す)。
<code>MAESTRO_BASE_DOMAIN</code> を空にしてもこの文言は出ません(下の行を参照)。
</td>
</tr>
<tr>
<td>証明書が取れず、ログに <code>*.</code> という対象が出る</td>
<td>
<code>MAESTRO_BASE_DOMAIN</code> が<b>空文字</b>。Caddy の設定検証は通ってしまうため、エラーは出ません。
成立しない名前で発行を要求し続けるので、気づかないと Let's Encrypt の失敗回数の制限に当たります。値を書く。
</td>
</tr>
<tr>
<td>ゲートウェイのホスト名だけ 502 になる</td>
<td>
<code>GATEWAY_UPSTREAM_PORT=</code> と等号だけ書いていて、中継先が <code>maestro:80</code> になっている。
<b>Caddy は何も言いません。</b>既定(4000)のままなら行ごとコメントアウトに戻す。変えているなら値を書く。
</td>
</tr>
<tr>
<td>ゲートウェイのホスト名が何も返さない/繋がらない</td>
<td><code>GATEWAY_HOSTNAME</code> が <code>MAESTRO_BASE_DOMAIN</code> の 1 つ下の階層になっていない。別ドメインや 2 階層下は覆われません。</td>
</tr>
<tr>
<td><code>expected 1 zone, got 0</code></td>
<td>
Cloudflare にゾーンを問い合わせた結果が 0 件。原因はふつう次のどれかです。
<ul>
<li>トークンの Zone Resources を<b>別のゾーン</b>に絞っている</li>
<li>トークンに <b>Zone / Zone / Read</b> を付けていない</li>
<li>そのアカウントにゾーンとして載っていない(ネームサーバーが Cloudflare を向いていない)</li>
</ul>
</td>
</tr>
<tr>
<td><code>timed out waiting for record to fully propagate</code></td>
<td>手元の名前解決が古い答えを返している。<code>deploy/caddy/Caddyfile</code> の <code>resolvers 1.1.1.1 1.0.0.1</code> が効くはずですが、外向きの DNS が塞がれている環境では通りません。</td>
</tr>
<tr>
<td><code>bind: address already in use</code></td>
<td>ホストの 80 / 443 を他のものが使っている。そちらを止めるか、<code>docker-compose.caddy.yml</code> の <code>ports</code> を空いている番号に変える(443 を変えると URL にポート番号が必要になります)。</td>
</tr>
<tr>
<td>全員が急にログインできなくなった(15 分ほどで戻る)</td>
<td>
<code>auth.secure_cookie: true</code> を入れていない。Caddy を挟むと全利用者が同じ接続元アドレスに見えるため、
<b>ログイン試行の制限がひとつの枠を共有</b>します。<b>全員合わせて</b> 15 分間に 10 回失敗すると、15 分間まとめて締め出されます。
発動する前なら誰か 1 人の成功で積んだ回数は消えますが、発動したあとは成功では戻りません。
ステップ5-2 を確認する(メールとパスワードでログインする構成のときに起きます)。
</td>
</tr>
<tr>
<td>ログインしても弾かれる/ログイン画面に戻る</td>
<td><code>auth.secure_cookie: true</code> を入れた状態で、平文の口に直接繋いでいる。secure 属性の付いたクッキーは HTTP では送られません。<code>https://</code> のホスト名で開く。</td>
</tr>
<tr>
<td>設定を書いたのに、起動したら元に戻っている</td>
<td>ホスト側の <code>config.yaml</code> を用意せずに重ねがけを始めた。ステップ4 をやり直す。<b>OAuth だけで運用している場合は認証ごと消えている</b>ので、まずポートを閉じてから復旧すること。</td>
</tr>
<tr>
<td>設定ファイルが読めないという警告が出る/MAESTRO が起動を拒否する</td>
<td>
ホストに <code>./config.yaml</code> が無いまま起動し、Docker が同名の<b>ディレクトリ</b>を作っている。
そのディレクトリを消し、ステップ4-2 のとおりファイルを置き直す。
<b>読めなかったこと自体では起動は止まりません</b>——設定は認証設定を持たない状態で読み込まれ、
そのあと環境によって割れます。パスワードを持つローカル管理者がいる環境と新規インストールは起動を拒否しますが、
<b>OAuth だけの環境は認証なしで立ち上がっています</b>。その場合はまずポートを閉じてから復旧すること。
</td>
</tr>
<tr>
<td>OAuth(Google / Gitea)でログインできない</td>
<td>コールバック先が古いまま。<code>auth.providers.*.callback_url</code> と、プロバイダ側に登録した URL を <code>https://maestro.example.com/...</code> に直す。</td>
</tr>
<tr>
<td>証明書を取り直してばかりで制限に当たった</td>
<td><code>caddy-data</code> ボリュームを消している。ここに証明書と ACME アカウント鍵が入るので、消さないこと。</td>
</tr>
</tbody>
</table>
<h2>手動で組む場合(Docker を使わない)</h2>
<p>
Docker を使わず、ホストに直接 Caddy を入れる場合も考え方は同じです。違いは 2 点だけです。
</p>
<ol class="steps">
<li>
<b>Caddy を自分でビルドする。</b>公式の配布物には Cloudflare 対応が入っていません。
<code>xcaddy build --with github.com/caddy-dns/cloudflare</code> で作るか、
<a href="https://caddyserver.com/download">公式のダウンロードページ</a>で <code>caddy-dns/cloudflare</code> を選んで取得します。
</li>
<li>
<b>中継先をコンテナ名からホストの口に変える。</b><code>deploy/caddy/Caddyfile</code> の
<code>reverse_proxy maestro:9876</code> を <code>reverse_proxy 127.0.0.1:9876</code> にします。
合わせて MAESTRO 側は <code>HOST=127.0.0.1</code> で起動し、外から直接届かないようにします。
</li>
</ol>
<p>
環境変数(<code>CLOUDFLARE_API_TOKEN</code> など)は、systemd で動かすなら
<code>systemctl edit caddy</code> で <code>Environment=</code> として渡します。Caddyfile に直接書かないでください。
ステップ5 の MAESTRO 側の設定(TLS を切る・安全なクッキー)は Docker の場合と同じです。
ステップ4(<code>config.yaml</code> をホスト側に置く)は<b>不要</b>です。Docker を使わない構成では設定ファイルは最初からホストにあり、消えません。
</p>
<h2>関連ファイル</h2>
<table>
<thead><tr><th>パス</th><th>役割</th></tr></thead>
<tbody>
<tr><td><code>docker-compose.caddy.yml</code></td><td>ベースに重ねる上書きファイル。Caddy を足し、MAESTRO のポート公開を消す</td></tr>
<tr><td><code>deploy/caddy/Dockerfile</code></td><td>Cloudflare 対応を組み込んだ Caddy を作る 2 段ビルド</td></tr>
<tr><td><code>deploy/caddy/Caddyfile</code></td><td>証明書の取り方と中継先の定義</td></tr>
<tr><td><code>.env.caddy.example</code></td><td>トークンとホスト名の見本。実物(<code>.env.caddy</code>)は追跡していない</td></tr>
</tbody>
</table>
<footer class="doc">MAESTRO — docs/operations/caddy-cloudflare-tls.html / 完成版(2026-08-10, issue #1094)</footer>
</div>
</body>
</html>