🎯 はじめに:この記事のスコープ
シリーズ「脱Microsoft・OSS移行」の 第 7 回 は、出先から Ubuntu へ入る経路を作りました。第 8 回 で帳票を Windows から剥がし、Linux 上で PDF を再現できるようになりました。
それでも Ubuntu だけで閉じない作業が 2 つ残ります。iOS シミュレーターは Mac、WPF と WebView2 は Windows でしか動きません。正本を Ubuntu に置いたまま、この 2 台を「ビルドと確認だけの出先」にする。それが本記事の SSH 手順です。向きは第 7 回の逆です。
| 本記事の扱う範囲 | 隣接記事 | |
|---|---|---|
| 本記事(第 9 回) | Ubuntu → Mac / Windows 11 への SSH 接続 / Mac のリモートログイン / Windows OpenSSH Server の導入と自動起動 / Entra ID アカウントが SSH の認証に使えない理由とローカル標準ユーザー / Public な Wi-Fi で送信元だけ許可する許可ルール / 鍵は Ubuntu に残す運用 / 接続後に確認する SDK と、SSH で閉じない受入 | 第 7 回(Ubuntu へ入る向き)、Linux から Mac / Windows へ出して TDD する(出先で何をするか)、人が残る切替(実機・実紙の受入) |
💡 レガシー置換 第 5 回 との関係: あちらは「出先で初めて落ちるテスト」「出先の緑を切替にしない」という 出先で何をするか の話です。本記事は 出先へどう繋ぐか の手順だけを書きます。あちらが概念として書いた「鍵は Linux に残す」「Ubuntu からの TCP 22 だけ許可する」を、コマンドに落とします。
🧭 結論:向きが逆でも土台は同じ、違うのは Windows 側の 3 つの罠
| 層 | Mac | Windows 11 | 共通の方針 |
|---|---|---|---|
| 経路 | 同じ Wi-Fi、または Tailscale | 同じ Wi-Fi、または Tailscale | 同じ机上のビルド機なので Tailscale 必須にはしない。Wi-Fi が Public でも ネットワーク種別は変えない |
| 認証 | 通常ユーザーの ~/.ssh/authorized_keys | ローカル標準ユーザーの .ssh\authorized_keys | 秘密鍵は Ubuntu にだけ置く。個人鍵は使い回さない |
| 権限 | 普段のユーザーで可 | ローカル標準ユーザーを SSH 専用に作る | 管理者にしない。SDK は管理者が入れ、ビルドは標準ユーザーで回す |
Mac は「リモートログイン」を有効にして公開鍵を置くだけです。手間の 9 割は Windows 側で、しかも詰まる場所が決まっています。
- Entra ID のアカウントでは Windows OpenSSH の認証が通らない(公開鍵でもパスワードでも)。普段のサインインは Entra ID のまま、SSH 専用のローカルユーザーを足す
- 管理者ユーザーだと
authorized_keysが無視される。読まれるのはC:\ProgramData\ssh\administrators_authorized_keysで、ACL の条件もある。標準ユーザーならこの罠ごと消える - 導入時に作られる許可ルールは Private プロファイルだけ。Wi-Fi が Public だと sshd は起動しているのに黙って繋がらない
以下、この 3 つを避ける順番で組みます。
🔑 Step 0:Ubuntu 側で専用鍵を作る
出先はビルド機であって、秘密の保管場所ではありません。この接続専用の Ed25519 鍵を作り、秘密鍵は Ubuntu のユーザー領域から出しません。
ssh-keygen -t ed25519 -f ~/.ssh/build_hosts_ed25519 -C "ubuntu-dev@build-hosts"
# 秘密鍵: ~/.ssh/build_hosts_ed25519 → Mac / Windows / リポジトリへコピーしない
# 公開鍵: ~/.ssh/build_hosts_ed25519.pub → この 1 行だけを 2 台へ登録する
cat ~/.ssh/build_hosts_ed25519.pub
# ssh-ed25519 AAAA... ubuntu-dev@build-hosts
- 既存の個人鍵(GitLab 用など)は使い回さない。用途が違う鍵は、失効させる単位も違う
- コメント欄は「どこから、何のために」が分かる文字列にする。棚卸しのときに効く
- 公開鍵の登録に、相手側のパスワードをチャットや Issue へ送る必要はない。登録は各マシンのローカル操作で完結する
🍎 Step 1:Mac 側 — リモートログインと authorized_keys
リモートログインを有効にする
「システム設定 → 一般 → 共有 → リモートログイン」をオンにし、接続を許可するユーザーを選びます。画面に表示される ssh <user>@<host> の形が、そのまま短いアカウント名の確認になります。Apple の手順
公開鍵を追記する
対象ユーザーでログインした Mac のターミナルで実行します。既存の authorized_keys は上書きせず、1 行だけ追記します。
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' 'ssh-ed25519 AAAA... ubuntu-dev@build-hosts' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
接続中の運用
- 電源に繋ぎ、スリープさせない。シミュレーターの起動途中でスリープに入ると、SSH ごと切れる
- Xcode が既に入っているなら 確認前に一律アップデートしない。今動いている組み合わせを先に記録し、不足分だけ足す(後述の Step 4)
🪟 Step 2:Windows 側 — ローカル標準ユーザー・OpenSSH Server・許可ルール
実施順序は固定です。① ローカル標準ユーザーを作る → ② 管理者が OpenSSH Server を導入・起動する → ③ そのユーザーで一度サインインする → ④ そのユーザー自身が公開鍵を登録する。①② は管理者、④ は標準ユーザー自身の PowerShell で実行します。
2-1:なぜローカル標準ユーザーか
Windows OpenSSH は Microsoft Entra アカウントによる認証をサポートしていません(Microsoft の記述)。普段のサインインが Entra ID なら、SSH ではローカルアカウントを使うしかありません。
そのローカルユーザーを 管理者にしない理由が 2 つあります。
- 管理者グループのメンバーは、ユーザープロファイルの
authorized_keysではなくC:\ProgramData\ssh\administrators_authorized_keysが読まれ、icaclsで ACL を Administrators と SYSTEM に絞る必要がある(Microsoft の鍵管理)。標準ユーザーなら既定のプロファイル配下で済む - 出先はビルドとテストを回す場所であって、設定を変える場所ではない。SDK の導入は管理者が別途行い、ビルドは標準ユーザーで回す。実際、今回の Windows 側は既存の .NET 10 SDK をそのまま使い、SDK の追加導入も昇格もしていない
普段の Entra ID サインインや端末の参加状態は変えません。
2-2:ローカル標準ユーザーを作る(管理者)
GUI なら「設定 → アカウント → 他のユーザー → アカウントの追加」から、Microsoft アカウントを使わないユーザーを追加します。権限は標準ユーザー、パスワードは Windows 本体で設定・保持します。
PowerShell で作る場合は、64 ビットの管理者 PowerShell で実行します。Users グループを SID で指定しているのは、Windows の表示言語でグループ名が変わっても動くようにするためです。
$devUser = Get-LocalUser -Name devuser -ErrorAction SilentlyContinue
if ($null -eq $devUser) {
$devPassword = Read-Host -Prompt 'Password for local devuser' -AsSecureString
$devUser = New-LocalUser -Name devuser -Password $devPassword -Description 'SSH build user'
# S-1-5-32-545 = Users(標準ユーザー)。Administrators には入れない
$membership = @(Get-LocalGroupMember -SID 'S-1-5-32-545' | Where-Object { $_.SID -eq $devUser.SID })
if ($membership.Count -eq 0) {
Add-LocalGroupMember -SID 'S-1-5-32-545' -Member $devUser
}
}
$devUser | Select-Object Name, Enabled
作成後、そのユーザーで一度サインインします。プロファイルと %USERPROFILE% が実体化し、後の鍵登録先ができます。既存の作業を閉じたくなければ、サインアウトではなく「ユーザーの切り替え」を使います。
2-3:OpenSSH Server を導入して自動起動にする(管理者)
管理者の PowerShell で、次のブロックをまとめて実行します。どの段階で失敗したかを残し、サービスが無い状態で Start-Service を繰り返さないための形です。再起動が必要なら自動では再起動せず、止めて知らせます。
& {
$ErrorActionPreference = 'Stop'
$principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Windows PowerShell を「管理者として実行」してください。'
}
$cap = Get-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
$cap | Select-Object Name, State
if ($cap.State -eq 'NotPresent') {
$install = Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
$install
if ($install.RestartNeeded) {
throw 'Windows の再起動が必要です。保存して再起動後、このブロックを再実行してください。'
}
}
$cap = Get-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
if ($cap.State -ne 'Installed') {
throw ('OpenSSH Server の導入が未完了です。State: ' + $cap.State)
}
Start-Service -Name sshd
Set-Service -Name sshd -StartupType Automatic
Get-Service -Name sshd | Select-Object Status, Name
Get-NetFirewallRule -Name OpenSSH-Server-In-TCP | Select-Object Name, Enabled, Profile, Action
}
最後の表示が Running なら導入は完了です。Microsoft の導入手順
2-4:待ち受けと既定ルールを確認する
sshd が起動していても、繋がるかどうかは別です。3 つ確認します。
Get-NetTCPConnection -State Listen -LocalPort 22 | Select-Object LocalAddress,LocalPort
Get-NetFirewallRule -Name OpenSSH-Server-In-TCP | Select-Object Enabled,Profile,Direction,Action
Get-NetConnectionProfile | Select-Object InterfaceAlias,NetworkCategory
今回の実機ではこう出ました。
LocalAddress LocalPort
------------ ---------
:: 22
0.0.0.0 22
Enabled Profile Direction Action
------- ------- --------- ------
True Private Inbound Allow
InterfaceAlias NetworkCategory
-------------- ---------------
Wi-Fi Public
Tailscale Private
待ち受けは全アドレスで開いている。既定の許可ルールは Private プロファイルだけ。ところが Wi-Fi は Public。つまり Wi-Fi 経由の Ubuntu からは、sshd が動いていても届きません。Tailscale インターフェイスは Private なので、こちら経由なら既定ルールのまま通ります。
2-5:Public な Wi-Fi なら、送信元を絞った許可ルールを足す(管理者)
やってはいけないのは、Wi-Fi の種別を Private に変えること、既定ルールを Public にも広げること、ファイアウォールを止めることです。Ubuntu 開発機からの TCP 22 だけを、Wi-Fi インターフェイスの Public プロファイルで許可します。
New-NetFirewallRule -Name 'Dev-SSH-WiFi' -DisplayName 'Dev SSH from Ubuntu (Wi-Fi)' `
-Enabled True -Direction Inbound -Action Allow -Protocol TCP -LocalPort 22 `
-RemoteAddress '<UbuntuのIP>' -InterfaceAlias 'Wi-Fi' -Profile Public
-RemoteAddressに書いた IP が DHCP で変わると、そこで止まります。ルーター側で予約するか、変わらない Tailscale IP を使います- Ubuntu と Windows の両方が tailnet に入っているなら、この節は不要です。Tailscale IP か MagicDNS 名で繋げば既定ルールで通ります。Wi-Fi ルールは tailnet に入っていない機材向けの手当てです
2-6:標準ユーザー自身が公開鍵を登録する
②③ が終わったら、標準ユーザーでサインインした状態の PowerShell で実行します。管理者アカウントで実行すると、別のプロファイルに書いてしまいます。先頭の if はその事故を止めるためのものです。
if ($env:USERNAME -ne 'devuser') { throw 'devuser でサインインしてから実行してください。' }
$sshDir = Join-Path $env:USERPROFILE '.ssh'
New-Item -ItemType Directory -Force -Path $sshDir | Out-Null
Add-Content -LiteralPath (Join-Path $sshDir 'authorized_keys') -Encoding ascii `
-Value 'ssh-ed25519 AAAA... ubuntu-dev@build-hosts'
>やOut-Fileで書かない。Windows PowerShell では UTF-16 になり、sshd が鍵を読めません。Add-Content -Encoding asciiで追記します- 接続に使うユーザー名は、そのユーザーで
whoamiを打てば<ホスト名>\devuserと出ます。ホスト名の部分は SSH には不要です - Windows Hello の PIN は SSH のパスワードではありません。公開鍵認証だけで繋ぐので、PIN を入力する場面はありません
2-7:(任意)既定シェルを PowerShell にする(管理者)
Windows OpenSSH の既定シェルは cmd.exe です。CLI からビルドとテストを叩くだけなら cmd でも困りませんが、PowerShell の方が扱いやすければレジストリで切り替えます。Microsoft の設定リファレンス
New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell `
-Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType String -Force
🧩 Step 3:Ubuntu 側の ~/.ssh/config
2 台とも同じ鍵で、IdentitiesOnly yes を付けます。個人鍵が複数あると、ssh は順に全部試し、Windows 側は試行回数で拒否することがあります。
Host mac
HostName <MacのIP または MagicDNS 名>
User <mac-user>
IdentityFile ~/.ssh/build_hosts_ed25519
IdentitiesOnly yes
ServerAliveInterval 60
Host win
HostName <WindowsのIP または MagicDNS 名>
User devuser
IdentityFile ~/.ssh/build_hosts_ed25519
IdentitiesOnly yes
ServerAliveInterval 60
接続確認は、相手のシェルを意識して書きます。Windows の既定シェルは cmd なので、head や grep のような Unix コマンドは使えません。
ssh mac 'xcode-select -p && xcodebuild -version'
ssh win "whoami && dotnet --version && dotnet workload list"
VS Code の Remote SSH 拡張は Windows OpenSSH をリモート側として扱えます。ただし本記事の用途は「ビルドとテストを Ubuntu 側から叩く」ことなので、IDE を出先に開く必要はほとんどありません。開くなら第 7 回と同じ手順で Host win を選ぶだけです。
🧪 Step 4:接続後に確認するもの(インストールはしない)
繋がった直後にやるのは、版の収集です。入れ直しや更新は、不足が確定してから行います。
dotnet --info
dotnet workload list
git --version
Mac ではさらに次を確認します。
xcode-select -p
xcodebuild -version
xcrun simctl list devices available
| 作業 | 必要なもの | 確認元 |
|---|---|---|
| MAUI iOS / Mac Catalyst | .NET 10 SDK、対象の MAUI workload、対応する Xcode とシミュレーター。macOS のバージョンだけでは Xcode の適合版を決めない | Mac |
| iPhone 実機 | Mac とのペアリング・端末の信頼・署名/プロビジョニング。iOS 16 以降は Developer Mode | Mac + 端末 |
| MAUI Android | .NET 10 SDK、MAUI Android workload、対応する Android SDK / JDK。実機なら USB デバッグと端末側の許可 | Mac または Windows |
| WPF ビルド・実行 | .NET 10 SDK と Windows。新 WPF は ActiveReports を参照しない。プレビュー用の WebView2 Runtime の有無 | Windows |
| A4 PDF 生成 | Playwright の Chromium と日本語フォント(第 8 回) | どちらでも |
MAUI の導入条件、iPhone の Developer Mode。WPF の通常のデスクトップアプリと、MAUI の Windows ターゲットは分けて扱います。MAUI Windows も確認する場合は、その時点で Windows 向けの手順と配布形式を照合します。
今回は、Mac 側で Xcode に対応する .NET を作業コピーへ追加し、iOS シミュレーター向けビルドと起動画面の確認まで通りました。Windows 側は既存 SDK のまま WPF のビルドと、帳票・端末ロジックのテストまで通りました。
作業コピーを分ける
出先での作業は、既存の開発ディレクトリーとは別の 作業コピーで行います。署名情報や業務データを含むディレクトリーを上書きしない。転送したソースは SHA-256 で照合し、ビルドしたのが正本と同じ内容であることを残します。
⛔ SSH で閉じないもの
SSH でビルド・テスト・ログ収集ができるようになっても、受入が終わるわけではありません。
| SSH で閉じる | ログイン中のデスクトップ・実機が要る |
|---|---|
| ビルド、ユニットテスト、接続テスト、ログ収集 | WPF プレビューの見え方、印刷ダイアログ、紙の排出 |
| iOS シミュレーターの起動とキャプチャ | iPhone / Android 実機の操作、カメラ読取 |
| バージョンの収集、不足分の判定 | 実プリンター、実用紙、配送契約の照合 |
これはレガシー置換シリーズが「出先の緑を、切替にしない」「人が残る切替」と書いている線と同じです。接続手順が整うほど、この線を越えたつもりになりやすくなります。
🪤 ハマり集(踏んだもの・踏む前に避けたもの)
| 症状 | 原因 | 対処 |
|---|---|---|
Start-Service sshd が「サービスが見つからない」 | OpenSSH Server 未導入 | 管理者で Add-WindowsCapability(2-3 のブロック)。RestartNeeded なら再起動してから再実行 |
| sshd は Running なのに Ubuntu から接続がタイムアウト | 既定の許可ルールが Private のみで、Wi-Fi が Public | 送信元を絞った Public ルール(2-5)を足す。種別は変えない。tailnet に入っているなら Tailscale IP で繋ぐ |
公開鍵を登録したのに Permission denied (publickey)(Windows) | 登録先ユーザーが管理者で、読まれるのが administrators_authorized_keys | 標準ユーザーへ切り替える。管理者のままなら administrators_authorized_keys + icacls で ACL を絞る |
| Entra ID のユーザー名で認証できない | Windows OpenSSH は Entra アカウント非対応 | ローカル標準ユーザーを作って、そのユーザーへ登録する |
authorized_keys を作り直したら Permission denied に戻った | > や Out-File で書いて UTF-16 になり、sshd が読めない | Add-Content -Encoding ascii で追記する。既存ファイルは Get-Content で 1 行目が読めるか確認 |
Too many authentication failures | Ubuntu 側に個人鍵が複数あり、順に試している | ~/.ssh/config に IdentitiesOnly yes と IdentityFile を明示 |
| Mac がスリープしてシミュレーターごと切れる | 電源未接続、スリープ設定 | 電源に繋ぐ。接続中はスリープさせない |
| パスワードを聞かれる(公開鍵が使われていない) | 鍵の登録先ユーザーが違う。Windows Hello の PIN は SSH と無関係 | 登録先を whoami のユーザーに合わせる。PIN は入れない |
📋 リスクと対策表
| リスク | 影響度 | 対策 |
|---|---|---|
| 秘密鍵を出先へコピーしてしまう | 高 | 秘密鍵は Ubuntu のユーザー領域だけ。IdentityFile を明示し、Mac / Windows / リポジトリ / 計画フォルダーへ置かない |
| TCP 22 を Wi-Fi 全体に開ける | 高 | -RemoteAddress で Ubuntu だけに絞る。既定ルールと種別は触らない。ファイアウォールを止めない |
| SSH ユーザーを管理者へ昇格する | 中 | しない。SDK 導入は管理者アカウントで別途行い、ビルドは標準ユーザーで回す |
| DHCP で Ubuntu の IP が変わり、許可ルールが空振り | 中 | ルーターで予約、または Tailscale IP を送信元にする |
| どの鍵が何用か分からなくなる | 中 | コメントに用途を付ける。接続が不要になったら authorized_keys の該当行を削除し、Ubuntu 側の秘密鍵も消す |
| 出先で既存ディレクトリーを壊す | 中 | 作業コピーを分け、署名情報や業務データを含む場所へ触らない |
| 出先の緑を切替と混同する | 高 | SSH で閉じるものと閉じないものの表を、受入の項目に写す |
✅ 第 9 回まとめ
- 第 7 回が Ubuntu へ入る向きなら、本記事は Ubuntu から出る向き。土台は同じ公開鍵認証・最小権限・鍵を出先に置かない
- Mac は「リモートログイン」と
authorized_keysだけ。電源に繋ぎ、Xcode を一律更新しない - Windows は 3 つの罠を順に避ける。Entra ID では認証が通らないのでローカルユーザー、管理者だと
authorized_keysが無視されるので標準ユーザー、既定ルールは Private だけなので Public な Wi-Fi には送信元限定のルール - 実施順序は固定。標準ユーザー作成 → 管理者が OpenSSH Server を導入・自動起動 → そのユーザーで一度サインイン → そのユーザー自身が
Add-Content -Encoding asciiで公開鍵を登録 - Tailscale に入っていれば Wi-Fi ルールは不要。Tailscale インターフェイスは Private なので既定ルールで通る
- 繋がったら版の収集。
dotnet --info、workload、xcode-select、simctl。入れ直しは不足が確定してから - SSH で閉じないものを先に表にする。WPF の見え方、印刷ダイアログ、紙、実機は、ログイン中のデスクトップと現物でしか受け入れられない
これで、正本を Ubuntu に置いたまま、Mac と Windows を「ビルドと確認だけの出先」として使えます。Windows を残す理由は、また一段小さくなりました。
次回(公開予定)「コスト削減効果と 1 年運用レビュー」では、ライセンス削減の実績と、本シリーズ全体の運用上のハマりどころをまとめます。
📚 シリーズ記事
| 回 | タイトル | 主題 |
|---|---|---|
| 1 | 総論:脱Microsoft・OSS移行 | 動機・コスト・全体地図 |
| 2 | Windows 11 + Ubuntu デュアルブート構築 | M.2 物理分離・KDE Plasma・GRUB |
| 3 | Ubuntu PC セットアップ | NVIDIA・サスペンド・Howdy・プリンター |
| 4 | Ubuntu 開発環境セットアップ | Docker Engine ネイティブ・DevContainer・Claude Code |
| 5 | Ubuntu データ共有セットアップ | rclone Drive + systemd・QNAP NFS/SMB・Obsidian・AppArmor |
| 6 | Microsoft 365 → Google Workspace 全面移行 | メール・SSO・MFA・MX 切替・OneDrive → Drive |
| 7 | Ubuntu リモート開発セットアップ | Tailscale + SSH + VS Code Remote / DevContainer・Wayland ホスト |
| 8 | ActiveReports → Playwright + Scriban 移行 | 帳票エンジン置換・バーコード・印刷品質 PDF |
| 9 | Mac / Windows をリモートビルド機にする(本記事) | Ubuntu から SSH で出る・Windows OpenSSH Server・ローカル標準ユーザー・送信元限定の許可ルール |
| 10 | コスト削減効果と 1 年運用レビュー(公開予定) | 実績ベースのコスト・運用上のハマりどころ |
🔗 関連リソース
CTS-KB 内の関連記事
- Ubuntu リモート開発セットアップ — 逆向き。出先から Ubuntu へ Tailscale + SSH + VS Code Remote で入る
- Linux から Mac / Windows へ出して TDD する — 繋いだ先で何をするか。出先で初めて落ちるテスト、出先の緑を切替にしない
- 人が残る切替 — SSH で閉じない受入。実機・実紙・契約・並行稼働
- ActiveReports → Playwright + Scriban 移行 — Windows に残っていた帳票を Linux へ移した前回
- JAN出荷検品の端末選定 — BT-W85から.NET MAUIへ — Mac 側で回す MAUI 側の事例
- L2TP から Tailscale へ — Tailscale の基礎。Wi-Fi ルールが不要になる経路
関連用語
- Entra ID — Windows OpenSSH の認証に使えないアカウント種別
- .NET MAUI — Mac 側で iOS シミュレーターを回す対象
- WSL2 — 第 7 回の「WSL の鍵と Windows ネイティブの鍵は別物」。本記事では Windows 側に WSL を使わない
- DevContainer — Ubuntu 側の正本を回す環境
- NAT — Tailscale が越える前提。同じ机上なら越えない
外部リソース
- Microsoft — OpenSSH Server configuration for Windows — Entra アカウント非対応の記述、
DefaultShell - Microsoft — Windows での OpenSSH の使用開始 — 導入と許可ルール
- Microsoft — Key-based authentication in OpenSSH for Windows —
administrators_authorized_keysと ACL - Microsoft — New-LocalUser — ローカルユーザー作成
- Apple — Mac でリモートログインを許可する — リモートログインの有効化
- VS Code — Remote Development using SSH — 出先に IDE を開く場合