構築ログ

Mac miniを司令塔、WindowsゲーミングPCを重機にする — TailscaleとSSHで作るハイブリッド開発環境の設計図

手元は軽いMac、GPUはWindows。Tailscaleの私設VPNとOpenSSHの公開鍵認証で2台をつなぐ設計図を、Tailscale・Microsoft Learn・VS Codeの公式手順にあるコマンドだけで組む。WSL2に入る経路と落とし穴も。

小さなMac miniから伸びる一本の光る線が雲(VPN)を通って大きなWindowsゲーミングPCに届き、鍵マークで認証されているイメージ
AI生成のイメージ画像です

手元は軽いMac、GPUはWindows。2台をつなぐのはTailscale(私設VPN)とSSH(公開鍵認証)の2本だけで、自宅の外からも同じ経路で届く。

これが俺の開発環境の設計図だ。Mac mini(M4 Pro)で書いて、型落ちRTX 2070 Superのデスクトップで回す。

この記事では、その配線を公式ドキュメントの手順だけで組む。速い遅いの体感は書かない。書けるのは「どこに何を置けば通るか」までだ。

なぜ2台をつなぐのか

理由は所信表明に書いた通りで、手取り19万の施工管理技士に100万円のMacは買えない。

コードを書く場所と、GPUをぶん回す場所を分ける。Macは軽くて静かで持ち出せる。Windowsは重くてうるさいが、CUDAが動く。

現場で言えば、司令塔が無線で重機に指示を出す関係だ。隣に立つ必要はない。無線さえ通っていればいい。その「無線」がTailscaleとSSHだ。

接続の仕組み

Tailscaleは、登録した端末それぞれに 100.x.y.z の範囲のIPアドレスを1つ割り当てる。公式によれば、端末が物理的にどこへ移動してもこのアドレスは変わらない。

VS Code Remote-SSH(Mac側)~/.ssh/config の Host を指すだけ同じ経路・同じ鍵Mac(司令塔)ssh クライアント~/.ssh/ の秘密鍵1Tailscale100.x.y.zMagicDNS名で呼べる2Windows(重機)OpenSSH Server(:22)authorized_keys3WSL2wsl.exe -dUbuntu など4GPUジョブ5鍵を差し出す側パスフレーズは手元でACLで検証誰が どのポートに届いてよいか公開鍵認証で検証authorized_keys と秘密鍵の照合ディストリとLinuxユーザー-d / --userCUDAで実行結果(ログ・生成物)は同じトンネルを逆向きに戻る。Mac側にGPUは要らない
接続の段取り。①Macが秘密鍵を持ってsshを打つ → ②Tailscaleが100.x.y.zの私設アドレスで相手を見つけ、ACLで「誰がどのポートに届いてよいか」を判定 → ③WindowsのOpenSSH Serverがauthorized_keysと秘密鍵を照合 → ④wsl.exeでWSL2のディストリとユーザーに入る → ⑤GPUジョブ。VS Code Remote-SSHはMac側の~/.ssh/configを読んで③に刺さる。

さらにMagicDNSが端末ごとにDNS名を自動登録する。公式の例では、monitoring という機体に ssh username@monitoring と打つだけで届く。完全な名前は machine-name.tailnet-name.ts.net の形で、2022年10月20日以降に作ったtailnetでは最初から有効だ。

NAT越えは、端末同士でUDPの直結を試み、通らなければDERPという中継サーバーを経由する仕組みだ。公式は「両者の違いは性能であって、セキュリティではない」とし、どちらもWireGuardでエンドツーエンド暗号化されると書いている。直結か中継かは tailscale status に direct か relay が出るかで分かる。

そしてACL。公式によれば、作った直後のtailnetは「全端末が互いに通信できる」状態で、管理コンソールのAccess controlsページから src(誰が)と dst(どの端末の何番ポートに)を絞れる。

つまり検証は2段ある。Tailscaleが「誰がどのポートに届いてよいか」を見て、その先でWindowsのOpenSSH Serverが「その鍵は登録済みか」を見る。図の②と③だ。

なお、Tailscale自身のSSHサーバー機能(Tailscale SSH)は、公式によればLinuxとmacOSのみでWindowsは対象外。だからWindows側にはOpenSSH Serverを立てる。

手順(公式のコマンドだけ)

1. Windows側: OpenSSH Serverを有効にする

Microsoft Learnの手順そのままだ。前提はWindows 10(build 1809)以降と管理者権限のPowerShell。GUIならスタートで「オプション機能」を検索して OpenSSH サーバー を追加。PowerShellなら次の通り。

Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'

公式によれば、インストール時にファイアウォール規則 OpenSSH-Server-In-TCP(TCP 22の受信許可)が自動で作られる。確認は次の1行。

Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP"

2. Mac側: 鍵を作る

VS Code公式の手順にあるコマンドで鍵ペアを作る。

ssh-keygen -t ed25519 -b 4096

Microsoft Learnは「空のパスフレーズは推奨しない」と明記している。秘密鍵(~/.ssh/id_ed25519)はMacから出さない。サーバーに置くのは .pub だけだ。

3. Windows側: 公開鍵を登録する

ここがWindows特有で、置き場所がアカウントの種類で変わる。

  • 標準ユーザー: C:\Users\<ユーザー名>\.ssh\authorized_keys
  • 管理者グループのユーザー: C:\ProgramData\ssh\administrators_authorized_keys

自分でセットアップしたゲーミングPCなら、たいてい後者だ。管理者用のファイルはACLを「AdministratorsとSYSTEMのみ」に絞る必要があり、公式のコマンドはこれ(管理者PowerShell)。

icacls.exe "C:\ProgramData\ssh\administrators_authorized_keys" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"

公開鍵の中身を上のファイルに1行追記したら、これを打つ。

設定ファイルは %programdata%\ssh\sshd_config。sshdは起動時にしか設定を読まないので、書き換えたらサービスを再起動する。パスワード認証を切るなら、OpenSSH本家の sshd_config マニュアルにある PasswordAuthentication(既定は yes)を no に。鍵で入れることを確認してから切るのが順番だ。

4. Tailscaleを両端に入れる

Windowsは公式サイトの .exe を実行し、タスクトレイのアイコンから Log in。Macも同様。同じアカウントで入れば同じtailnetに乗る。

公式の推奨がひとつ。TailscaleはWindows側にだけ入れる。WSL2の中にも入れると、公式の説明では「Tailscaleのパケットの中にTailscaleのパケットが収まらない」状態になって通らない。

もうひとつ、タスクトレイのPreferencesにある Run unattended を有効にすると、ログインユーザーではなくシステムとしてTailscaleが動く。

5. Macから打つ

ssh <Windowsのユーザー名>@<MagicDNS名>

初回はホスト鍵の確認が出るので yes。つながらないときは tailscale ping <MagicDNS名> でTailscaleの層だけを切り分けられる。macOSのApp Store版はCLIが /Applications/Tailscale.app/Contents/MacOS/Tailscale にある。

6. VS Code Remote-SSH

VS Code公式は、接続先として「公式のOpenSSH Serverを使うWindows 10 / Server 2016/2019(1803+)」をサポート対象に挙げている。

コマンドパレットで Remote-SSH: Connect to Host… を選び、手順5と同じ user@hostname を入れる。毎回打つのが面倒なら ~/.ssh/config に書く。

Host juki
    HostName <MagicDNS名>
    User <Windowsのユーザー名>
    IdentityFile ~/.ssh/id_ed25519

公式は「パスワードやトークンは保存されないので鍵認証を推奨する」としている。図の一番上、VS Codeの線が③に刺さっているのはこの意味だ。

7. WSL2に入る

Windowsにsshで入った時点で、そこはcmd.exe(既定)だ。GPUジョブを流したいのはWSL2の中なので、あと1段入る。一番素直なのは、Microsoft Learnの基本コマンドにある wsl.exe を打つことだ。

wsl --distribution <ディストリ名> --user <Linuxユーザー名>

これで図の④、WSL2側のシェルに落ちる。ここから先はWSL2+CUDAの記事で組んだ環境そのままだ。

落とし穴(公式に書いてあるものだけ)

ファイアウォール。 OpenSSH側は、公式が「OpenSSH-Server-In-TCP が有効でないと接続は拒否かリセットされる」と書いている。手順1の確認コマンドで見る。Tailscale側は「ほとんどの場合、ポートを開ける必要はない」が公式の立場で、UDPが塞がれてもDERP中継に落ちるだけだ。

WSL2のNAT。 Microsoft Learnによれば、WSL2は既定でNAT構成で、Windowsとは別の仮想アダプタと固有のIPを持つ。WSL2の中でsshdのようなサーバーを直接受けたい場合、外からは素通しにならない。公式が示す方法は2つ。netsh interface portproxy add v4tov4 でWindows側のポートをWSL2のIP(wsl hostname -I)に転送するか、Windows 11 22H2以降なら .wslconfig に networkingMode=mirrored を書いてLANから直接届くようにするか(この場合Hyper-Vファイアウォールの受信許可も別途要る)。

俺の設計図では、この面倒を避けて「Windowsのsshdで受けて wsl.exe で1段入る」経路を本線にしている。WSL2側にsshdを立てるのは必要になってからでいい。

スリープ。 正直に書くと、公式FAQに「スリープ中の挙動」を明記した記述は見つけられなかった。相手が眠っていればsshは届かない、という当たり前の話として、重機側の電源設定は各自で確かめてほしい。俺は「ジョブ中は起こしておく」前提で組んでいる。

次にやること

配線が通ったら、重機に最初のジョブを載せる。同日公開のOllama入門は、この経路の先でローカルLLMを動かす話だ。

重機をこれから選ぶなら、VRAM基準で仕分けた今週のセール機と二刀流ランキングへ。司令塔は今のPCでいい。金はGPUに全振りしろ、が所信表明からの一貫した結論だ。

無線が通れば、司令塔はどこにいてもいい。


※本記事は各公式ドキュメント(2026年9月時点)の記載に基づく設計メモです。速度・遅延などの実測は行っておらず、ネットワーク設定の安全性を保証するものではありません。設定変更は各公式ドキュメントを確認のうえ自己責任で行ってください。

参考(一次情報):