Tailscale 組み込みのリバースプロキシでVPN向けWebアプリをHTTPS対応する

TailscaleエージェントのWebサーバー機能はドキュメントルートを指定するほかシンプルな固定テキストとリバースプロキシを指定できます。本ブログではリバースプロキシを用いてTailscaleのVPN経由でWebアプリケーションをHTTPSでホストする様子をご紹介します。
2023.03.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

2023/03/25更新 Tailscaleエージェントv1.38.1でserve、funnelサブコマンドが変更されたのを反映

ども、大瀧です。

TailscaleのVPNエージェントには、様々な組み込み機能があります。本ブログではそのうちのひとつであるリバースプロキシ機能を利用し、TailscaleのVPN経由でWebアプリケーションをホストする様子をご紹介します。

TailscaleエージェントのWebサーバー機能

TailscaleエージェントのWebサーバー機能は、一般的なWebサーバーと同様にドキュメントルートを指定するほかシンプルな固定テキストとリバースプロキシを指定できます。詳しくは以下のドキュメントや設定コマンドのヘルプ tailscale serve --help を参照してください。

TailscaleデバイスでWebアプリケーションがポートをListenし他のTailscaleデバイスからの接続を受け付けることももちろん出来るのですが、リバースプロキシを経由することでTLS終端を手軽に利用できるメリットがあります。Tailscale HTTPSというTailscaleデバイスに割り当てられるVPN内のFQDN [デバイス名].tailnet-XXXX.ts.net のTLS証明書を提供する機能があり、リバースプロキシ経由の通信に自動で適用されます。

設定と動作確認

設定方法は非常にシンプルです。TailscaleのWeb管理画面の[DNS]でTailscale HTTPSが有効になっていることを確認します。

設定コマンドでは、以下の形式で設定します(バージョンv1.38.1で変わったので併記します)。

tailscale serve https [パス] http://127.0.0.1:[ポート番号]

v1.38.1より前

tailscale serve [パス] proxy [ポート番号]

今回は以下の環境で動作確認しました。

  • プラットフォーム: Amazon EC2 東京リージョン
  • OS: Amazon Linux 2
  • アーキテクチャ: Arm64
  • Tailscaleバージョン: 1.36.2

まずは、プロキシ先のWebアプリケーションとしてこちらの記事のDockerコンテナを使い回します。

$ sudo amazon-linux-extras install docker
$ sudo service docker start
Redirecting to /bin/systemctl start docker.service
$ sudo docker run -d -p 6060:80 traefik/whoami
Unable to find image 'traefik/whoami:latest' locally
latest: Pulling from traefik/whoami
029cd1bf7e7c: Pull complete
e73b694ead4f: Pull complete
cd132d421fcc: Pull complete
Digest: sha256:24829edb0dbaea072dabd7d902769168542403a8c78a6f743676af431166d7f0
Status: Downloaded newer image for traefik/whoami:latest
fdac84837295ae8d7543be33370f9bd05db68cdd4bc3257a6d230e8978f65af6
$

6060番ポートのルートパス(/)に対してレスポンスとしてリクエスト情報を返す、シンプルなWebアプリケーションです。

$ curl localhost:6060
Hostname: fdac84837295
IP: 127.0.0.1
IP: 172.17.0.2
RemoteAddr: 172.17.0.1:36454
GET / HTTP/1.1
Host: localhost:6060
User-Agent: curl/7.88.1
Accept: */*

$

では、この6060番ポートにリバースプロキシを設定します。

$ sudo tailscale serve https / http://127.0.0.1:6060
$ tailscale serve status
https://ip-XX-XX-XX-XX.tailnet-XXXX.ts.net (tailnet only)
|-- / proxy http://127.0.0.1:6060

$

これでOKです。手元のTailscaleデバイスからTailscaleのFQDNにHTTPSでアクセスしてみると...

% curl https://ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
Hostname: fdac84837295
IP: 127.0.0.1
IP: 172.17.0.2
RemoteAddr: 172.17.0.1:50648
GET / HTTP/1.1
Host: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
User-Agent: curl/7.86.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 100.113.200.88

%

レスポンスが返ってきました! X-Forwarded-Forヘッダから、Tailscale VPN(100.64.0.0/10のShared Address Space)経由ということがわかりますね。

ちなみにcURLのオプションを調整してTLS証明書の様子を確認してみると...

% curl -v -I https://ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
*   Trying 100.108.243.21:443...
* Connected to ip-XX-XX-XX-XX.tailnet-XXXX.ts.net (100.108.243.21) port 443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
*  start date: Mar 11 21:32:30 2023 GMT
*  expire date: Jun  9 21:32:29 2023 GMT
*  subjectAltName: host "ip-XX-XX-XX-XX.tailnet-XXXX.ts.net" matched cert's "ip-172-31-47-65.tailnet-XXXX.ts.net"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: HEAD]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net]
* h2h3 [user-agent: curl/7.86.0]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x15a813200)
> HEAD / HTTP/2
> Host: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
> user-agent: curl/7.86.0
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
HTTP/2 200
< content-type: text/plain; charset=utf-8
content-type: text/plain; charset=utf-8
< date: Sat, 11 Mar 2023 22:33:45 GMT
date: Sat, 11 Mar 2023 22:33:45 GMT
< content-length: 214
content-length: 214

<
* Connection #0 to host ip-XX-XX-XX-XX.tailnet-XXXX.ts.net left intact

IssuerからLet's EncryptのTLS証明書ということがわかりますね。Let's EncryptのTLS証明書を利用するためにはACMEと呼ばれる証明書発行のための手続きをセットアップする必要がありますが、Tailscale HTTPSはその手続きをマネージドで任せられるわけですね。便利!

ちなみに、ローカルホスト以外にプロキシを設定しようとすると、非サポートということでエラーになります。

$ sudo tailscale serve https / http://172.31.1.1:6060
only localhost or 127.0.0.1 proxies are currently supported
$

Subnet Routerの代わりになったら便利かなと思っていたのですが、現時点では無理そうです。TraefikのTailscale HTTPS連携で代替しましょう。

まとめ

Taiscaleエージェントのリバースプロキシ機能を利用し、WebアプリケーションをHTTPSでホストする様子をご紹介しました。Webアプリケーションの開発環境ではオレオレ証明書を利用しがちですが、これだけ手軽にTLS証明書が利用できるのであれば開発環境でも正規のTLS証明書でデバッグなどに使えるのではないでしょうか。

参考URL