🙌

マルチモニタをオフにして Bastion で RDP を使う

2022/10/27に公開

Bastion が規定でマルチモニタ有効化されるのが辛い

というのがありまして、GitHub でも議論がなされています。
現在では状況が少し変わって、なんか Azure CLI 側には --configure という option がついたようで close されてますね。

https://github.com/Azure/azure-cli/issues/23866

じゃあ PowerShell function にしちゃいましょうねという話

いろいろ GitHub 上でやり取りした結果を PowerShell Function にして profile に追加してしまおうかと思います。
profile はたとえば手元のマシンでは OneDrive for Business が有効化されているのでこちらのフォルダにありました。

"C:\Users\xxxxxxxx\OneDrive - Microsoft\Documents\PowerShell\profile.ps1"

たぶん環境によるので以下の URL を見てください。

https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_profiles

その profile.ps1 的なやつに以下の PowerShell の code を追加します。
必須ではない parameter として $VMResourceGroupName がありますが、Azure VM が Azure Bastion と異なる Resource group の場合には指定します。
同じ場合には省略すれば $VMResourceGroupName$ResourceGroupName と同一とします。

Function Connect-MyAzBastionRdp() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)] [string] $VMName,
        [string] $VMResourceGroupName,
        [Parameter(Mandatory = $true)] [string] $ResourceGroupName,
        [Parameter(Mandatory = $true)] [string] $BastionName
    )

    if ([string]::IsNullOrEmpty($VMResourceGroupName)) {
        $VMResourceGroupName = $ResourceGroupName
    }

    @('Az.Accounts', 'Az.Network') | Import-Module
    $SubscriptionId = (Get-AzContext).Subscription.Id
    $VMResourceID = "/subscriptions/$SubscriptionId/resourceGroups/$VMResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName"
    $BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName

    $Params = @{
        Method  = 'GET'
        Uri     = "https://$BastionDnsName/api/rdpfile?resourceId=$VMResourceId&format=rdp"
        Headers = @{
            'Content-Type'  = 'application/json'
            'Authorization' = "Bearer $((Get-AzAccessToken).Token)"
        }
    }

    $RdpExclude = @('use multimon', 'signscope', 'signature')
    $RdpFile = (Invoke-RestMethod @Params).Split("`n") | Where-Object {
        if (-Not [string]::IsNullOrEmpty($_)) {
            ($Name, $Type, $Value) = $_.Split(':', 3, [System.StringSplitOptions]::TrimEntries)
            (-Not $RdpExclude.Contains($Name))
        }
    }

    $RdpFilePath = Join-Path $Env:TEMP "$VMName.rdp"
    @('screen mode id:i:1', 'desktopwidth:i:1920', 'desktopheight:i:1080', 'smart sizing:i:1', $RdpFile) | Out-File $RdpFilePath
    & 'mstsc.exe' $RdpFilePath
}

これにより、以下のような cmdlet が使えるようになります。

Connect-MyAzBastionRdp -VMName vm-ad01 -ResourceGroupName active-directory -BastionName bast-hub00

My という部分が独自 cmdlet を示している気持ちで作っていますが、別にどんな名前にしても大丈夫です。

Workaround を GitHub の issue で共有したらもっと改善されたのが来た

過去の内容の記録なのでしまっちゃいます

長くはなったけどわかりやすいっすね。
すばらしー!

$VMName = "RDM-WOA"
$ResourceGroupName = "TestWoa"
$BastionName = "TestWoA-vnet-bastion"

@('Az.Accounts','Az.Network') | Import-Module
$SubscriptionId = (Get-AzContext).Subscription.Id
$VMResourceID = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines/$VMName"
$BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName

$Params = @{
    Method = 'GET'
    Uri = "https://$BastionDnsName/api/rdpfile?resourceId=$VMResourceId&format=rdp"
    Headers = @{
        'Content-Type' = 'application/json'
        'Authorization' = "Bearer $((Get-AzAccessToken).Token)"
    }
}

$RdpExclude = @('use multimon','signscope','signature')
$RdpFile = (Invoke-RestMethod @Params).Split("`n") | Where-Object {
    if (-Not [string]::IsNullOrEmpty($_)) {
        ($Name, $Type, $Value) = $_.Split(':', 3, [System.StringSplitOptions]::TrimEntries)
        (-Not $RdpExclude.Contains($Name))
    }
}

$RdpFilePath = Join-Path $Env:TEMP "$VMName.rdp"
$RdpFile | Out-File $RdpFilePath
& 'mstsc.exe' $RdpFilePath

https://github.com/Azure/azure-cli/issues/23866#issuecomment-1295295069

わーくあらうんど

過去の内容の記録なのでしまっちゃいます

叩いてる API の様子を Fiddler で眺めていたら、以下のような感じでマルチモニタをオフにしつつ、つなげられるようになりました。
ポイントとしては Get-AzAccessToken で JWT をもらってきて、それを使って Bastion の API を叩いて RDP ファイルをもらい、手元で少し編集した後 mstsc に食わせる、という感じです。

$ResourceGroupName = "xxxxxxxx"
$BastionName = "bast-xxxxxxxx"
$ResourceId = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/xxxxxxxx/providers/Microsoft.Compute/virtualMachines/vm-xxxxxxxx"
$RdpFileName = "$($ResourceId.Split("/")[8]).rdp"

$Token = (Get-AzAccessToken).Token
$Headers = @{
    'Content-Type'  = 'application/json'
    'Authorization' = "Bearer $token"
}

$BastionDnsName = (Get-AzBastion -ResourceGroupName $ResourceGroupName -Name $BastionName).DnsName
$(Invoke-RestMethod -Method Get `
    -Uri "https://$BastionDnsName/api/rdpfile?resourceId=$ResourceId&format=rdp" `
    -Headers $headers).Replace("use multimon:i:1","") | Out-File $RdpFileName
mstsc $RdpFileName
# Remove-Item $RdpFileName

あと、地味にうれしいのは az network bastion rdp を叩いた Terminal で prompt が返ってこないのに対し、こちらの場合には prompt 返ってくるのでその Terminal がそのまま使えます。

注意事項

一応 API に沿ってるので問題ないかなとは思いますが、使えなくなっても何も保証はないです。

参考

  • Get-AzAccessToken
    Azure AD の JWT がこんなに簡単に取れるとは思ってもみなかった

https://learn.microsoft.com/powershell/module/az.accounts/get-azaccesstoken?view=azps-9.0.1

  • az network bastion rdp

https://learn.microsoft.com/cli/azure/network/bastion?#az-network-bastion-rdp

  • リモート デスクトップ サービスでサポートされる RDP プロパティ

https://learn.microsoft.com/windows-server/remote/remote-desktop-services/clients/rdp-files

  • PowerShell だけで (Azure CLI を使わずに) Bastion を Native Client で RDP する

https://zenn.dev/skmkzyk/articles/bastion-rdp-powershell-only

  • Remote Desktop したときに、最大化してるんだけど Window は小さくしたい

https://zenn.dev/skmkzyk/articles/remote-desktop-smart-sizing

Microsoft (有志)

Discussion