ADR-0001: homelab アプリ(bulletin)の外部公開方式
- ステータス: Accepted
- 日付: 2026-06-27
- 決定者: 自分
- タグ: homelab, security, networking, k3s
コンテキストと課題
k3s homelab 上で内部運用していた掲示板アプリ(bulletin)を、自分だけが他PCやスマホ・外出先からも使えるようにしたい。内部は hosts 運用(bulletin.shooonng.com を MetalLB の Ingress IP 192.168.3.200 に向ける)で完成済み。
- 達成したいこと: 内部運用中のアプリを、自分だけが外部からも安全に使えるようにする。
- 制約:
- ホームルーターのポート開放はしたくない(攻撃面を増やしたくない)。
- 公開ドメインは Cloudflare で管理しているゾーン(
shooonng.com)配下。 - 内部 hosts 運用は維持したい(LAN 内は Ingress 直結のまま)。
- 「自分だけ」の守り方: URL 秘匿は弱い(TLS 証明書発行で FQDN が Certificate Transparency ログに公開される)。認証層で守る必要がある。
検討した選択肢
- 選択肢A: Cloudflare Tunnel + Cloudflare Access
- 選択肢B: DNS A レコード公開 + ルーターのポートフォワード
- 選択肢C: URL 秘匿のみ(認証なしで公開)
決定
採用: 選択肢A(Cloudflare Tunnel + Cloudflare Access)
決め手:
- ルーターのポート開放が不要(cloudflared がアウトバウンド接続を張る)。
- Cloudflare Access により「許可したメールアカウントだけ」に認証で絞れる。URL 秘匿に依存しない。
- 内部 hosts 運用と両立する(外部は Tunnel 経由、内部は Ingress 直結の二系統)。
各選択肢の評価
選択肢A: Cloudflare Tunnel + Access
- 良い点: ポート開放不要 / 認証で「自分だけ」を担保 / 内部運用と両立 / グローバルIP変動に非依存。
- 悪い点: Cloudflare への依存。cloudflared をクラスタに常駐させる運用が増える。
- 採用理由: 制約(ポート開放回避・認証必須)を最もきれいに満たす。
選択肢B: DNS A レコード + ポートフォワード
- 良い点: 構成が古典的で理解しやすい。Cloudflare 依存が小さい。
- 悪い点: ルーターに直接ポートを開ける必要があり攻撃面が増える。動的グローバルIPだと DDNS 運用も必要。homelab を直接インターネットに晒すリスク。
- 却下理由: ポート開放を避けたいという制約に反する。
選択肢C: URL 秘匿のみ
- 良い点: 設定が最も簡単。
- 悪い点: FQDN は CT ログで公開されるため秘匿は成立しない。実質無防備。
- 却下理由: 「自分だけ」をセキュリティとして担保できない。
結果(Consequences)
- 良い面: スマホ(モバイル回線)から Access ログインを経てアプリに到達できることを確認。ポート開放ゼロで外部公開を実現。
- トレードオフ: Cloudflare エッジで TLS 終端するため、外部=エッジ証明書、内部=Ingress の LE 証明書、の二系統構成になった(cloudflared → Ingress は内部 HTTPS)。
- 今後の宿題:
- SealedSecrets の封印鍵バックアップを安全な場所に保管(復号の生命線)。
- 同方式を Nextcloud 公開にも横展開予定。
- 長期的には Proxmox VM のディスク拡張で Longhorn の容量に余裕を持たせる。
実装メモ・確定値
- 公開 FQDN:
bulletin.shooonng.com(Cloudflare ゾーンshooonng.com配下、proxied/CNAME) - Tunnel ID:
3f55d9da-1471-4084-82ab-6418e83c361b(名前: bulletin) - origin:
https://ingress-nginx-controller.ingress-nginx.svc:443 - cloudflared config の要点(
originRequest):httpHostHeader: bulletin.shooonng.comoriginServerName: bulletin.shooonng.com(SNI を正しく送るために必須)
- TLS: cert-manager + Let’s Encrypt、DNS-01(Cloudflare)、ClusterIssuer
letsencrypt-cloudflare - Secret 管理: SealedSecrets controller v0.38.1(kube-system)、Tunnel 認証情報は SealedSecret 化して argocd-apps に commit
- GitOps:
jacoloves/argocd-appsのapps/cloudflare/(namespace/SealedSecret/ConfigMap/Deployment/Application) - 運用順序の鉄則: Access のポリシーを先に有効化してから DNS(CNAME)を Tunnel に向ける(逆だと一時的に無認証公開される)
ハマった点 / 学び
Longhorn でボリュームが faulted・Pod が Pending
- 原因:
No available disk candidates。実空きではなく over-provisioning 枠でスケジュール判定される。Nextcloud の 20Gi×3レプリカが各ノードを圧迫していた。 - 対処:
storage-over-provisioning-percentageを 100→200 に。恒久対策は VM ディスク拡張。
- 原因:
Pod が CrashLoopBackOff(
exec format error)- 原因: Apple Silicon Mac でビルドすると arm64 になり、amd64 ノードで実行不可。
- 対処:
docker build --platform linux/amd64。DOCKER_DEFAULT_PLATFORM=linux/amd64を常設すると安全。同一タグ入れ替え時は古いイメージをctr images rmするか、タグを上げる。
Tunnel から origin に繋ぐと 502(
x509: certificate is valid for ingress.local)- 原因: cloudflared が Service 名で HTTPS 接続し SNI が合わず、ingress-nginx がフォールバックのデフォルト証明書(
ingress.local)を返したため検証失敗。 - 対処:
originServerName: bulletin.shooonng.comを指定。これで正しい SNI が送られ Ingress が LE 証明書を返し、検証も一致する。
- 原因: cloudflared が Service 名で HTTPS 接続し SNI が合わず、ingress-nginx がフォールバックのデフォルト証明書(
ConfigMap を更新しても cloudflared に反映されない
- 原因:
subPathでマウントしたファイルは ConfigMap 更新が自動反映されない(subPath 特有の挙動)。 - 対処: ConfigMap 変更後に
kubectl rollout restart。subPath を使わなければ自動反映される。
- 原因:
Access のログイン画面が出ず素通りに見えた
- 原因: ブラウザに既存の Access セッションが残っていた。
- 対処: シークレットタブで確認。実際にはポリシーは正しく機能していた。
参考リンク
- Cloudflare Tunnel / Access ドキュメント
- cert-manager DNS-01 (Cloudflare) 設定
- SealedSecrets: https://github.com/bitnami-labs/sealed-secrets
- 関連手順書: 19-k3s-deploy.md(本構築のベース)