📘

Tailscale Funnelで自宅サーバーをCloudFrontのオリジンにする

2022/12/30に公開

ども、takiponeです。
GL.iNet Brume 2で自宅サーバーを用意したので、ブログエンジンGhostをホストしCloudFrontとTailscale Funnelを組み合わせてインターネットに公開する様子をご紹介します。

Tailscale Funnelとは

TailscaleはVPNサービスの一つで、メルカリさんの採用事例が有名ですね。
Wireguardをベースにハブアンドスポークとピアツーピアを自動で使い分けたりと、開発者目線の便利機能が数多く提供されているのが特徴です。

https://tailscale.com/

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のインストール手順は以下のブログを参照してください。

https://takipone.com/use-brume2-as-a-personal-server/

Docker Composeの構成ファイル docker-compose.yaml を示します。hogehogeのディレクトリ名とGhostのURL指定は任意のものでOKです。ネットワークモードは上記ブログ記事にあるホストモードにし、コンテナ間通信はlocalhost宛のポート番号を想定しています。

/opt/hogehoge/docker-compose.yaml
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/に配置しましょう。

/opt/hogehoge/nginx/nginx.conf
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.のリンクを踏めば招待操作と同等に有効化されます。

https://tailscale.com/blog/introducing-tailscale-funnel/

まずはドキュメントに従い、ノードでのFunnel設定を許可するACLを設定します。Tailscale管理画面のAcccess ControlページのポリシーJSONのGroupsnodeAttrに追加します。

  "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コマンドで実行します。

以下の形式で設定します。

Tailscale Funnelのコマンド形式
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の検証メモ

つらつら書いているので、興味があれば覗いてみてくださいませ。

https://zenn.dev/takipone/scraps/fca57cc9bece69

まとめ

インターネットに自宅サーバーを公開する手段としてTailscale Funnelを利用し、CloudFrontと組み合わせる構成をご紹介しました。この構成は、私の個人ブログで今日も元気に稼働しています。

https://takipone.com/

手元にBrume 2が2台あるので、冗長構成を組んだ例を以下の記事で紹介しています。

https://zenn.dev/takipone/articles/d43dd1c0bcf416

Discussion