DockerでHeadscaleを構築:Tailscaleのコントロールプレーンをセルフホストして無制限のメッシュVPNを実現する

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

現場の課題:CGNAT環境下でのプライベートメッシュネットワーク拡張

自宅ラボ、コロケーションラック、モバイル端末をまたいでサーバーへアクセスするのは、決して簡単なことではありません。多くの一般家庭向けISPでは、加入者がCGNAT(キャリアグレードNAT)の配下に置かれ、グローバルルーティング可能なIPv4アドレスが割り当てられません。また、事前の予告なく変動する動的IPが割り当てられるケースも多々あります。従来の回避策として用いられてきた「ルーターのポートを開放してDynamic DNSを設定する」という手法は、設定後わずか数分でインターネット上のボットによる大量スキャンに内部管理インターフェースを晒すリスクを抱えています。

自宅ラボの規模が拡大するにつれて、環境は急速に脆くなります。当初はProxmoxノード2台のシンプルな構成であっても、ステージング用コンテナ、拠点外のTrueNASバックアップ、外出先のリモートノートPCなどを追加していくと、エンドポイントは瞬く間に30台以上に膨れ上がります。静的な拠点間トンネル(Site-to-Site)は、ISPによってIPリースの再割り当てが行われた瞬間に切断されてしまいます。3箇所の異なる物理拠点にまたがるファイアウォール開放ルールの管理は、あっという間に運用管理の泥沼と化してしまいます。

根本原因の分析:従来のVPNやマネージドSaaSが行き詰まる理由

自宅ラボが従来のリモートアクセス手法で限界を迎えるのは、主に以下3つのアーキテクチャ上のボトルネックが原因です。

  • ハブ&スポーク型の帯域ボトルネック: 従来のOpenVPNや中央集約型のWireGuardトンネルでは、すべてのトラフィックが単一のメインサーバーを経由します。そのため、自宅のギガビット回線に障害が発生したり、ISPのアップロード帯域制限(30Mbpsなど)を受けたりすると、たとえリモートクライアント同士が高速なデータセンター回線上にあったとしても、クライアント間の通信が完全に遮断されてしまいます。
  • WireGuardフルメッシュ設定の疲弊: WireGuardは驚異的なスループット(ギガビット回線で850Mbps以上を記録することも珍しくありません)を誇りますが、ネットワークトポロジーの自動管理機能はありません。20台目のデバイスを追加するには、キーペアを生成し、既存の19台すべてのピアの設定ファイルを手動で更新する必要があります。エンドポイントIPの指定を1箇所ミスするだけで、ルーティング全体が破綻します。
  • SaaS版の制限と制約: Tailscaleは、WireGuardにSTUNによるNAT越えとDERPリレーへのフォールバックを組み合わせることで、メッシュネットワークの自動調整を実現しました。しかし、TailscaleのSaaSプランには制約があります。無料プランではアカウントあたり3ユーザー、100デバイスまでに制限されています。また、外部のOAuthプロバイダー(Google、GitHub、Microsoftなど)が必須であり、ルーティングトポロジーのマップ情報も自社の管理下にないプロプライエタリなインフラ上でホストされます。

ソリューションの比較:WireGuard、公式Tailscale、Headscale

分散した自宅ラボのインフラを相互接続する際の、主要な3つのアプローチの比較は以下のとおりです。

比較項目 純粋なWireGuard 公式Tailscale (SaaS) セルフホスト型Headscale
アーキテクチャ 手動メッシュまたはハブ&スポーク 自動調整型メッシュ 自動調整型メッシュ(オープンソース)
NAT越え 手動でのポートフォワーディングが必須 自動(STUN/DERP) 自動(STUN/DERP)
リソースフットプリント カーネルモジュール(RAM 10MB未満) クライアントデーモン(RAM 約40MB) サーバーデーモン(RAM 約35〜60MB)
デバイス数上限 無制限(手動保守が必要) 無料プランでは制限あり(100ノード) 無制限
プライバシーと制御性 100%自己完結 調整用メタデータをSaaS側で保持 100%自前ハードウェア上でセルフホスト
クライアント互換性 標準のWireGuardアプリ 公式Tailscaleクライアント 公式Tailscaleクライアント(カスタムログインURL指定)

公式Tailscaleは導入が非常に手軽です。一方、Headscaleを使用すれば、ゼロコンフィグでのNAT越えメッシュネットワークという同等の利便性を享受しつつ、アイデンティティ、アクセストークン、トポロジーログに対する完全な主権を手元に維持できます。

最適なアプローチ:Dockerによる本番運用レベルのHeadscale構築

Debian 12またはUbuntu 24.04を搭載した月額4ドル程度の安価なVPS(1 vCPU、1GB RAM)でも、Headscaleを動作させるには十分すぎる性能が得られます。リバースプロキシの配下でHeadscaleをDockerコンテナとして稼働させることで、実行環境の状態を分離し、アップグレードを簡素化し、TLS証明書のハンドシェイクを自動化できます。

ステップ1:ディレクトリ構成と基本設定

データベースファイルと設定テンプレートを永続化するために、ホスト上に専用ディレクトリを作成します:

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

Headscaleのリポジトリから公式サンプル設定ファイルを直接ダウンロードします:

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

config/config.yaml を開き、主要なネットワーク設定を調整します。公開ドメイン、内部リッスンインターフェース、メッシュピアに割り当てるCGNATサブネットを定義します:

server_url: https://headscale.example.com:443
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

derp:
  server:
    enabled: false
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  auto_update_enabled: true
  update_frequency: 24h

database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

dns:
  magic_dns: true
  base_domain: mesh.internal
  nameservers:
    list:
      - 1.1.1.1
      - 1.0.0.1

ステップ2:Docker Composeのセットアップ

/opt/headscale/ に docker-compose.yml ファイルを作成します。リバースプロキシが受信トラフィックを安全に処理できるよう、コンテナポートは必ずlocalhostにバインドします:

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

コンテナを起動し、初期化ログを確認します:

docker compose up -d
docker compose logs -f headscale

ステップ3:リバースプロキシとTLS終端

Tailscaleクライアントは、長時間持続するWebSocketとgRPCストリームを使用してHTTPS経由でHeadscaleと通信します。暗号化されていないポート8080を直接インターネットへ公開してはいけません。Caddyを使用している場合、Caddyfile にわずか3行追加するだけで自動TLS化が完了します:

headscale.example.com {
    reverse_proxy 127.0.0.1:8080
}

Nginxを使用したい場合は、プロトコルアップグレード用のヘッダーを明示的に転送するようバーチャルホストを設定します:

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

    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_redirect off;
        proxy_buffering off;
        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;
    }
}

ステップ4:ユーザーと事前認証キーの作成

Headscaleでは、ノードをユーザーごとにグループ化して管理します。まず、管理用のネームスペース(ユーザー)を作成します:

docker exec -it headscale headscale users create homelab-admin

個人のワークステーションであれば対話型ログインで問題ありませんが、ヘッドレスノードには、再利用可能な事前認証キー(Pre-Auth Key)を生成して使用します:

docker exec -it headscale headscale preauthkeys create --user homelab-admin --reusable --expiration 720h

発行された48文字のトークンは安全に保管してください。このトークンはAnsible Playbookやcloud-initテンプレートに直接組み込むことができます。

ステップ5:公式Tailscaleクライアントでのノード登録

Headscaleは、Linux、macOS、iOS、Android、Windows向けの公式Tailscaleバイナリとシームレスに連携します。事前認証キーを使ってヘッドレスLinuxホストを登録するには、次のコマンドを実行します:

sudo tailscale up --login-server https://headscale.example.com --authkey <YOUR_PREAUTH_KEY>

事前認証トークンを使わず対話型認証を行いたい場合は、key引数を省略します:

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

ターミナルに登録用URLが表示されます。そのリンクをコピーし、パラメータからマシンキーを抽出して、Headscaleサーバー側でマシンを承認します:

docker exec -it headscale headscale nodes register --user homelab-admin --key <MACHINE_KEY_FROM_TERMINAL>

運用の実践テクニックとベストプラクティス

  • SQLiteのアトミックな自動バックアップ: Headscaleは状態を /opt/headscale/data/db.sqlite に記録します。コンテナの稼働中に通常の cp コマンドでこのファイルをコピーしないでください。代わりに、夜間のcronジョブなどで安全な特定時点のスナップショットを作成します:sqlite3 /opt/headscale/data/db.sqlite ".backup '/backup/headscale-$(date +%F).sqlite'"。
  • 既存のLANサブネットへのブリッジ接続: プリンター、ネットワークカメラ、スマート電源タップなどはVPNデーモンを直接実行できません。ローカルのRaspberry PiやミニPCをゲートウェイとして活用し、tailscale up --login-server https://headscale.example.com --advertise-routes=192.168.1.0/24 を実行します。その後、Headscale側で docker exec -it headscale headscale routes enable -r <ROUTE_ID> を実行してルートを承認します。
  • 競合しないMagicDNSの設定: mesh.internal などの内部専用サフィックスを使用してください。これにより、実在する外部ドメインに干渉することなく、ノード間の名前解決(例:grafana.mesh.internal)が自動的かつ確実に行われます。
  • コンテナイメージのタグ固定: headscale:latest の利用は避けてください。マイナーリリース間でデータベースのマイグレーションが発生するためです。更新前に変更履歴やスキーマの変更点を確認できるよう、headscale:0.23.0 のように特定のリリースバージョンにタグを固定しましょう。
  • ピア間の直接接続テスト: tailscale status および tailscale ping <DESTINATION_IP> を使用してルーティングのパフォーマンスを確認します。直接のUDPホールパンチングが成功していれば、ブロードバンド環境では通常20ms未満の往復レイテンシが得られます。トラフィックがDERPリレーにフォールバックしている場合は、クライアント側のファイアウォールでポート41641の送信UDP通信が許可されているか確認してください。
Share: