ドメインも固定IPもポート開放もなしに、https://grafana.<tailnet>.ts.net のような
名前でサービスにアクセスできるようにする。使うのは Tailscale と tsdproxy だけ。
うちのルータはポート開放に対応していない。つまり外から入ってくる経路が作れない。 Let's Encrypt の HTTP-01 チャレンジも通らないし、リバースプロキシを立てても インターネットからは届かない。
Tailscale は WireGuard の上に P2P の仮想ネットワークを作るので、この制約を丸ごと回避できる。 さらに tailnet 内のホスト名に対して Let's Encrypt の証明書を発行してくれるのが効く。 DNS-01 チャレンジを Tailscale 側が代行するので、こちらは何も公開しなくていい。
素直にやると https://<host>.ts.net:3000 のようにポート番号で区別することになるが、
それだと証明書は1枚で済む代わりにポートを覚える羽目になる。
tsdproxy は
プロキシ対象ごとに Tailscale のデバイスを作る。
つまり grafana、jellyfin がそれぞれ独立したノードとして tailnet に生え、
それぞれが自分の証明書を持つ。結果として、
https://grafana.<tailnet>.ts.net
https://jellyfin.<tailnet>.ts.net
https://prometheus.<tailnet>.ts.net
のように全部 443 でアクセスできる。ポート番号を覚える必要がなくなる。
コンテナのラベルから自動検出させる方法もあるが、host ネットワークで動いているサービス
(ラベルを持たない)も混ざっていたので、ファイルで列挙する lists プロバイダを使った。
# config/services.yaml
grafana:
ports:
443/https:
targets: [ "http://127.0.0.1:3000" ]
dashboard: { label: "Grafana" }
portainer:
ports:
443/https:
targets: [ "https://127.0.0.1:9443" ]
tlsValidate: false # 自己署名証明書なので検証を切る
このファイルは保存すると自動で読み直される。再起動は要らない。
公式ドキュメントを見ながら設定を書いたら、起動ループに入った。
field dashboard not found in type config.config
field proxyAccessLog not found in type config.LogConfig
原因は、ドキュメントが main ブランチの内容で、リリース済みのバージョンより
先を行っていたこと。タグを指定してソースの設定構造体を読みに行くのが早い。
internal/config/config.go ← リリースタグのものを見る
それ以来、イメージは :latest ではなく明示的なバージョンで固定している
(別の理由でも固定すべきだった)。
9個のサービスを設定したのに、1個だけ成功して8個が
API key ... not valid で失敗した。
Tailscale の認証キーには使い捨て(one-shot)と再利用可能(reusable)があり、 既定は使い捨て。最初のデバイスがキーを消費した時点で残りが全部弾かれる。 1サービス1デバイスの構成では必ず Reusable にする。
ついでに、生成されたデバイスは既定で90日後にキーが失効する。 サーバー用途なら管理画面で「Disable key expiry」しておかないと、 3ヶ月後に全部落ちる。
127.0.0.1 と書くと 0.0.0.0 に書き換えられる
管理用の待受アドレスを 127.0.0.1 にしたのに、
コンテナ内で 0.0.0.0 になっていた。
ソースを読んだら、Docker 内で動いている場合に
"127.0.0.1" という文字列と完全一致したら "0.0.0.0" に置換する
という処理が入っていた。コンテナ内のループバックに縛ると外から到達できないので、
親切のつもりの実装だと思う。
回避策は localhost と書くこと。文字列一致を外れるので置換されない。
http:
hostname: localhost # "127.0.0.1" と書くと 0.0.0.0 に書き換えられる
port: 8081
11サービスに HTTPS のホスト名が付いた。費用はゼロ。ポート開放もゼロ。 証明書の更新も Tailscale が勝手にやる。