839の日記

趣味の話を書くブログです。

Mac mini 2018 (Apple T2) にProxmox VE 9.2を導入した

はじめに

使っていなかったMac mini 2018を自宅サーバーとして活用するため、Proxmox VE 9.2を導入しました。 Mac mini 2018はIntel CPUを搭載しているため、アーキテクチャ上は一般的なx86_64のLinuxサーバーとして利用できます。一方、Apple T2セキュリティチップを搭載しているため、通常のPCと同じように扱うには少々厄介な部分があります。

正直なところ、苦労してMac miniを使うくらいなら、メルカリで売却して普通のミニPCに買い替えることも考えました。 ただ売却できたとしても、おそらく4〜5万円程度。その予算で、現在のMac miniと同等の32GB RAM・1TB SSD・10GbEを備えたミニPCを購入するのは難しそうです。 ハードウェア自体はまだ十分使えるスペックですし、買い替えに追加費用をかけるのももったいない。ということで、Apple特有の問題と向き合いながら、なんとかProxmoxを動かすことにしました。

今回の作業では、特に以下の問題に遭遇しました。

  • Apple T2による外部メディアの起動制限
  • macOSを残したままLinuxをインストールするためのディスク構成
  • VentoyからDebianインストーラーを起動できない問題
  • applesmc の初期化失敗によって起動に約2分かかる問題
  • Linux標準のドライバーではファン制御ができない問題

最終的には、macOSとのデュアルブートを維持したままProxmox VE 9.2を導入し、ファン制御まで動作する状態にできました。

1. macOSを残してProxmoxを導入する方針

最初はmacOSを完全に消してProxmox専用機にすることも考えましたが、将来的にMac miniを売却する可能性も一応あるので、通常どおりmacOSを起動できる状態は残しておくことにしました。 Intel Mac向けmacOSの今後のメジャーアップデートはあまり期待していませんが、だからといってmacOSを削除する必要もありません。 そこで、macOSとProxmoxのデュアルブート構成を目指しました。

なぜDebian 13経由でProxmoxをインストールするのか

Proxmox VEには公式のインストールISOが用意されており、通常はそれを使えば簡単に導入できます。 ただし、Proxmoxの公式インストーラーは基本的にインストール先ディスク全体を初期化する方式です。

今回のMac miniはSSDが1台しかなく、そのSSDにはmacOSがインストールされています。 公式ISOでそのままインストールするとmacOSの領域も消えてしまうため、別の導入方法が必要でした。

そこで採用したのが、先にDebian 13をインストールし、その上にProxmox VE 9.2を導入する方法です。 Debianのインストーラーなら、既存のmacOSやEFIパーティションを保持したまま、空き領域にLinuxをインストールできます。 その後、DebianにProxmoxのAPTリポジトリを追加して、Proxmoxカーネルと管理パッケージを導入します。

Mac mini 2018
├── macOS Sequoia
│   └── 既存のAPFS領域を保持
│
└── Debian 13 + Proxmox VE 9.2
    ├── Debianの基本システム
    ├── Proxmoxカーネル
    ├── Proxmox管理サービス
    ├── KVM / QEMU
    └── LXC

これはDebian上の仮想マシンでProxmoxを動かすという意味ではありません。DebianそのものがProxmoxホストになります。 そもそもProxmox VE 9はDebian 13をベースにしているため、公式ISOでインストールした場合もDebianの基本システムを利用しています。 したがって、今回の構成だからDebianを別途メンテナンスする必要があるわけではなく、普段のセキュリティアップデートもAPTでDebianとProxmoxをまとめて適用できます。

macOSの領域を維持するためにDebianのインストーラーを利用する、というのが今回の方法を選んだ最大の理由です。

2. Apple T2の起動セキュリティ設定

Mac mini 2018にはApple T2セキュリティチップが搭載されています。 このため、Linuxをインストールする前に起動セキュリティの設定を変更する必要がありました。

設定手順

  1. Macを再起動し、Command + R を押してmacOS復旧環境を起動
  2. 「ユーティリティ」から「起動セキュリティユーティリティ」を開く
  3. 「安全な起動」を「セキュリティなし」に変更
  4. 「外部起動」を許可

今回の環境では、この設定によりUSBメモリからDebianのインストーラーを起動できるようになりました。

注意点として、セキュリティなしに変更するとAppleの起動時検証の保護が弱まります。Linuxを起動するための設定変更ではありますが、セキュリティ上のトレードオフがあります。 また、T2搭載MacでのLinux動作には独自の制約があるため、一般的なIntel Macの導入手順をそのまま適用できるとは限りません。

参考: https://wiki.t2linux.org/

3. VentoyからDebianインストーラーが起動しない

Debian 13のインストール用USBメモリを用意したところ、早速トラブルに遭遇しました。 当初は普段使っているVentoyを使用して、Debian 13のnetinst ISOを起動しようとしましたが、正常に起動できませんでした。

Ventoy Normal Mode

通常の起動方法を選択すると、Start Device Error のようなエラーが発生し、インストーラーまで進めませんでした。

Ventoy GRUB2 Mode

GRUB2 ModeではDebianインストーラーのメニューまでは表示されました。 しかし、Install を選択すると、そのまま画面が停止してしまいました。

カーネルオプションに nomodeset を追加しても改善しませんでした。

解決策:ISOをUSBメモリへ直接書き込む

最終的に、Ventoyの使用を諦め、Raspberry Pi ImagerでDebianのISOをUSBメモリに直接書き込みました。

そのUSBメモリからMac miniを起動すると、Appleの起動選択画面には複数の EFI Boot が表示されました。 今回の環境では、そのうち2番目を選択するとDebianインストーラーを起動できました。 原因をVentoy側の特定の不具合まで切り分けたわけではありませんが、少なくともこのMac miniとDebian 13の組み合わせでは、ISOを直接書き込む方法で解決しました。

変なハマりを回避したいなら、素直にUSBにISOを直接書き込むほうが良いと思います。

4. macOSを残してDebian 13・Proxmox VE 9.2をインストールする

無事にDebianインストーラーを起動できたので、次は既存のmacOSを残したままProxmoxを導入します。

macOSのパーティションを縮小する

今回のディスク構成で気を付けたのは、既存のEFI領域とmacOSのAPFS領域を破壊しないことです。 あらかじめmacOS側でAPFSの領域を約80GBに縮小し、残りの空き領域をLinux側で利用することにしました。

最終的なパーティション構成は以下のとおりです。

パーティション 容量 用途
/dev/nvme0n1p1 300MiB EFI System Partition
/dev/nvme0n1p2 74.5GiB macOS / APFS
/dev/nvme0n1p3 59.6GiB Debian / Proxmox
/dev/nvme0n1p4 797.4GiB Proxmox用LVM

これはProxmoxの導入とストレージ構築まで完了した時点の構成です。Debianのインストール時点では、約797GiBの領域は未割り当てのまま残していました。

EFI領域は既存のものを使用する

Debianインストーラーではパーティション設定を手動にしました。 ポイントは、既存EFIパーティションを /boot/efi にマウントしつつ、フォーマットしないことです。

macOSのAPFS領域も変更せず、Debian用に約64GBのext4パーティションを新規作成しました。 swap用の専用パーティションは作成していません。 残りの領域は、Proxmox導入後にLVM-thin用として構成しています。 これによってmacOSとDebianの両方を保持できました。

実際にすべての作業後にmacOS側への起動も確認できています。

Debian 13からProxmox VE 9.2を導入する

Debian 13のインストール後、ProxmoxのAPTリポジトリを追加しました。

まず署名鍵をインストールします。

wget https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg \
  -O /usr/share/keyrings/proxmox-archive-keyring.gpg

続いて、APTリポジトリを追加します。

/etc/apt/sources.list.d/pve-install-repo.sources

Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg

まずProxmoxのカーネルをインストールします。

この際、万が一Proxmoxカーネルで正常に起動できなかった場合に備え、元のDebianカーネルを復旧用として保持することにしました。

apt update
apt-mark manual linux-image-6.12.113+deb13-amd64
apt full-upgrade
apt install proxmox-default-kernel

apt-mark manual で指定しているカーネルのバージョンは今回の環境のものなので、別の環境で実施する際は読み替える必要があります。 Proxmoxカーネルで再起動できることを確認してから、本体を導入しました。

apt install proxmox-ve postfix open-iscsi chrony

導入後は、以下のコマンドでカーネルを確認しました。

uname -r

結果:

7.0.14-26-pve

これでProxmox VE 9.2の導入が完了しました。 Debian経由で導入したため、Proxmox公式ISOでインストールした場合とは異なり、ネットワークブリッジやLVM-thinストレージなどは手動で構成しています。

5. Proxmoxの起動に約2分かかる問題

Proxmoxのインストール後、システムの起動に約2分かかる問題が発生しました。 起動そのものには成功するものの、サーバーとして使うにはかなり長い状態です。

systemd-analyzeで原因を調べる

まず起動時間を確認しました。

systemd-analyze
systemd-analyze blame

結果は以下のとおりです。

Startup finished in 1.638s (kernel)
+ 2min 64ms (userspace)
= 2min 1.703s

時間がかかっていたサービスは次の2つでした。

1min 45.315s ifupdown2-pre.service
1min 45.314s systemd-udev-settle.service

最初はProxmoxのネットワーク設定に問題があるのかと思いました。 しかし、実際にはネットワーク自体ではなく、デバイスの初期化待ちが原因でした。

原因はapplesmcの初期化失敗

カーネルログを確認すると、次のエラーが出ていました。

applesmc applesmc.768: probe with driver applesmc failed with error -5
applesmc: driver init failed (ret=-5)!

applesmc はAppleのSMCにアクセスするLinuxカーネルドライバーです。

今回の環境では、Apple T2を搭載したMac miniに対してこのドライバーの初期化が失敗していました。 その影響でudevのデバイス初期化待ちが長引き、結果として起動時間が約2分になっていました。

暫定対策:applesmcをブラックリストへ登録

まずは問題のドライバーを自動ロードしないようにしました。

cat > /etc/modprobe.d/blacklist-applesmc.conf <<'EOF'
blacklist applesmc
EOF

update-initramfs -u -k all
reboot

再起動すると、起動時間は大きく改善しました。

Startup finished in 1.639s (kernel)
+ 10.324s (userspace)
= 11.964s
項目 対策前 対策後
起動時間 約122秒 約12秒
udev待機 約105秒 約3秒

約110秒の短縮です。

注意:ブラックリスト登録は最終的な解決策ではない

applesmc を無効化することで起動時間は改善しましたが、それだけではファン制御やSMC関連の機能が正常に使えるとは限りません。 Mac miniを常時稼働させるのであれば、CPU温度だけでなく、ファンの回転数や負荷時の熱挙動も確認する必要があります。 今回もこの暫定対策で終わらせず、次にT2対応ドライバーを導入しています。

6. Apple T2搭載Mac miniでファン制御を実現する

Mac mini 2018をLinuxサーバーとして運用するうえで、最も気になったのがファン制御です。

通常のLinuxでは、lm-sensors や /sys/class/hwmon を通して温度やファン情報を取得できます。 今回もCPU、NVMe、チップセットなどの温度センサーは利用できていました。 一方、標準の applesmc ドライバーは初期化に失敗しており、Apple T2環境のファン制御が利用できない状態でした。

パッチ済みapplesmc-t2とt2fanrdを導入

その後、T2に対応した applesmc-t2 と、ファン制御デーモンの t2fanrd を使用する構成に変更しました。

主な構成は次のとおりです。

項目 最終構成
SMCドライバー パッチ済み applesmc-t2
ファン制御 t2fanrd
ファンカーブ 35〜65℃で線形制御
CPU PL1 55W
CPU PL2 80W
Turbo Boost 有効
異常時 最大回転へのフォールバック

ドライバーについてはDKMSを利用し、再起動後もファン制御設定が維持されることを確認しています。 また、制御デーモンが停止した場合にファンを最大回転へフォールバックさせる動作や、異常終了後の自動復旧についても検証しました。

CPU電力制限も調整

ファン制御だけでなく、CPUの電力制限も設定しました。 今回の最終構成では、以下の値を使用しています。

PL1: 55W
PL2: 80W
Turbo Boost: enabled

これらは今回の個体と冷却環境で検証した設定であり、すべてのMac miniに推奨する値ではありません。

ベンチマーク結果

今回使用したMac miniのスペックは次のとおりです。

項目 スペック
モデル Mac mini 2018(Macmini8,1)
CPU Intel Core i7-8700B
メモリ 32GB
SSD Apple SSD AP1024M / 1TB
ネットワーク 10GbE
セキュリティチップ Apple T2
Linux Debian 13(Trixie)
Proxmox Proxmox VE 9.2
カーネル 7.0.14-26-pve

Geekbench 6.7.1を実行した結果は以下でした。

項目 スコア
Single-Core 1,672
Multi-Core 6,219

一方で、負荷試験中には最高100℃、熱制限カウンターの増加も観測しました。 つまり、ファン制御が機能し、通常再起動後も復旧することは確認できましたが、すべての高負荷条件でサーマルスロットリングが発生しないわけではありません。 サーバー用途ではベンチマークの最高スコアだけでなく、長時間負荷に対する温度・電力・ファンの挙動が重要だと思います。

今回の検証では、ファン制御がLinuxから利用できるようになったことに加え、異常時のフォールバックまで確認していました。

まとめ

Mac mini 2018にProxmox VE 9.2を導入し、macOSとのデュアルブートを維持したまま自宅サーバーとして利用できる構成を作りました。

今回のハマりどころをまとめると、次のとおりです。

問題 対処
T2によるUSB起動制限 起動セキュリティ設定を変更
macOSを保持したい Debianを手動パーティションにインストール
VentoyでDebianが起動しない ISOをUSBへ直接書き込む
起動に約2分かかる applesmc の初期化失敗を特定
ファン制御が使えない applesmc-t2 + t2fanrd を導入

特に苦労したのは、Apple T2に関連する部分です。 一般的なPCであればほとんど意識しない起動セキュリティやSMCの問題に、思った以上に時間を取られました。

一方、これらを解決できれば、10GbEと32GBのメモリを搭載したMac mini 2018は、Proxmoxの小型ホストとして十分活用できると感じています。

参考資料

UGREEN NASync DXP4800 Plusに別OSを入れる

はじめに

FAQに

UGREEN NASに unraid または true nas をインストールできますか?
これらのデバイスにはサードパーティのシステムがインストールされている可能性がありますが、非公式にサポートされているシステムをインストールすると潜在的な問題が発生する恐れがあります。これらのサードパーティOSを仮想マシンにインストールすることをお勧めします。

と記載されているので自己責任で。

UGOSのバックアップ

前提としてDXP4800 PlusはM.2スロットが3つあり、2つはユーザーが自由に取り外しができる。 1つのM.2スロットはUGOS(UGREEN独自のOS)が入っていて、初期状態ではそこから起動する。 このUGOS用スロットを無視して、他の2スロットのどちらかにOSを入れて使う方法もあるが、せっかくなら3スロットすべて活用したいところ。 そこでOSを入れ替える前にUGOSを万が一のためにバックアップしておく。

バックアップ方法の選択肢

バックアップには以下の2通りがある。

  • 物理的な方法:SSDを分解して抜き出し、別のストレージとして保管(メーカー保証が無効になる可能性あり)
  • 論理的な方法:SSDの内容を丸ごとコピー

UGOSが入っているSSDは128GBなので、容量に不満がある場合は物理的に交換するしかない。 自分はリセールバリューも考えて、論理的な方法でバックアップを取った。 ただし論理的な方法は、ミスると復元できなくなるリスクがあるためハイリスクになる。 不安な方は別ディスクに復元までの動作確認をしておくのがおすすめ。

実施した手順 (概要)

  1. NASに適当に1枚SSDを挿してUGOSを起動し、ストレージとして扱えるようにしておく
  2. UGOSのUIからSSHを有効化してSSHし、ddコマンドでシステムSSDのコピーを1で挿したSSDに保存
  3. 2で保存したコピーを手元のPCに保存
  4. 再起動中にCtrl+F12を連打してBIOSに入り、Watchdogを無効化 & 挿したSSDがブートディスクとして使えるように設定
  5. USB等から自分が挿したSSDにOSをインストール
  6. インストール完了後の再起動後にCtrl+F2を連打してブートディスク変更
  7. システム用SSDをddコマンドでコピーを取って何らかの方法で手元のPCに保存

最短経路で行くなら初手BIOSでWatchdog無効化 & USBブートで任意のOS起動からのシステムSSDを別のUSBに保存とかもできると思う。 事故ってUGOSが消えて復旧できないと地味に面倒なことになるので自分は安心感がある手順でやった。

事故ってしまった場合はサポートに泣きつくと何かしらの方法で何とかしてくれるっぽいコメントは海外のスレで見た。

システムディスクのコピー

やることは基本どのOSでも同じだと思うが、UGOSでやったときの手順。 まず lsblk でシステムディスクのパスを確認する。

  • nvme0n1: 自分が挿したSSD
  • nvme1n1: UGOSが入っているSSD
$ lsblk
NAME                                     MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
zram0                                    252:0    0   963M  0 disk  [SWAP]
zram1                                    252:1    0   963M  0 disk  [SWAP]
zram2                                    252:2    0   963M  0 disk  [SWAP]
zram3                                    252:3    0   963M  0 disk  [SWAP]
nvme0n1                                  259:0    0 931.5G  0 disk
├─nvme0n1p1                              259:12   0  15.3G  0 part
└─nvme0n1p2                              259:13   0 916.3G  0 part
  └─md1                                    9:1    0 916.1G  0 raid1
    └─ug_55F4C6_1743584188_pool1-volume1 253:0    0 916.1G  0 lvm   /home
                                                                    /volume1
nvme1n1                                  259:3    0 119.2G  0 disk
├─nvme1n1p1                              259:4    0   256M  0 part  /boot
├─nvme1n1p2                              259:5    0     2G  0 part
├─nvme1n1p3                              259:6    0    10M  0 part  /mnt/factory
├─nvme1n1p4                              259:7    0     2G  0 part  /rom
├─nvme1n1p5                              259:8    0     2G  0 part  [SWAP]
├─nvme1n1p6                              259:9    0     4G  0 part  /ugreen
├─nvme1n1p7                              259:10   0   109G  0 part  /overlay
└─nvme1n1p128                            259:11   0   239K  0 part

パスがわかったらddコマンドでコピーを取る。 gzipで圧縮しながら取ったら大体3.4GB程度のサイズになった。

$ sudo dd if=/dev/nvme1n1 bs=64M status=progress conv=sync | gzip > /home/XXX/ugos.img.gz
[sudo] password for XXX:
68585259008 bytes (69 GB, 64 GiB) copied, 504 s, 136 MB/s

システムディスクの復元

作ったバックアップを元に書き込むだけ。 システム用のSSDに対して書き込むのでシステム用のSSD以外で作業環境をブートさせておく必要がある。

gunzip -c ugos.img.gz > ugos.img
dd if=ugos.img of=/dev/nvme1 bs=64M status=progress conv=sync,noerror

この手順で自分は復元してUGOSが起動できるところまで確認できているが、くれぐれも自己責任で。

LEDの調整

UGOS以外のOSではフロントパネルのLEDを制御してくれないので常にロード中っぽい光り方をする。 いい感じの光らせ方にするために GitHub - miskcoo/ugreen_leds_controller: An LED Controller for UGREEN's DX/DXP NAS Series を使わせてもらった。

Proxmox上でdebファイルからインストールしようとしたところ、linux-headersを入れろと言われるものの linux-headers-pve-6.8.12-9-pve パッケージがなくて困っていた。 パッケージを適当に眺めていたら proxmox-headers-6.8.12-9-pve を見つけて入れたらheaders周りは解決した。

またrootディレクトリでインストールしようとすると

N: Download is performed unsandboxed as root as file '/root/led-ugreen-dkms_0.3_amd64.deb' couldn't be accessed by user '_apt'. - pkgAcquire::Run (13: Permission denied)

というエラーが出たので chmod 644 しつつdebファイルを /tmp ディレクトリに移動させてインストールしてうまく動作した。

ざっくりまとめると以下のような感じ。

su -
apt install -y proxmox-headers-6.8.12-9-pve
wget https://github.com/miskcoo/ugreen_leds_controller/releases/download/v0.3/led-ugreen-dkms_0.3_amd64.deb
wget https://github.com/miskcoo/ugreen_leds_controller/releases/download/v0.3/led-ugreen-utils_0.3_amd64.deb
chmod 644 *.deb
mv *.deb /tmp
apt install -y /tmp/led-ugreen-dkms_0.3_amd64.deb
apt install -y /tmp/led-ugreen-utils_0.3_amd64.deb

PVEじゃなくてもdebianベースなら頑張れば大体取り込めそうな気はした。

LEDの色は任意の色に調整可能だが、再起動するとデフォルトの色に戻ってしまうので手が空いたら永続化の方法を調べたい。

検証

測ったときの構成は以下。

  • 付属品SSD 128GB x1
  • SN850X 4TB x2
  • Seagate IronWolf Pro 16TB x2
  • Crucial CT32G56C46S5 x2

OSごとの速度計測

UGOSはランダムI/Oがやや遅かった。PVE上のVMで動かすのも遅くなるが、大体それと同じぐらいの遅さなのでVMとして動いていたりするんだろうか。 sambaの設定はデフォルト設定で同じSSDを使って計測したので大体環境は同じはず。 ネイティブDebianが頭一つ早いが、たまたま早かっただけで平均的にはUbuntuと同じぐらいかなーとは思っている。 元々そこそこのスペックのマシンでPVEを立ち上げてNASを運用していたが、そちらと比べるとCPU性能が律速要因になっている印象を持った。

消費電力

prometheusの集計などもあるので真にアイドルな時間というものはないが、あんまり人がアクセスしない時間帯でも37w程度で思ったよりは電力を食っていた。 UGOSだと何らかの最適化が走ってるかもしれないが、海外のスレ見てもアイドル時で30w前半くらいっぽいのてそんなに差もなさそうな感触。 またLEDの調整を行うと定期的にディスクの状態を確認しに行くからか消費電力が平均的に上がる傾向があった。

13時過ぎぐらいからLEDの調整をやめてみた図

CrystalDiskMarkを走らせたときの消費電力は大体ピークで69W程度だった。

13世代のi5でNASとして動かしてた時も35-50w以内で動いてた気はするので正直この消費電力は期待外れだったが、これ以上スペックが下がると10G転送がかなりきつそうなのでこの辺が10G NASとしては妥当なのかもしれない。

CPU性能

CrystalDiskMarkを走らせたときで以下のような感じ。

速攻でCPU温度が100度付近まで上がっているので冷却性能はあんまり良くないのかもしれない。 ただすぐに70度とかまで下がったりしているので本当か?という気持ちにはなる。

所感

速攻OS入れ替えているので説得力が低いが、UGOSはUI格好いいし、システムモニターもついてるし、使いやすそうでいいなーとは思った。 ただどこまでメンテし続けてくれるのかわからないのと、OS入れ替えるにしてもLinuxでSMBやNFS入れるのがシンプルザ・ベストかなということで入れ替えた。

ハードウェア面としては冷却性能や消費電力はやや期待外れではあったものの、10G NICでM.2 x3 + SATA x4な上に対応ポートが多彩で6万にしてはコスパ良いと思う。

アクセス元に応じてCloudflare Tunnel or Directな経路を通す

背景

自鯖Webアプリに出先からアクセスするためにCloudflare Tunnel(プロキシ) + mTLSの構成を取っていた。 LAN内にいるときはプライベートIPなどを直接指定してもアクセスできるのだが、URLを都度変えるのが面倒なのでCloudflare Tunnel経由でアクセスしていた。

ただCloudflare Tunnel経由だといくつかネガティブな点があるためLAN内では直接アクセスしたくなった。

主なモチベーションは以下の2点。

  • Cloudflare Tunnel + mTLSは証明書の設定がされていても確率的にエラーになる問題に悩まされていたので、せめてLAN内ではストレスから解放されたい*1*2
  • インターネット(Cloudflare Tunnel)を経由すると写真の同期が遅くなったり、最大100MBまでしか通信できないなどの制約があるのを解消したい*3

最終的に目指しているのは同じURLにアクセスしても、アクセス元に応じて適切な経路が透過的に選択される仕組み。

  • LAN外からのアクセスの場合:Cloudflare TunnelとmTLSを経由して自鯖Webアプリへ接続
  • LAN内からのアクセスの場合:直接自鯖Webアプリへ接続

構成

元々のCloudflare Tunnelの設定は以下のような感じ。

ingress:
  - hostname: app1.example.com
    service: http://example1
  - hostname: app2.example.com
    service: http://example2

外からアクセスする際は https://app1.example.com でアクセスするため、LAN内でも同様にアクセスするためにSSL証明書を取る必要がある。 オレオレ証明書を作って各端末に配布するのは面倒だし、LE証明書の更新を自前構成で管理するのも嫌だったのでcaddyを使ってみることにした。

PVEでcaddy用のdebian LXCを作り、その中でdocker composeで起動させ、コンテナの管理はsystemdに任せる。 cloudflareでDNS-01 Challengeを行うので対応したイメージを利用。

services:
  app:
    image: ghcr.io/caddybuilds/caddy-cloudflare:2.9.1
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    ports:
      - 80:80
      - 443:443
      - 443:443/udp
    volumes:
      - /etc/caddy/conf:/etc/caddy
      - /etc/caddy/site:/srv
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - CLOUDFLARE_API_TOKEN=XXX

volumes:
  caddy_data:
  caddy_config:

Caddyfile

{
  acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}

app1.example.com {
  reverse_proxy http://example1
}

app2.example.com {
  reverse_proxy http://example2
}

LAN内のDNSについてはOpenWrtで設定した。LAN内にDNSサーバーがあれば何でもよい。 普段はNextDNSを使っているため、家のWi-Fiの接続設定->DNS->手動設定で今回の用意したサーバーを使うようにした。

config domain
        option name 'app1.example.com'
        option ip 'caddyを動かしているプライベートIP'

config domain
        option name 'app2.example.com'
        option ip 'caddyを動かしているプライベートIP'

Cloudflareのトークン指定とcaddyの設定がうまく動作していればcaddy起動時に証明書を取得してくれる。

ERR_SSL_PROTOCOL_ERROR の解消

Chromeでアクセスすると確率的に ERR_SSL_PROTOCOL_ERROR が表示され、アクセスできない問題に遭遇した。 この問題は2025/3時点でChrome系のブラウザだけで確認しており、FirefoxやSafariなどでは起きないという特徴もあった。

調べたところ、Cloudflare(プロキシ)ではデフォルトでTLSv1.3が使われており、かつECHの利用もデフォルトで有効化されているためだった。 あんまり調べていないがECHに対応しているのが現時点ではChromeだけなのでFirefoxやSafariで問題が起きなかったのだと思う。 ECHの影響であるという切り分けにはChromeのNetLogを利用した。

ECHを無効化するにはTLSv1.2を使うか、(フリープランなので)APIを叩いてECHを無効化するかの2択だったが、TLSv1.2を使い続けても時間の問題になりそうなのでcliでECHを無効化することにした。

# 実行前
$ > curl https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ech \
  -H "X-Auth-Email: $EMAIL" \
  -H "X-Auth-Key: $CLOUD_FLARE_GLOBAL_API_KEY" \
{"result":{"id":"ech","value":"on","modified_on":null,"editable":true},"success":true,"errors":[],"messages":[]}

# ECH無効化
$ > curl -XPATCH https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ech \
  -H "X-Auth-Email: $EMAIL" \
  -H "X-Auth-Key: $CLOUD_FLARE_GLOBAL_API_KEY" \
  -H "Content-Type: application/json" --data '{"id":"ech","value":"off"}'

# 実行後
$ > curl https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ech \
  -H "X-Auth-Email: $EMAIL" \
  -H "X-Auth-Key: $CLOUD_FLARE_GLOBAL_API_KEY" \
{"result":{"id":"ech","value":"off","modified_on":null,"editable":true},"success":true,"errors":[],"messages":[]}

しばらくするとECHが返ってこなくなっていることを確認。

$ > curl "https://dns.google/resolve?name=your.domain.com&type=HTTPS"
{"Status":0,...,ech=XXX ipv6hint=...}

$ > curl "https://dns.google/resolve?name=your.domain.com&type=HTTPS"
{"Status":0,..., ipv6hint=...}

また、caddyでもちょうど先週ECH対応が入り2.10系からサポートする*4と見かけたが恐らく本件の問題に直接関係はないので2.10系に乗り換えても問題は解決しないと思う。

参考

*1:iPhoneの場合、プライベートウィンドウで開くことで高確率でこの問題を回避できる

*2:AndroidのChromeやPCでは比較的起きにくい気がする

*3:obsidianのlivesyncサーバーをsync mode: livesyncで運用していて結構な頻度でアクセスしているのもあり、LAN内で解決できるならそうしたい

*4:https://github.com/caddyserver/caddy/issues/4221#issuecomment-2702361739