Tailscale Funnelで自宅サーバーをCloudFrontのオリジンにする
ども、takiponeです。
GL.iNet Brume 2で自宅サーバーを用意したので、ブログエンジンGhostをホストしCloudFrontとTailscale Funnelを組み合わせてインターネットに公開する様子をご紹介します。
Tailscale Funnelとは
TailscaleはVPNサービスの一つで、メルカリさんの採用事例が有名ですね。
Wireguardをベースにハブアンドスポークとピアツーピアを自動で使い分けたりと、開発者目線の便利機能が数多く提供されているのが特徴です。
Tailscale Funnel(以下Funnel)はVPNノードをインターネットに公開する機能で、現在は招待制のAlpha版が提供されています。Funnelはインターネットからの接続に備えてLet's EnrcyptのTLS証明書の自動デプロイを内包したHTTPSサービスでListenし、TLS暗号を解いたHTTPリクエストとしてVPNノードの指定ポートにフォワードします。クライアント-DERP(Tailscaleの中継サーバー)間はTLS、DERP-VPNノード間はWireguardという異なる暗号化レイヤーによるセキュアなHTTP通信を提供します。ローカルマシンのサービスをインターネットに公開する仕組みは古くからNgrokが提供してきましたが、最近はVPNサービスの追加機能として例えばCloudflare Tunnelがあり、Tailscale Funnelはこれら既存サービスをフォローする格好なのでこれからどう差別化していくのかが楽しみです。
構成の概要
今回の構成では、CloudFrontのオリジンとしてFunnelのエンドポイントを指定、自宅サーバーをTailscaleのVPNノードとして以下の組み合わせでWebサーバーを構成しました。
FunnelのみでもWebサーバーをインターネットに公開することができるのですが、以下の理由からCloudFrontを挟むことにしました。
- Tailscale Funnelには転送量の上限(具体的な数字は未公開)があるので、転送量をセーブしつつ上限を超えた場合もサービスを継続したい
- カスタムドメインを利用したい(Funnelはカスタムドメイン未対応)
- 元の構成でもCloudFrontを利用していたので、オリジンの切り替えのみで対処したい
小規模な個人ブログなので、CloudFrontの料金は月間1TBまでの無料枠を見込んでいます。
自宅サーバー(Brume 2)の構成
自宅サーバー側では、Docker ComposeでブログエンジンGhostとNginxをセットで動かします。Ghostブログエンジンは元々使っていたものなので、NginxのみでHTMLなどの静的Webサイトや他のWebアプリケーションと組み合わせることもできます。Dockerのインストール手順は以下のブログを参照してください。
Docker Composeの構成ファイル docker-compose.yaml
を示します。hogehoge
のディレクトリ名とGhostのURL指定は任意のものでOKです。ネットワークモードは上記ブログ記事にあるホストモードにし、コンテナ間通信はlocalhost
宛のポート番号を想定しています。
version: '3.9'
services:
nginx:
image: "nginx"
network_mode: "host"
volumes:
- "/opt/hogehoge/nginx/:/etc/nginx/"
ghost:
image: "ghost:5"
network_mode: "host"
environment:
- NODE_ENV=development
- url=https://example.com/
- caching__frontend__maxAge=300
- database__client=sqlite3
- database__connection__filename=content/data/ghost.db
- database__useNullAsDefault=true
- database__debug=false
volumes:
- "/opt/hogehoge/ghost/content/:/var/lib/ghost/content/"
Nginxの構成は以下の通りです。80番ポートがBrume 2のWeb管理画面用途で使用済みなので、今回は2367番ポートでListenします。Nginxの動作にはあとmime.types
ファイルが必要なので、公式リポジトリなどからダウンロードし、/opt/hogehoge/nginx/
に配置しましょう。
user nginx;
worker_processes 1;
error_log /dev/stderr warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;
sendfile on;
keepalive_timeout 65;
gzip on;
server_tokens off;
server {
listen 2367;
server_name _;
location / {
proxy_pass http://localhost:2368;
}
proxy_redirect off;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffers 32 4k;
}
}
Docker Composeでコンテナを起動します。
# cd /opt/hogehoge
# docker-compose up -d
Building with native build. Learn about native build in Compose here: https://docs.docker.com/go/compose-native-build/
Starting hogehoge_ghost_1 ... done
Starting hogehoge_nginx_1 ... done
# docker-compose ps
Name Command State Ports
---------------------------------------------------------------------
hogehoge_ghost_1 docker-entrypoint.sh node ... Up
hogehoge_nginx_1 /docker-entrypoint.sh ngin ... Up
#
これでOKです。2367番ポートでGhostブログエンジンをホストできました。
Tailscale Funnelの設定
続いてTailscale Funnel(以下Funnel)を有効化します。Funnelの利用には招待が必要ですが、↓のブログの末尾にあるThe first batch of users can join the alpha by following this link.
のリンクを踏めば招待操作と同等に有効化されます。
まずはドキュメントに従い、ノードでのFunnel設定を許可するACLを設定します。Tailscale管理画面のAcccess ControlページのポリシーJSONのGroups
とnodeAttr
に追加します。
"Groups": [
:
"group:funnel": ["<Tailscaleアカウントのメールアドレス>"],
],
:
"nodeAttrs": [
{
"target": ["group:funnel"],
"attr": ["funnel"],
},
],
groups
要素でfunnelグループを定義し、funnelグループのノードでFunnel有効化を許可する、というACLになります。
Tailscaleノードの更新
管理画面でFunnelを許可したので、Tailscaleノードの設定を進めていきましょう。Brume 2にプリインストールされるOpenWRT 21.02ではTailscaleのバージョンが古く、Funnelを利用できません。今回はTailscaleのバイナリファイルをパッケージのものとすげ替えて利用します。パッケージによる更新を行うとファイルの整合性がとれなくなるため、パッケージマネージャによる更新をあらかじめ抑制しておきます(Tailscaleのバージョンアップは適宜手作業で頑張りましょう)。
# opkg flag hold tailscale tailscaled
Setting flags for package tailscale to hold.
Setting flags for package tailscaled to hold.
Tailscaleのリポジトリからバイナリをダウンロードして上書きし、サービスを再起動します。
# wget https://pkgs.tailscale.com/stable/tailscale_1.34.1_arm64.tgz
Downloading 'https://pkgs.tailscale.com/stable/tailscale_1.34.1_arm64.tgz'
Connecting to 167.172.11.40:443
Writing to 'tailscale_1.34.1_arm64.tgz'
tailscale_1.34.1_arm 100% |*******************************| 19889k 0:00:00 ETA
Download completed (20366993 bytes)
# tar zxf tailscale_1.34.1_arm64.tgz
# ls
tailscale_1.34.1_arm64 tailscale_1.34.1_arm64.tgz
# mv tailscale_1.34.1_arm64/tailscale* /usr/sbin
# service tailscale restart
logtail started
Program starting: v1.34.1-t328b49c4d-g921b59a2e, Go 1.19.2-ts3fd24dee31: []string{"/usr/sbin/tailscaled", "--cleanup"}
LogID: e4b1b040651ec6438eb3921d91bf430a3b2ecfa5c573f0cf60ed644cc5cbc4a8
logpolicy: using system state directory "/var/lib/tailscale"
dns: [rc=unknown ret=direct]
dns: using "direct" mode
dns: using *dns.directManager
flushing log.
logger closing down
#
続いてノードをTailscaleに登録します。表示されたURLにWebブラウザからアクセス、TailscaleのアカウントでログインすればTailscaleへの接続は完了です。
# tailscale up
To authenticate, visit:
https://login.tailscale.com/a/xxxxxxxxxxxx
# (Webブラウザからアクセスし、アカウント認証する)
Success.
#
いよいよFunnelを設定します。Funnelの設定はtailscale
コマンドで実行します。
- 2024/11/06更新: v1.52.1でfunnel/serveサブコマンドが変更されたのを反映
- 2023/03/21更新: v1.38.1でserveサブコマンドが変更されたのを反映
以下の形式で設定します。
tailscale funnel --bg --tls-terminated-tcp <エクスポートするポート番号> <転送先ポート番号>
FunnelにはHTTPSとTCPSの2つの転送モードがあります。今回はアクセス元のIPアドレスを取得したかったので、FunnelでHTTP処理を行わずにTLSターミネーションのみ行う、TCPS転送モードを指定する --tls-terminated-tcp
オプションを指定して443番ポートで待ち受ける形にしました。転送先ポート番号はNginxコンテナがListenしている2367番です。
# tailscale funnel --bg --tls-terminated-tcp 443 2367
これでOKです。
(参考)v1.52.0より前の古い設定コマンド
いよいよFunnelを設定します。Funnelの設定はtailscale
コマンドで2段階で実行します。まずはサービスを以下の形式で登録します。
tailscale serve https <公開するパス> http://127.0.0.1:<転送先ポート番号>
今回は特にパスを絞る必要は無いのでルート(/
)、ポート番号は上述のNginxのListenしている2367番です。以下のコマンドを実行します。
# tailscale serve https / http://127.0.0.1:2367
ちなみにこのプロキシ設定についてドキュメントでの説明があまり見当たらず、tailscale serve --help
のヘルプが比較的詳しかったです。例えばバックエンドのWebサーバーなしでTrailscaleのみで固定のレスポンスを返す以下の設定はテスト用に便利そうです。
tailscale serve https / text:"Hello, world!"
上記を設定すると、他のTailscaleノードから443番へのHTTPSアクセスがサービスにプロキシされるようになります。インターネットからのアクセスを有効化するために以下を実行します。
# tailscale funnel 443 on
(古い設定コマンドはここまで)
これでOKです。Tailscale管理画面には以下のようにFunnel
のバッヂ表示が付きます。
アクセスするエンドポイントは、Tailscaleノードに振られる以下のFQDNです。
<ホスト名>.tailnet-XXXX.ts.net
こちらにWebブラウザでアクセスしてみると...
HTTPSでアクセスできました!
CloudFrontの構成
ここからは、CDNのCloudFrontのオリジンにTailscale Funnelエンドポイントを構成します。
AWS管理コンソールのCloudFrontディストリビューション作成画面のオリジンドメインに、さきほどのTailscale Funnelのエンドポイントを入力します。
特別な設定はあまりないのですのが、Tailscale FunnelがSNIで動作しているので、オリジンリクエストでHostヘッダを転送しないように注意します。具体的には、オリジンリクエストポリシーを未指定にするか、ポリシーのオリジンリクエストヘッダーにHost
を含めないようにします。
また、Ghostの管理画面(/ghost/*
)は一般的なWeb管理画面と同様Cookieやヘッダーの転送が求められるので、キャッシュをオフにしそれらの転送を有効にするビヘイビアを設定しましょう。
Apppendix. 直接アクセスの抑制
CloudFrontを経由しない、インターネットからTailscale Funnelエンドポイントへの直接アクセスを抑制したい場合もあるでしょう。Nginxに設定を追加してみました。
:
http {
:
server {
:
set_real_ip_from 127.0.0.0/8;
set_real_ip_from ::1/128;
set_real_ip_from fd7a:115c:a1e0:ab12:4843:xxxx:xxxx:xxxx; # DERPのIPv6アドレス
set_real_ip_from 100.xx.xx.xx/32; # DERPのIPv4アドレス
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set $forbidden_flag 1;
if ($http_user_agent ~* 'Amazon CloudFront') {
set $forbidden_flag 0;
}
if ($remote_addr ~ "^100") {
set $forbidden_flag 0;
}
if ($forbidden_flag) {
return 403;
}
}
}
CloudFrontでUser-Agentヘッダの転送をオフにすると、CloudFrontからのオリジンアクセスのUser-AgentにはAmazon CloudFront
がセットされます。それとTailscaleの他のノード(IPv4の100.xx.xx.xx
)からのアクセスを許可、Tailscale Funnel経由の他のアクセス(手元の環境ではすべてIPv6 ULA経由だった)を拒否する形にしてみました。Tailscale周りのv4/v6の使い分けの様子が変わる可能性がありそうなので、ときおりログを見ながら設定を調整していきたいと考えています。
Appendix. Tailscale Funnelの検証メモ
つらつら書いているので、興味があれば覗いてみてくださいませ。
まとめ
インターネットに自宅サーバーを公開する手段としてTailscale Funnelを利用し、CloudFrontと組み合わせる構成をご紹介しました。この構成は、私の個人ブログで今日も元気に稼働しています。
手元にBrume 2が2台あるので、冗長構成を組んだ例を以下の記事で紹介しています。
Discussion