CTS-KB
情シス・社内IT📚 シリーズ 9/10

Mac / Windows をリモートビルド機にする — Ubuntu から SSH で出て MAUI・WPF を確認する

⏱ 約 16 分で読めます
#脱Microsoft#Ubuntu#SSH#OpenSSH#Windows 11#macOS#PowerShell#リモート開発#.NET MAUI#WPF#Entra ID#ファイアウォール#情シス

🎯 はじめに:この記事のスコープ

シリーズ「脱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 つの罠

🧭 結論:向きが逆でも土台は同じ、違うのは Windows 側の 3 つの罠

Windows 11(出先)

Mac(出先)

Ubuntu 開発機(正本)

ssh mac

ssh win

専用 Ed25519 秘密鍵(ここから出さない)

~/.ssh/configHost mac / Host win

リモートログインauthorized_keys

Xcode / iOS シミュレーター

許可ルール送信元 = Ubuntu だけ

OpenSSH Serverローカル標準ユーザー

WPF / WebView2

層MacWindows 11共通の方針
経路同じ Wi-Fi、または Tailscale同じ Wi-Fi、または Tailscale同じ机上のビルド機なので Tailscale 必須にはしない。Wi-Fi が Public でも ネットワーク種別は変えない
認証通常ユーザーの ~/.ssh/authorized_keysローカル標準ユーザーの .ssh\authorized_keys秘密鍵は Ubuntu にだけ置く。個人鍵は使い回さない
権限普段のユーザーで可ローカル標準ユーザーを SSH 専用に作る管理者にしない。SDK は管理者が入れ、ビルドは標準ユーザーで回す

Mac は「リモートログイン」を有効にして公開鍵を置くだけです。手間の 9 割は Windows 側で、しかも詰まる場所が決まっています。

  1. Entra ID のアカウントでは Windows OpenSSH の認証が通らない(公開鍵でもパスワードでも)。普段のサインインは Entra ID のまま、SSH 専用のローカルユーザーを足す
  2. 管理者ユーザーだと authorized_keys が無視される。読まれるのは C:\ProgramData\ssh\administrators_authorized_keys で、ACL の条件もある。標準ユーザーならこの罠ごと消える
  3. 導入時に作られる許可ルールは 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 ModeMac + 端末
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 failuresUbuntu 側に個人鍵が複数あり、順に試している~/.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移行動機・コスト・全体地図
2Windows 11 + Ubuntu デュアルブート構築M.2 物理分離・KDE Plasma・GRUB
3Ubuntu PC セットアップNVIDIA・サスペンド・Howdy・プリンター
4Ubuntu 開発環境セットアップDocker Engine ネイティブ・DevContainer・Claude Code
5Ubuntu データ共有セットアップrclone Drive + systemd・QNAP NFS/SMB・Obsidian・AppArmor
6Microsoft 365 → Google Workspace 全面移行メール・SSO・MFA・MX 切替・OneDrive → Drive
7Ubuntu リモート開発セットアップTailscale + SSH + VS Code Remote / DevContainer・Wayland ホスト
8ActiveReports → Playwright + Scriban 移行帳票エンジン置換・バーコード・印刷品質 PDF
9Mac / Windows をリモートビルド機にする(本記事)Ubuntu から SSH で出る・Windows OpenSSH Server・ローカル標準ユーザー・送信元限定の許可ルール
10コスト削減効果と 1 年運用レビュー(公開予定)実績ベースのコスト・運用上のハマりどころ

🔗 関連リソース

CTS-KB 内の関連記事

関連用語

  • Entra ID — Windows OpenSSH の認証に使えないアカウント種別
  • .NET MAUI — Mac 側で iOS シミュレーターを回す対象
  • WSL2 — 第 7 回の「WSL の鍵と Windows ネイティブの鍵は別物」。本記事では Windows 側に WSL を使わない
  • DevContainer — Ubuntu 側の正本を回す環境
  • NAT — Tailscale が越える前提。同じ机上なら越えない

外部リソース