Tailscale HTTPSのTLS証明書をTraefikで動的に取得する

2023.01.31

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

ども、大瀧です。 Tailscale HTTPSはTailscaleのVPNノード間通信のためにLet's EncryptによるTLS証明書を提供するサービスです。これまではTailscale CLIとCaddyが証明書取得に対応していましたが、OSSのリバースプロキシTraefikでも利用できるようになっていたので試してみた様子を紹介します。

動作確認環境

  • OS: Amazon Linux 2
  • アーキテクチャ: Arm64
  • Tailscale: v1.36.0
  • Traefik: v3.0.0-beta2

Tailscale HTTPSとTraefikバージョン3はいずれもベータリリースの段階です。本番ワークロードには利用できないことに注意してください。手順は以下のブログ記事が詳しいです。

手順1. インストール

まずはTailscaleとTraefikをそれぞれインストールします(Traefikはワンバイナリなので、ダウンロードするだけです)。

$ curl -fsSL https://tailscale.com/install.sh | sh
$ sudo tailscale up
# 表示されるURLにブラウザからアクセスし、Tailscaleのアカウントで認証します
$ wget https://github.com/traefik/traefik/releases/download/v3.0.0-beta2/traefik_v3.0.0-beta2_linux_arm64.tar.gz
$ tar zxf traefik_v3.0.0-beta2_linux_arm64.tar.gz
$ ls traefik
traefik
$

これでOKです。

手順2. 構成

Tailscale HTTPSはWeb管理画面の[DNS]で有効になっていることを確認します。

続いてTraefikです。今回はお試しなのでバイナリと同じカレントディレクトリにtraefik.tomlファイルとdynamic.tomlファイルを作成しました。

traefik.toml

[entryPoints]
	[entryPoints.websecure]
		address = ":443"

[providers]
	[providers.file]
		filename = "dynamic.toml"

[certificatesResolvers.myresolver.tailscale]

[api]
	debug = true

[log]
	level = "DEBUG"

TraefikにはLet's EncryptのTLS証明書を取得するCertificate Resolversという仕組みがあり、ここにTailscale HTTPS(tailscale)をセットします。9行目の部分です。

dynamic.toml

[http]
	[http.routers]
		[http.routers.towhoami]
			service = "whoami"
			rule = "Host(`ip-XX-XX-XX-XX.tailnet-XXXX.ts.net`)"
			[http.routers.towhoami.tls]
				certResolver = "myresolver"

	[http.services]
		[http.services.whoami]
			[http.services.whoami.loadBalancer]
				[[http.services.whoami.loadBalancer.servers]]
					# docker run -d -p 6060:80 traefik/whoami
					url = "http://localhost:6060"

2〜7行目のルーター設定には、TailscaleノードのDNS名ip-XX-XX-XX-XX.tailnet-XXXX.ts.netへのアクセスに対して、先ほどのCertificate Resolversをセットしています。

9〜14行目のターゲット設定では`http://localhost:6060`にリクエストを転送するので、レスポンスを返すためのDockerコンテナを実行しておきます。

$ sudo amazon-linux-extras install docker
$ sudo service docker start
$ sudo docker run -d -p 6060:80 traefik/whoami
$ curl localhost:6060
Hostname: ebdbc05b441b
IP: 127.0.0.1
IP: 172.17.0.2
RemoteAddr: 172.17.0.1:48740
GET / HTTP/1.1
Host: localhost:6060
User-Agent: curl/7.79.1
Accept: */*
$

これでOKです。

動作確認

ではTraefikを起動します。

$ sudo ./traefik
2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/cmd/traefik/traefik.go:100 > Traefik version 3.0.0-beta2 built on 2022-12-07T16:32:34Z version=3.0.0-beta2
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/cmd/traefik/traefik.go:107 > Static configuration loaded  staticConfiguration={"api":{"dashboard":true,"debug":true},"certificatesResolvers":{"myresolver":{"tailscale":{}}},"entryPoints":{"websecure":{"address":":443","forwardedHeaders":{},"http":{},"http2":{"maxConcurrentStreams":250},"transport":{"lifeCycle":{"graceTimeOut":"10s"},"respondingTimeouts":{"idleTimeout":"3m0s"}},"udp":{"timeout":"3s"}}},"global":{"checkNewVersion":true},"log":{"format":"common","level":"DEBUG"},"providers":{"file":{"filename":"dynamic.toml","watch":true},"providersThrottleDuration":"2s"},"serversTransport":{"maxIdleConnsPerHost":200}}
2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/cmd/traefik/traefik.go:685 >
Stats collection is disabled.
Help us improve Traefik by turning this feature on :)
More details on: https://doc.traefik.io/traefik/contributing/data-collection/

2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:72 > Starting provider aggregator aggregator.ProviderAggregator
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/server_entrypoint_tcp.go:188 > Starting TCP Server entryPointName=websecure
2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *file.Provider
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *file.Provider provider configuration config={"filename":"dynamic.toml","watch":true}
2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *traefik.Provider
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *traefik.Provider provider configuration config={}
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:217 > Configuration received config={"http":{"serversTransports":{"default":{"maxIdleConnsPerHost":200}},"services":{"api":{},"dashboard":{},"noop":{}}},"tcp":{},"tls":{},"udp":{}} providerName=internal
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:217 > Configuration received config={"http":{"routers":{"towhoami":{"rule":"Host(`ip-XX-XX-XX-XX.tailnet-XXXX.ts.net`)","service":"whoami","tls":{"certResolver":"myresolver"}}},"services":{"whoami":{"loadBalancer":{"passHostHeader":true,"responseForwarding":{"flushInterval":"100ms"},"servers":[{"url":"http://localhost:6060"}]}}}},"tcp":{},"tls":{},"udp":{}} providerName=file
2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *acme.ChallengeTLSALPN
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *acme.ChallengeTLSALPN provider configuration config={}
2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *tailscale.Provider
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *tailscale.Provider provider configuration config={"ResolverName":"myresolver"}
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/tls/tlsmanager.go:294 > No default certificate, fallback to the internal generated certificate tlsStoreName=default
2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/aggregator.go:47 > No entryPoint defined for this router, using the default one(s) instead entryPointName=["websecure"] routerName=towhoami
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/tls/tlsmanager.go:294 > No default certificate, fallback to the internal generated certificate tlsStoreName=default
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:256 > Creating load-balancer entryPointName=websecure routerName=towhoami@file serviceName=whoami@file
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:298 > Creating server entryPointName=websecure routerName=towhoami@file serverName=6d3e5017708777ea serviceName=whoami@file target=http://localhost:6060
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/tracing/forwarder.go:26 > Added outgoing tracing middleware entryPointName=websecure middlewareName=tracing middlewareType=TracingForwarder routerName=towhoami@file serviceName=whoami
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/recovery/recovery.go:22 > Creating middleware entryPointName=websecure middlewareName=traefik-internal-recovery middlewareType=Recovery
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/router/tcp/manager.go:235 > Adding route for ip-172-31-23-250.tailnet-96f8.ts.net with TLS options default entryPointName=websecure
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/provider/tailscale/provider.go:253 > Fetched certificate for domain "ip-172-31-23-250.tailnet-96f8.ts.net" providerName=myresolver.tailscale
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:217 > Configuration received config={"http":{},"tcp":{},"tls":{},"udp":{}} providerName=myresolver.tailscale
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/aggregator.go:47 > No entryPoint defined for this router, using the default one(s) instead entryPointName=["websecure"] routerName=towhoami
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/tls/certificate.go:158 > Adding certificate for domain(s) ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/tls/tlsmanager.go:294 > No default certificate, fallback to the internal generated certificate tlsStoreName=default
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:256 > Creating load-balancer entryPointName=websecure routerName=towhoami@file serviceName=whoami@file
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:298 > Creating server entryPointName=websecure routerName=towhoami@file serverName=6d3e5017708777ea serviceName=whoami@file target=http://localhost:6060
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/tracing/forwarder.go:26 > Added outgoing tracing middleware entryPointName=websecure middlewareName=tracing middlewareType=TracingForwarder routerName=towhoami@file serviceName=whoami
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/recovery/recovery.go:22 > Creating middleware entryPointName=websecure middlewareName=traefik-internal-recovery middlewareType=Recovery
2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/router/tcp/manager.go:235 > Adding route for ip-XX-XX-XX-XX.tailnet-XXXX.ts.net with TLS options default entryPointName=websecure

起動ログの中にTailscale HTTPSのTLS証明書のメッセージが見えますね。Tailscaleをインストールした手元のMacからアクセスしてみます。

% curl -v https://ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
*   Trying 100.89.238.14:443...
* Connected to ip-XX-XX-XX-XX.tailnet-XXXX.ts.net (100.89.238.14) 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: Jan 29 12:41:45 2023 GMT
*  expire date: Apr 29 12:41:44 2023 GMT
*  subjectAltName: host "ip-XX-XX-XX-XX.tailnet-XXXX.ts.net" matched cert's "ip-XX-XX-XX-XX.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: GET]
* 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 0x13a815200)
> GET / 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
< content-type: text/plain; charset=utf-8
< date: Sun, 29 Jan 2023 13:43:59 GMT
< content-length: 439
<
Hostname: ebdbc05b441b
IP: 127.0.0.1
IP: 172.17.0.2
RemoteAddr: 172.17.0.1:55100
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-Host: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: ip-XX-XX-XX-XX.ap-northeast-1.compute.internal
X-Real-Ip: 100.113.200.88

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

アクセス出来ました!TLSネゴシエーションのログからTailscaleノードのDNS名に対応するTLS証明書を受け取っているのが読み取れますね。

まとめ

Tailscale HTTPSのTLS証明書をTraefikから動的に取得する構成をご紹介しました。TraefikはKubernetes IngressコントローラDockerの連携が充実しているので、VPN越しの遠隔の開発環境のコンテナなどでHTTPSをサポートする手段として便利に使えそうですね。

TraefikにはLet's EncryptのTLS証明書を動的に取得する仕組みが元々含まれていますが、ACME対応のためにインターネット向け構成の手間がかかりがちなところをTailscaleに外出しできるのは地味に便利だと思っています。Cloudflare Tunnelでも、プライベートネットワーク向けにCloudflare SSL/TLSの証明書を提供してくれる仕組みがあると良いかもしれませんね。

参考URL