Task 04 — SSH Connectivity to Nodes
DOCUMENT CATEGORY: Implementation Guide SCOPE: Azure Local Cluster Nodes — Post-Deployment PURPOSE: Deploy the Open SSH for Windows Arc VM extension on each cluster node and configure Azure Arc SSH tunneled access (HybridConnectivity) so operators can reach nodes securely without opening inbound firewall ports or requiring direct network adjacency.
Overview
Azure Arc SSH routes connections through the HybridConnectivity relay — no public IP, no open inbound port 22 on upstream firewalls. The flow is:
Operator workstation → Azure Relay (HybridConnectivity) → Arc agent → WindowsOpenSSH (sshd)
This task installs OpenSSH using the first-party WindowsOpenSSH Arc VM extension
(publisher: Microsoft.Azure.OpenSSH, type: WindowsOpenSSH) — avoiding any WinRM/PSRemoting
dependency — and configures the HybridConnectivity endpoint on each Arc-enrolled node.
Each node must already be Arc-enrolled (completed in
Phase 01 — Arc Registration).
Arc agent version ≥ 1.31 is required. Check with azcmagent version on each node.
Extension Reference
| Field | Value |
|---|---|
| Display Name | Open SSH for Windows |
| Publisher | Microsoft.Azure.OpenSSH |
| Type | WindowsOpenSSH |
| Extension Name | WindowsOpenSSH |
| Installs | OpenSSH Server (sshd), sets service to Automatic, creates Windows Firewall rule |
Prerequisites
| Requirement | Details |
|---|---|
| Azure permissions | Contributor or Owner on the subscription (register RP + create endpoints) |
| Arc extension deploy | microsoft.hybridcompute/machines/extensions/write on each Arc machine resource |
| Azure CLI (scripts) | az CLI logged in (az account show); az extension add --name ssh |
| YAML key (orchestrated) | compute.nodes.<key>.arc_resource_id populated per node |
After this task, operators connecting via az ssh arc need the
Virtual Machine Local User Login role on each Arc machine resource.
RBAC assignments are out of scope for this task — assign at resource group level to avoid
hitting the per-subscription role assignment limit.
Variables from variables.yml
| Path | Type | Description |
|---|---|---|
compute.nodes.<key>.arc_resource_id | string | Arc machine resource ID per node |
compute.azure_local.arc_resource_group | string | Resource group |
azure_platform.subscriptions.lab.id | string | Subscription ID |
Execution Options
- Azure Portal
- Orchestrated Script (Mgmt Server)
- Standalone Script
When to use:
- Ad-hoc installs or verifying extension state on individual nodes
- No scripting environment available
- Post-deployment check or re-install of a specific node
Step 1 — Install the WindowsOpenSSH extension (per node)
Repeat for each cluster node.
- Open the Azure portal and navigate to Azure Arc → Machines
- Select the Arc machine for the first node (e.g.
azl-demo-01-n01) - In the left menu under Settings, select Extensions
- Click + Add
- Find and select Open SSH for Windows (publisher:
Microsoft.Azure.OpenSSH) - Click Next → accept defaults → Review + create → Create
- Wait for the extension to show Succeeded in the Extensions list
- Repeat steps 2–7 for every cluster node
After install, confirm the extension state in the portal Extensions blade shows
ProvisioningState: Succeeded. You can also query via CLI:
az connectedmachine extension show `
--resource-group "rg-c01-azl-eus-01" `
--machine-name "azl-demo-01-n01" `
--name "WindowsOpenSSH"
Step 2 — Configure HybridConnectivity (once per subscription + per node)
2a — Register the resource provider (one-time per subscription):
# Register RP (one-time per subscription)
az provider register -n Microsoft.HybridConnectivity
az extension add --name ssh
# Poll until registered
do {
Start-Sleep 15
$state = az provider show -n Microsoft.HybridConnectivity --query registrationState -o tsv
Write-Host "RP state: $state"
} until ($state -eq 'Registered')
# Run the following block once per node — change $machine each time
$sub = "5e04c7f2-0298-439a-ab56-6d81d6bdc796"
$rg = "rg-c01-azl-eus-01"
$machine = "azl-demo-01-n01" # <-- change per node
# Create default connectivity endpoint
az rest `
--method put `
--uri "https://management.azure.com/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.HybridCompute/machines/$machine/providers/Microsoft.HybridConnectivity/endpoints/default?api-version=2023-03-15" `
--body '{\"properties\":{\"type\":\"default\"}}'
# Configure SSH service on the endpoint
az rest `
--method put `
--uri "https://management.azure.com/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.HybridCompute/machines/$machine/providers/Microsoft.HybridConnectivity/endpoints/default/serviceconfigurations/SSH?api-version=2023-03-15" `
--body '{\"properties\":{\"serviceName\":\"SSH\",\"port\":22}}'
Connecting:
az ssh arc `
--resource-group "rg-c01-azl-eus-01" `
--name "azl-demo-01-n01" `
--local-user "DOMAIN\username"
When to use:
- Initial deployment across all nodes from the management server
- Running inside a CI/CD pipeline
- Consistent, logged execution using the YAML config as the single source of truth
Prerequisites:
az account show # Must be logged in
az extension add --name ssh
Script location:
scripts/deploy/04-cluster-deployment/phase-06-post-deployment/
task-04-ssh-connectivity-to-nodes/
powershell/
Invoke-SSHConnectivity-Orchestrated.ps1
Script:
#Requires -Version 5.1
<#
.SYNOPSIS
Deploys the WindowsOpenSSH Arc extension and configures HybridConnectivity
SSH access on all Azure Local cluster nodes.
.DESCRIPTION
Phase 06 — Post-Deployment | Task 04 — SSH Connectivity to Nodes
For each node in variables.yml:
1. Deploys the WindowsOpenSSH Arc VM extension via az connectedmachine extension create
2. Registers Microsoft.HybridConnectivity RP (once per subscription)
3. Creates the HybridConnectivity default endpoint
4. Configures the SSH service on the endpoint
After this task, operators connect with:
az ssh arc | Enter-AzVM (Az.Ssh module)
No WinRM/PSRemoting required — the Arc agent handles the OpenSSH install.
.PARAMETER ConfigPath
Path to infrastructure YAML config. Defaults to config/variables.yml
relative to CWD (repo root).
.PARAMETER TargetNode
Limit to specific node key names. Empty = all nodes in config.
.PARAMETER WhatIf
Log planned actions without making changes.
.PARAMETER LogPath
Override log directory. Defaults to logs\task-04-ssh-connectivity-to-nodes\ in CWD.
.PARAMETER SshdPort
Port to configure on the HybridConnectivity SSH endpoint. Default: 22.
.NOTES
Run from the repo root.
Requires: az CLI logged in with Contributor/Owner on the subscription.
Requires: powershell-yaml module (Install-Module powershell-yaml)
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[string] $ConfigPath = "",
[string[]] $TargetNode = @(),
[switch] $WhatIf,
[string] $LogPath = "",
[int] $SshdPort = 22
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
#region LOGGING -----------------------------------------------------------------
$taskFolderName = "task-04-ssh-connectivity-to-nodes"
$timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
if ([string]::IsNullOrEmpty($LogPath)) {
$LogPath = Join-Path (Get-Location).Path "logs\$taskFolderName"
}
if (-not (Test-Path $LogPath)) { New-Item -ItemType Directory -Path $LogPath -Force | Out-Null }
$logFile = Join-Path $LogPath "${timestamp}_SSH-Connectivity.log"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$entry = "[{0}] [{1,-5}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level, $Message
$entry | Tee-Object -FilePath $logFile -Append | Out-Null
$color = switch ($Level) {
"ERROR" { "Red" }; "WARN" { "Yellow" }; "OK" { "Green" }; default { "Cyan" }
}
Write-Host $entry -ForegroundColor $color
}
#endregion
#region CONFIG LOADING ----------------------------------------------------------
if ([string]::IsNullOrEmpty($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path "config\variables.yml"
}
if (-not (Test-Path $ConfigPath)) {
Write-Log "Config not found: $ConfigPath" -Level "ERROR"; throw "Config not found: $ConfigPath"
}
Write-Log "Loading config: $ConfigPath"
Import-Module powershell-yaml -ErrorAction Stop
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Yaml
#endregion
#region AZ CLI CHECK ------------------------------------------------------------
Write-Log "Verifying Azure CLI authentication"
$azSub = az account show --query id -o tsv 2>$null
if (-not $azSub) {
Write-Log "Not logged in to Azure CLI. Run: az login --use-device-code" -Level "ERROR"
throw "az CLI not authenticated"
}
Write-Log "Authenticated — subscription context: $azSub" -Level "OK"
#endregion
#region NODE LIST ---------------------------------------------------------------
$allNodes = @($cfg.compute.nodes.GetEnumerator()) # compute.nodes
if ($TargetNode.Count -gt 0) {
$allNodes = $allNodes | Where-Object { $_.Key -in $TargetNode }
Write-Log "Filtered to $($allNodes.Count) node(s): $($TargetNode -join ', ')"
} else {
Write-Log "Processing all $($allNodes.Count) node(s) from config"
}
if ($allNodes.Count -eq 0) { Write-Log "No matching nodes — exiting" -Level "WARN"; exit 0 }
#endregion
#region HYBRIDCONNECTIVITY RP (once per subscription) ---------------------------
# Parse subscription from first node's arc_resource_id
$firstArcId = $allNodes[0].Value.arc_resource_id # compute.nodes.<key>.arc_resource_id
$subId = ($firstArcId -split '/')[2]
Write-Log "Checking HybridConnectivity RP on subscription: $subId"
if (-not $WhatIf) {
az account set --subscription $subId | Out-Null
$rpState = az provider show -n Microsoft.HybridConnectivity --query registrationState -o tsv 2>$null
if ($rpState -ne 'Registered') {
Write-Log "Registering Microsoft.HybridConnectivity RP — may take 2-5 minutes"
az provider register -n Microsoft.HybridConnectivity | Out-Null
$retries = 0
do {
Start-Sleep -Seconds 15
$rpState = az provider show -n Microsoft.HybridConnectivity --query registrationState -o tsv 2>$null
Write-Log " RP state: $rpState (attempt $($retries+1)/20)"
$retries++
} until ($rpState -eq 'Registered' -or $retries -ge 20)
if ($rpState -ne 'Registered') { throw "HybridConnectivity RP registration timed out" }
}
Write-Log "HybridConnectivity RP: Registered" -Level "OK"
} else {
Write-Log "[WHATIF] Would register Microsoft.HybridConnectivity RP on subscription $subId if needed"
}
#endregion
#region GET LOCATION FROM FIRST NODE RESOURCE GROUP ----------------------------
# az connectedmachine extension create requires --location (must match the Arc machine's region)
$firstRg = ($firstArcId -split '/')[4]
$location = az group show --name $firstRg --query location -o tsv 2>$null
if (-not $location) { $location = $cfg.azure_platform.region } # azure_platform.region fallback
Write-Log "Arc resource region: $location"
#endregion
#region PROCESS NODES -----------------------------------------------------------
$results = [System.Collections.Generic.List[PSObject]]::new()
foreach ($nodeEntry in $allNodes) {
$arcResourceId = $nodeEntry.Value.arc_resource_id # compute.nodes.<key>.arc_resource_id
$hostname = $nodeEntry.Value.hostname # compute.nodes.<key>.hostname
$arcParts = $arcResourceId -split '/'
$nodeSub = $arcParts[2]
$nodeRg = $arcParts[4]
$machineName = $arcParts[-1]
Write-Log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Write-Log "Node: $hostname | Arc machine: $machineName | RG: $nodeRg"
$result = [PSCustomObject]@{
Node = $hostname
ExtensionOK = $false
EndpointOK = $false
SshServiceOK = $false
Errors = [System.Collections.Generic.List[string]]::new()
}
#-- Step 1: Deploy WindowsOpenSSH Arc extension --------------------------------
Write-Log "[$hostname] Step 1/3 — Deploy WindowsOpenSSH Arc extension"
if (-not $WhatIf) {
try {
# Idempotent: if already installed, show will succeed; create is also idempotent
$extState = az connectedmachine extension show `
--resource-group $nodeRg `
--machine-name $machineName `
--name "WindowsOpenSSH" `
--query "provisioningState" `
-o tsv 2>$null
if ($extState -eq 'Succeeded') {
Write-Log "[$hostname] WindowsOpenSSH extension already installed" -Level "OK"
$result.ExtensionOK = $true
} else {
Write-Log "[$hostname] Installing WindowsOpenSSH extension (this may take 2-3 minutes)"
az connectedmachine extension create `
--resource-group $nodeRg `
--machine-name $machineName `
--name "WindowsOpenSSH" `
--publisher "Microsoft.Azure.OpenSSH" `
--type "WindowsOpenSSH" `
--location $location | Out-Null
# Wait for provisioning
$retries = 0
do {
Start-Sleep -Seconds 20
$extState = az connectedmachine extension show `
--resource-group $nodeRg --machine-name $machineName `
--name "WindowsOpenSSH" --query "provisioningState" -o tsv 2>$null
Write-Log "[$hostname] Extension state: $extState (attempt $($retries+1)/15)"
$retries++
} until ($extState -in @('Succeeded','Failed') -or $retries -ge 15)
if ($extState -eq 'Succeeded') {
$result.ExtensionOK = $true
Write-Log "[$hostname] WindowsOpenSSH extension installed" -Level "OK"
} else {
$result.Errors.Add("Extension provisioning state: $extState")
Write-Log "[$hostname] Extension install did not succeed: $extState" -Level "ERROR"
}
}
} catch {
$result.Errors.Add("Extension: $($_.Exception.Message)")
Write-Log "[$hostname] Extension install failed: $($_.Exception.Message)" -Level "ERROR"
}
} else {
Write-Log "[$hostname] [WHATIF] Would deploy WindowsOpenSSH extension via az connectedmachine extension create"
$result.ExtensionOK = $true
}
#-- Step 2: HybridConnectivity default endpoint --------------------------------
Write-Log "[$hostname] Step 2/3 — Create HybridConnectivity default endpoint"
$epUri = "https://management.azure.com/subscriptions/$nodeSub/resourceGroups/$nodeRg/providers/Microsoft.HybridCompute/machines/$machineName/providers/Microsoft.HybridConnectivity/endpoints/default?api-version=2023-03-15"
if (-not $WhatIf) {
try {
az rest --method put --uri $epUri --body '{\"properties\":{\"type\":\"default\"}}' | Out-Null
$result.EndpointOK = $true
Write-Log "[$hostname] HybridConnectivity default endpoint created/verified" -Level "OK"
} catch {
$result.Errors.Add("Endpoint: $($_.Exception.Message)")
Write-Log "[$hostname] Endpoint creation failed: $($_.Exception.Message)" -Level "ERROR"
}
} else {
Write-Log "[$hostname] [WHATIF] Would PUT default endpoint: $epUri"
$result.EndpointOK = $true
}
#-- Step 3: SSH service configuration ------------------------------------------
Write-Log "[$hostname] Step 3/3 — Configure SSH service on endpoint (port $SshdPort)"
$svcUri = "https://management.azure.com/subscriptions/$nodeSub/resourceGroups/$nodeRg/providers/Microsoft.HybridCompute/machines/$machineName/providers/Microsoft.HybridConnectivity/endpoints/default/serviceconfigurations/SSH?api-version=2023-03-15"
if (-not $WhatIf) {
try {
az rest --method put --uri $svcUri `
--body "{\"properties\":{\"serviceName\":\"SSH\",\"port\":$SshdPort}}" | Out-Null
$result.SshServiceOK = $true
Write-Log "[$hostname] SSH service configured on port $SshdPort" -Level "OK"
} catch {
$result.Errors.Add("SshService: $($_.Exception.Message)")
Write-Log "[$hostname] SSH service config failed: $($_.Exception.Message)" -Level "ERROR"
}
} else {
Write-Log "[$hostname] [WHATIF] Would PUT SSH service config (port $SshdPort): $svcUri"
$result.SshServiceOK = $true
}
$results.Add($result)
}
#endregion
#region SUMMARY -----------------------------------------------------------------
Write-Log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Write-Log "SUMMARY"
$ok = @($results | Where-Object { $_.Errors.Count -eq 0 })
$failed = @($results | Where-Object { $_.Errors.Count -gt 0 })
Write-Log "Succeeded: $($ok.Count) / $($results.Count)" -Level "OK"
$failed | ForEach-Object {
Write-Log "FAILED: $($_.Node)" -Level "ERROR"
$_.Errors | ForEach-Object { Write-Log " $_" -Level "ERROR" }
}
Write-Log "Log: $logFile"
#endregion
Connecting:
# Azure CLI
az ssh arc `
--resource-group "rg-c01-azl-eus-01" `
--name "azl-demo-01-n01" `
--local-user "DOMAIN\username"
# Az.Ssh PowerShell module (Install-Module Az.Ssh, Az.Ssh.ArcProxy)
Enter-AzVM `
-ResourceGroupName "rg-c01-azl-eus-01" `
-Name "azl-demo-01-n01" `
-LocalUser "username"
When to use:
- Configuring a single site without the full toolkit repo checked out
- Re-running after a node replacement
- Troubleshooting or re-enabling Arc SSH access on specific nodes
Prerequisites:
az account show # must be logged in
az extension add --name ssh
Script:
Fill in the #region CONFIGURATION block with your environment values, then run from
any machine with az CLI access.
#Requires -Version 5.1
<#
.SYNOPSIS
Standalone: deploys the WindowsOpenSSH Arc extension and configures
Azure Arc SSH (HybridConnectivity) on Azure Local cluster nodes
without requiring the toolkit YAML config.
#>
[CmdletBinding(SupportsShouldProcess)]
param([switch]$WhatIf)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
#region CONFIGURATION -----------------------------------------------------------
# Replace all values for your environment.
# Example values use the fictional "Infinite azurelocal Corp" (prefix: iic).
# Azure subscription ID containing the Arc-enrolled nodes
[string]$SubscriptionId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# Resource group containing the Arc machine resources
[string]$ArcResourceGroup = 'rg-iic01-azl-eus-01'
# Azure region of the Arc resource group (must match Arc machine region)
[string]$Location = 'eastus'
# SSH port (almost always 22)
[int]$SshdPort = 22
# Arc machine names (must match the HybridCompute machine resource names in Azure)
[string[]]$ArcMachineNames = @(
'iic-01-n01',
'iic-01-n02'
)
#endregion
#region LOGGING -----------------------------------------------------------------
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$entry = "[{0}] [{1,-5}] {2}" -f (Get-Date -Format "HH:mm:ss"), $Level, $Message
$color = switch ($Level) {
"ERROR" { "Red" }; "WARN" { "Yellow" }; "OK" { "Green" }; default { "Cyan" }
}
Write-Host $entry -ForegroundColor $color
}
#endregion
Write-Log "SSH Connectivity — Standalone"
Write-Log "Subscription : $SubscriptionId"
Write-Log "Arc RG : $ArcResourceGroup"
Write-Log "Nodes : $($ArcMachineNames -join ', ')"
if ($WhatIf) { Write-Log "[WHATIF MODE — no changes will be made]" -Level "WARN" }
#region AZ CLI CHECK ------------------------------------------------------------
$azSub = az account show --query id -o tsv 2>$null
if (-not $azSub) {
Write-Log "Azure CLI not authenticated. Run: az login --use-device-code" -Level "ERROR"
throw "az not authenticated"
}
if (-not $WhatIf) { az account set --subscription $SubscriptionId | Out-Null }
Write-Log "Azure CLI context: $SubscriptionId" -Level "OK"
#endregion
#region HYBRIDCONNECTIVITY RP ---------------------------------------------------
Write-Log "Checking HybridConnectivity RP registration"
if (-not $WhatIf) {
$rpState = az provider show -n Microsoft.HybridConnectivity --query registrationState -o tsv 2>$null
if ($rpState -ne 'Registered') {
Write-Log "Registering Microsoft.HybridConnectivity — waiting up to 5 minutes"
az provider register -n Microsoft.HybridConnectivity | Out-Null
$retries = 0
do {
Start-Sleep 15
$rpState = az provider show -n Microsoft.HybridConnectivity --query registrationState -o tsv 2>$null
Write-Log " RP state: $rpState (attempt $($retries+1)/20)"
$retries++
} until ($rpState -eq 'Registered' -or $retries -ge 20)
if ($rpState -ne 'Registered') { throw "HybridConnectivity RP registration timed out" }
}
Write-Log "HybridConnectivity RP: Registered" -Level "OK"
} else {
Write-Log "[WHATIF] Would register HybridConnectivity RP if not already registered"
}
#endregion
#region PROCESS NODES -----------------------------------------------------------
foreach ($machineName in $ArcMachineNames) {
Write-Log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Write-Log "Node: $machineName"
# Step 1 — Deploy WindowsOpenSSH extension
Write-Log "[$machineName] Step 1/3 — Deploy WindowsOpenSSH Arc extension"
if (-not $WhatIf) {
try {
$extState = az connectedmachine extension show `
--resource-group $ArcResourceGroup --machine-name $machineName `
--name "WindowsOpenSSH" --query "provisioningState" -o tsv 2>$null
if ($extState -eq 'Succeeded') {
Write-Log "[$machineName] Already installed" -Level "OK"
} else {
Write-Log "[$machineName] Installing (2-3 minutes)..."
az connectedmachine extension create `
--resource-group $ArcResourceGroup `
--machine-name $machineName `
--name "WindowsOpenSSH" `
--publisher "Microsoft.Azure.OpenSSH" `
--type "WindowsOpenSSH" `
--location $Location | Out-Null
$retries = 0
do {
Start-Sleep 20
$extState = az connectedmachine extension show `
--resource-group $ArcResourceGroup --machine-name $machineName `
--name "WindowsOpenSSH" --query "provisioningState" -o tsv 2>$null
Write-Log "[$machineName] State: $extState (attempt $($retries+1)/15)"
$retries++
} until ($extState -in @('Succeeded','Failed') -or $retries -ge 15)
Write-Log "[$machineName] Extension: $extState" -Level $(if ($extState -eq 'Succeeded') {"OK"} else {"ERROR"})
}
} catch {
Write-Log "[$machineName] Extension install failed: $($_.Exception.Message)" -Level "ERROR"
}
} else {
Write-Log "[$machineName] [WHATIF] Would deploy WindowsOpenSSH extension"
}
# Step 2 — HybridConnectivity endpoint
Write-Log "[$machineName] Step 2/3 — Create HybridConnectivity default endpoint"
$epUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ArcResourceGroup/providers/Microsoft.HybridCompute/machines/$machineName/providers/Microsoft.HybridConnectivity/endpoints/default?api-version=2023-03-15"
if (-not $WhatIf) {
try {
az rest --method put --uri $epUri --body '{\"properties\":{\"type\":\"default\"}}' | Out-Null
Write-Log "[$machineName] Endpoint created/verified" -Level "OK"
} catch {
Write-Log "[$machineName] Endpoint PUT failed: $($_.Exception.Message)" -Level "ERROR"
}
} else { Write-Log "[$machineName] [WHATIF] Would PUT: $epUri" }
# Step 3 — SSH service config
Write-Log "[$machineName] Step 3/3 — Configure SSH service on endpoint (port $SshdPort)"
$svcUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ArcResourceGroup/providers/Microsoft.HybridCompute/machines/$machineName/providers/Microsoft.HybridConnectivity/endpoints/default/serviceconfigurations/SSH?api-version=2023-03-15"
if (-not $WhatIf) {
try {
az rest --method put --uri $svcUri `
--body "{\"properties\":{\"serviceName\":\"SSH\",\"port\":$SshdPort}}" | Out-Null
Write-Log "[$machineName] SSH service enabled on port $SshdPort" -Level "OK"
} catch {
Write-Log "[$machineName] SSH service config failed: $($_.Exception.Message)" -Level "ERROR"
}
} else { Write-Log "[$machineName] [WHATIF] Would PUT SSH service config (port $SshdPort)" }
}
#endregion
Write-Log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Write-Log "Done. Connect with: az ssh arc --resource-group $ArcResourceGroup --name <node> --local-user <user>" -Level "OK"
Connecting:
az ssh arc `
--resource-group 'rg-iic01-azl-eus-01' `
--name 'iic-01-n01' `
--local-user 'svc-azlocal-admin'
Validation
1 — Verify extension state on all nodes
$sub = "5e04c7f2-0298-439a-ab56-6d81d6bdc796"
$rg = "rg-c01-azl-eus-01"
foreach ($machine in @("azl-demo-01-n01", "azl-demo-01-n02")) {
$state = az connectedmachine extension show `
--resource-group $rg `
--machine-name $machine `
--name "WindowsOpenSSH" `
--query "provisioningState" `
-o tsv
Write-Host "$machine WindowsOpenSSH = $state" -ForegroundColor $(if ($state -eq 'Succeeded') {'Green'} else {'Red'})
}
2 — Verify HybridConnectivity SSH endpoint
$sub = "5e04c7f2-0298-439a-ab56-6d81d6bdc796"
$rg = "rg-c01-azl-eus-01"
foreach ($machine in @("azl-demo-01-n01", "azl-demo-01-n02")) {
$uri = "https://management.azure.com/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.HybridCompute/machines/$machine/providers/Microsoft.HybridConnectivity/endpoints/default/serviceconfigurations/SSH?api-version=2023-03-15"
Write-Host "`nNode: $machine"
az rest --method get --uri $uri | ConvertFrom-Json | Select-Object -ExpandProperty properties
}
Expected output per node:
{
"serviceName": "SSH",
"port": 22,
"resourceProvisioningState": "Succeeded"
}
3 — Test an Arc SSH connection
az ssh arc `
--resource-group "rg-c01-azl-eus-01" `
--name "azl-demo-01-n01" `
--local-user "DOMAIN\username" `
--command "hostname"
Validation Summary
| Check | Command | Expected |
|---|---|---|
| Extension installed | az connectedmachine extension show ... --query provisioningState | Succeeded |
| HybridConnectivity RP | az provider show -n Microsoft.HybridConnectivity --query registrationState -o tsv | Registered |
| SSH endpoint | az rest --method get --uri .../serviceconfigurations/SSH | resourceProvisioningState: Succeeded |
| Arc SSH connect | az ssh arc ... --command hostname | Returns node hostname |
RBAC — Operator Access
The WindowsOpenSSH extension and HybridConnectivity endpoint control infrastructure.
Each operator who needs to CONNECT via Arc SSH also needs the Virtual Machine Local User Login
role on the Arc machine resources (or the containing resource group):
$sub = "5e04c7f2-0298-439a-ab56-6d81d6bdc796"
$rg = "rg-c01-azl-eus-01"
$objectId = "<operator-or-group-entra-object-id>"
az role assignment create `
--role "Virtual Machine Local User Login" `
--assignee $objectId `
--scope "/subscriptions/$sub/resourceGroups/$rg"
Microsoft Entra ID SSH login (AADSSHLoginForLinux) is Linux-only. On Windows nodes,
sshd authenticates with Windows credentials (local or domain accounts). The
Virtual Machine Local User Login RBAC role only controls who can open the
HybridConnectivity tunnel — not who can authenticate to sshd itself.
Troubleshooting
| Issue | Cause | Resolution |
|---|---|---|
| SSH connection refused on port 22 | sshd service not running or Windows Firewall blocking | Start service: Start-Service sshd; Set-Service sshd -StartupType Automatic; verify firewall: Get-NetFirewallRule -Name *ssh* |
| Arc SSH tunnel fails with RBAC error | User lacks Virtual Machine Local User Login role | Assign the role: az role assignment create --role "Virtual Machine Local User Login" --assignee <objectId> --scope <resourceScope> |
| Authentication fails with valid credentials | sshd_config not configured for password or key auth | Edit C:\ProgramData\ssh\sshd_config to enable PasswordAuthentication yes or configure authorized keys; restart: Restart-Service sshd |
Navigation
| Previous | Up | Next |
|---|---|---|
| Task 3: Security Groups | Phase 06 Index | Task 5: Storage Configuration |