Headscaleをセルフホストする:Tailscaleクライアント向けプライベートWireGuardメッシュVPNの構築

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

従来型VPNルーティングが大規模環境で破綻する理由

開発者のワークステーション、エッジクラスタ、マルチクラウド上のVMを相互接続する際、これまでは妥協を強いられるのが一般的でした。OpenVPNのような従来のハブ&スポーク型システムでは、すべてのパケットが中央のゲートウェイを経由します。月額20ドル程度の単一クラウドVPSに800Mbpsのトラフィックを流そうとすれば、CPU使用率は瞬時に100%に張り付き、レイテンシは15msから140msへと急増し、ファイル転送は完全に滞ってしまいます。

この状況を一変させたのがTailscaleです。軽量な協調サーバーを介してポイントツーポイントのWireGuardトンネルを確立することで、クライアントマシンはNATの障壁を越えて直接通信できるようになります。トラフィックは常に最短の物理ルートを通過します。しかし、HIPAAやSOC 2、あるいは厳格なゼロトラスト要件に縛られている組織にとって、Tailscaleがホストする協調サーバーに依存することには重大な制約が伴います。ルーティングのメタデータ、ノードの公開鍵、デバイスのマッピング情報がサードパーティのインフラ上に残ってしまうためです。

エアギャップ環境のテストベッドを運用している場合や、ネットワークのメタデータを自社の境界外に一切出さない保証が求められる場合、外部の協調サーバーを利用することはできません。Headscaleは、コントロールプレーンを自前で保持できるようにすることで、この課題を解決します。

アーキテクチャ:コントロールプレーンとデータプレーン

制御フローとデータフローの違いを理解しておくと、トラブルシューティングをスムーズに進めることができます。

  • データプレーン(WireGuard): 実際の通信トラフィックは、エンドポイント間においてUDPによるPeer-to-Peer(P2P)で流れます。これらのパケットが協調サーバーを経由することはありません。対称型CGNATの背後にあるなど、直接のNATホールパンチングに失敗した場合のみ、フォールバックとして暗号化されたDERP(Designated Encrypted Relay for Packets)リレーを介してルーティングされます。
  • コントロールプレーン(Headscale): Headscaleは、Go言語で記述されたTailscale協調サーバーの軽量なオープンソース実装です。公開鍵の配布、キャリアグレードNATプール100.64.0.0/10からの内部IPアドレスの割り当て、アクセスコントロールリスト(ACL)の配信などを担います。

HeadscaleはTailscaleネイティブのプロトコルを採用しているため、Linux、macOS、iOS、Windows、Android向けの標準クライアントパッケージがそのまま動作します。洗練されたクライアントのUI/UXを維持しながら、ネットワークトポロジの管理権限をSaaSベンダーに渡さずに済むのが大きな利点です。

実践:DockerとNginxによるHeadscaleのデプロイ

Tailscaleクライアントがノードマップを取得し、WireGuardの鍵を交換するには、有効なHTTPSエンドポイントが必要です。ここでは、Let’s EncryptでTLS終端を行うNginxのリバースプロキシの背後で、コンテナとしてHeadscaleを起動します。

1. ディレクトリと設定ファイルの準備

サーバー上にプロジェクトのディレクトリツリーを作成し、デフォルトの設定テンプレートを取得します。

mkdir -p /opt/headscale/config /opt/headscale/data
cd /opt/headscale

wget -q https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -O config/config.yaml

config/config.yamlを開き、公開ドメイン、リッスンポート、IPプールを設定します。

server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090

ip_prefixes:
  - 100.64.0.0/10
  - fd7a:115c:a1e0::/48

db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite

log:
  format: text
  level: info

2. Docker Composeによるデプロイ

/opt/headscale/配下にdocker-compose.ymlを作成します。

services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    restart: unless-stopped
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    ports:
      - "127.0.0.1:8080:8080"
    command: headscale serve

コンテナを起動します。

docker compose up -d

3. TLS対応Nginxリバースプロキシのセットアップ

Tailscaleクライアントは協調サーバーを常時ポーリングし、アクティブなWebSocket接続を維持します。Nginxの設定ではUpgradeヘッダーを中継し、プロキシバッファリングを無効化してください。

server {
    listen 80;
    server_name headscale.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name headscale.example.com;

    ssl_certificate /etc/letsencrypt/live/headscale.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/headscale.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

ユーザーの作成とノードの登録

Headscaleでは、「ユーザー」と呼ばれる名前空間でマシンをグループ化します。まずは運用インフラ用の初期ユーザーを作成しましょう。

docker exec -it headscale headscale users create ops-team

方法A:事前認証キー(Pre-Auth Keys)による自動参加

無人サーバー、CI/CDノード、クラウドインスタンスの場合、Web画面での手動認証よりも自動登録が適しています。30日間有効な再利用可能キーを生成します。

docker exec -it headscale headscale preauthkeys create --user ops-team --reusable --expiration 720h

コマンドを実行すると、48文字のトークン(例: 9e2c6b4a8f103d...)が出力されます。このキーをクライアントのCLIに渡します。

sudo tailscale up \
  --login-server https://headscale.example.com \
  --authkey 9e2c6b4a8f103d81b9ecf0246a81d09e \
  --accept-routes

方法B:ノートPCやワークステーション向け対話型登録

個人のノートPCをオンボーディングする場合は、認証キーなしで接続を開始します。

sudo tailscale up --login-server https://headscale.example.com

ターミナルに登録リンク(例: https://headscale.example.com/register/mkey:abc123def456)が表示されます。キーのパラメータを抽出し、Headscaleホスト側でデバイスを承認します。

docker exec -it headscale headscale nodes register --user ops-team --key mkey:abc123def456

ノードに内部IPアドレス(例: 100.64.0.2)が割り当てられたことを確認します。

docker exec -it headscale headscale nodes list

本番運用から得られた知見

ベアメタルノードやAWSインスタンスにまたがってHeadscaleを運用すると、事前に考慮しておくべき現実的なエッジケースがいくつか見えてきます。

1. 高負荷時におけるSQLiteの安全なバックアップ

SQLiteは40MB未満のメモリ消費量で300〜500程度のアクティブピアを容易に処理できます。ただし、単純なファイルコピーを行うと、鍵交換中に不完全な書き込みやページの破損が発生するリスクがあります。毎日のcronジョブ内でSQLiteのオンラインバックアップツールを使用してください。

sqlite3 /opt/headscale/data/db.sqlite ".backup '/var/backups/headscale-$(date +%F_%H%M).sqlite'"

2. レガシーサブネット向けのサブネットルート公開

プリンター、レガシーストレージ機器、ベアメタルのデータベースなどでは、クライアントバイナリを直接実行できない場合があります。その場合は、特定のゲートウェイマシンからサブネットをアドバタイズさせます。

sudo tailscale up --login-server https://headscale.example.com --advertise-routes=192.168.10.0/24

サーバー側でルートを確認して有効化するまで、Headscaleはパケットを転送しません。

# 利用可能なルートを一覧表示し、ルートID(例: ID 1)を確認
docker exec -it headscale headscale routes list

# ルートID 1を有効化
docker exec -it headscale headscale routes enable -r 1

3. DERPリレーへのフォールバックを防止する

WireGuardの直接ハンドシェイクはUDPポート41641に依存しています。クラウドのセキュリティグループやローカルのファイアウォールで受信UDPがブロックされていると、ピアは通知なしにDERPリレーサーバーへフォールバックします。その結果、レイテンシは8msから90ms超へと急増し、スループットは大幅に低下します。ホストのファイアウォールでUDP 41641を必ず開放してください。2つのメッシュエンドポイント間で簡易チェックを実行できます。

tailscale ping 100.64.0.3

direct 198.51.100.22:41641ではなくpong from ... via DERP(fra)と表示された場合は、直ちにインバウンドUDPファイアウォールルールを確認してください。

インフラストラクチャの次のステップ

Headscaleをセルフホストすることで、メッシュネットワーキングの高速性、標準のTailscaleクライアントバイナリ、そしてルーティングテーブルに対する完全な制御権を自組織のものにできます。ライセンスプランの制限や予期せぬレート制限、外部へのトラッキングも一切ありません。クラウドインスタンス上にテストノードを立ち上げ、認証キーを生成し、手元のハードウェアでダイレクトピア通信の低遅延を体感してみてください。

Share: