WAC vMode Behind Entra App Proxy

Reach the new Windows Admin Center from anywhere, with Entra sign-in and MFA in front of it and no VPN

Table of Contents

Preface

Windows Admin Center Virtualization Mode (WAC vMode) is Microsoft’s new web console for managing Hyper-V clusters, currently in public preview. It’s a big step up from the Administration Mode WAC you’ve probably used before, and I’ll do more on it once it goes GA.

So you’ve got your shiny new Hyper-V cluster and WAC vMode all set up, then your friend with Azure Local comes over and says “but I can manage my cluster from anywhere, securely, without a VPN.” Well, guess what? You can too, and you probably already have everything you need.

This all started when I was sitting on an unnamed campus near Seattle and wanted to spin up a VM on one of my clusters, but they blocked EVERYTHING: UniFi Teleport, Tailscale, you name it. What don’t they block? Microsoft Entra. One of Entra ID’s best-kept secrets is Entra application proxy. It’s simple to set up, puts your Conditional Access policies in front of the app, supports high availability with more than one connector, and only needs Entra ID P1.

It mostly works out of the box. Two settings, one in Entra and one on the WAC gateway, are the difference between a working dashboard and an “Unauthorized Access (Expired)” page a few seconds after you sign in.

How it works

Application proxy flow: the user signs in to Microsoft Entra ID, the Application Proxy Service passes the request to the connector, and the connector forwards it to the on-premises web server
Diagram: Microsoft Learn, CC BY 4.0
  1. The browser goes to the external URL, and the Application Proxy Service sends it to Microsoft Entra ID to sign in.
  2. Entra checks the sign-in against Conditional Access (MFA here) and hands back a token.
  3. The browser comes back to the Application Proxy Service with the token.
  4. (Only for Kerberos single sign-on, which this setup doesn’t use.) The connector gets a Kerberos ticket from AD for the user.
  5. The connector, which already holds an outbound connection to the service, forwards the request to the internal URL: the WAC gateway.
  6. The response goes back the same way.

The connector only makes outbound connections, so nothing on your network accepts inbound traffic.

What you need

  • WAC vMode installed on a server. I used the public preview 2.8.2.8 for this.
  • Microsoft Entra ID P1 or P2. It’s included in Microsoft 365 Business Premium, E3 and E5.
  • A server for the private network connector that can reach the WAC gateway on 443, and reach the internet outbound on 443 and 80 (80 is for certificate revocation checks). If your egress is filtered, allow the URLs in Microsoft’s connector requirements.

That’s enough to publish WAC at https://wacv-yourtenant.msappproxy.net/.

Optional: your own domain name

To use wacv.yourdomain.com instead, you also need:

  • A custom domain verified in Entra.
  • A TLS certificate for that name, as a .pfx with its private key. Entra doesn’t issue or renew it for you. A certificate from your internal CA works, as long as the devices you connect from trust its root.
  • Access to your public DNS, to add one CNAME that points the name at Application proxy.

Install Windows Admin Center

Starting the WAC vMode installer from PowerShell

This installer works perfectly fine on Windows Server Core.

The new installer asks which mode you want. Pick Virtualization Mode.

The Windows Admin Center installer asking for Administration or Virtualization mode

When it asks for a TLS certificate, give it a certificate for the gateway’s internal name (jkt-wacv-71.internal.net here). The connector talks to the gateway on that name. The public name gets its own certificate in Entra later.

Install the private network connector

In the Entra admin center go to Application proxy, then Download connector service, and accept the terms. Copy the installer to the connector server.

Private Network Connector Download pane in the Entra admin center

On a server with the desktop experience, run the installer, sign in with an account that’s an Application Administrator or Global Administrator when it asks, and you’re done.

Connector on Server Core

My connector server runs Server Core, and the installer failed near the end with:

User authentication failed. Make sure you enabled Application Proxy in the Azure Management Portal and try to register again.

Application proxy was enabled and the prerequisites were fine. The real error is only in the MSI log in %TEMP%:

System.DllNotFoundException: Unable to load DLL 'IEFRAME.dll': The specified module could not be found.

The sign-in window needs the desktop experience, and Server Core doesn’t have it. The installer then rolls everything back. The fix is to install without registering, then register with a token.

Install the connector without the sign-in step:

.\MicrosoftEntraPrivateNetworkConnectorInstaller.exe REGISTERCONNECTOR="false" /q
.\MicrosoftEntraPrivateNetworkConnectorInstaller.exe REGISTERCONNECTOR="false" /q

Then register it. This asks Entra for a device code, so you can sign in from any browser on any device, then hands the token to the connector’s own registration script. Put your tenant ID (from the Entra admin center Overview page) on the first line:

$TenantId = '<your-tenant-id>'   # Entra admin center > Overview

$client = '55747057-9b5d-4bd4-b387-abf52a8bd489'   # Microsoft's connector registration app
$login  = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0"

# 1. Get a sign-in code, then sign in with it from any browser
$code = Invoke-RestMethod "$login/devicecode" -Method Post -Body @{
    client_id = $client
    scope     = 'https://proxy.cloudwebappproxy.net/registerapp/user_impersonation'
}
Write-Host $code.message -ForegroundColor Yellow

# 2. Wait for the sign-in to finish
do {
    Start-Sleep 5
    $token = try {
        Invoke-RestMethod "$login/token" -Method Post -Body @{
            grant_type  = 'urn:ietf:params:oauth:grant-type:device_code'
            client_id   = $client
            device_code = $code.device_code
        }
    } catch { if ("$_" -notmatch 'authorization_pending') { throw } }
} until ($token)

# 3. Register the connector
Set-Location "$env:ProgramFiles\Microsoft Entra private network connector"
.\RegisterConnector.ps1 -modulePath "$PWD\Modules\" -moduleName MicrosoftEntraPrivateNetworkConnectorPSModule `
    -AuthenticationMode Token -Token $token.access_token -TenantId $TenantId -Feature ApplicationProxy
$TenantId = '<your-tenant-id>'   # Entra admin center > Overview

$client = '55747057-9b5d-4bd4-b387-abf52a8bd489'   # Microsoft's connector registration app
$login  = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0"

# 1. Get a sign-in code, then sign in with it from any browser
$code = Invoke-RestMethod "$login/devicecode" -Method Post -Body @{
    client_id = $client
    scope     = 'https://proxy.cloudwebappproxy.net/registerapp/user_impersonation'
}
Write-Host $code.message -ForegroundColor Yellow

# 2. Wait for the sign-in to finish
do {
    Start-Sleep 5
    $token = try {
        Invoke-RestMethod "$login/token" -Method Post -Body @{
            grant_type  = 'urn:ietf:params:oauth:grant-type:device_code'
            client_id   = $client
            device_code = $code.device_code
        }
    } catch { if ("$_" -notmatch 'authorization_pending') { throw } }
} until ($token)

# 3. Register the connector
Set-Location "$env:ProgramFiles\Microsoft Entra private network connector"
.\RegisterConnector.ps1 -modulePath "$PWD\Modules\" -moduleName MicrosoftEntraPrivateNetworkConnectorPSModule `
    -AuthenticationMode Token -Token $token.access_token -TenantId $TenantId -Feature ApplicationProxy

When it asks “Are you trying to sign in to…”, choose Continue.

Pass the token as a plain string. Microsoft’s docs show -Token as a SecureString, but in connector 1.5.5130.0 the parameter is a [String]. A SecureString arrives empty, and registration fails with “Make sure you are an administrator of the AD directory”, even when you’re a Global Administrator.

Registering the connector on Server Core with a device code

Check the connector

Back in Application proxy, the connector should show as Active in the Default group.

The connector showing Active in the Default connector group

Microsoft recommends at least two connectors per group, so one can be patched or rebooted without taking the app down.

Publish the app

Create the app from Application proxy, then Configure an app (or Enterprise applications, then New application, then Add an on-premises application). Then, on its Application proxy page:

Setting Value
Internal Url https://jkt-wacv-71.internal.net/ (the gateway’s internal name)
External Url wacv, with your custom domain (or the msappproxy.net default) picked from the dropdown
Pre Authentication Microsoft Entra ID
Connector Group The group your connector is in

The wacv app’s Application proxy settings

Turn off header translation

On the Advanced tab, turn Translate URLs in headers off.

The Advanced tab with Translate URLs in headers turned off

With it on, Application proxy rewrites the Host header to the internal name, so WAC sees jkt-wacv-71.internal.net while the browser is on wacv.publicdns.com. Sign-in still works, which is what makes this confusing, but WAC’s anti-forgery checks reject most of what happens after it.

Upload the certificate (custom domain only)

Back on the Basic tab, click Add SSL Certificate (it becomes View/Update SSL Certificate once one is bound), pick the .pfx, enter its password and upload it.

Uploading the PFX in the SSL certificate pane

The portal doesn’t ask for the certificate when you save the URLs, so it’s easy to miss. Until it’s uploaded, the custom domain serves Microsoft’s *.msappproxy.net certificate and every browser warns. Once uploaded it took a few minutes before the new certificate was being served.

Add the DNS record (custom domain only)

The bottom of the page shows the CNAME to create, for example wacv.publicdns.com -> wacv-<tenant>.msappproxy.net. Add it at your DNS provider.

Allow the external name on the WAC gateway

WAC only accepts WebSocket and cross-origin requests from origins it knows, and out of the box that list is empty. Through the proxy, the browser’s origin is the external URL, so add it to appsettings.json on the gateway and restart the service:

$file = 'C:\Program Files\WindowsAdminCenter\Service\appsettings.json'
$url  = 'https://wacv.publicdns.com'

Copy-Item $file "$file.bak"
$json = Get-Content $file -Raw
$json = $json.Replace('"CorsSites": [],', "`"CorsSites`": [ `"$url`" ],")
$json = $json.Replace('"WebSocketAllowedOrigins": []', "`"WebSocketAllowedOrigins`": [ `"$url`" ]")
[IO.File]::WriteAllText($file, $json)

Restart-Service WindowsAdminCenter
$file = 'C:\Program Files\WindowsAdminCenter\Service\appsettings.json'
$url  = 'https://wacv.publicdns.com'

Copy-Item $file "$file.bak"
$json = Get-Content $file -Raw
$json = $json.Replace('"CorsSites": [],', "`"CorsSites`": [ `"$url`" ],")
$json = $json.Replace('"WebSocketAllowedOrigins": []', "`"WebSocketAllowedOrigins`": [ `"$url`" ]")
[IO.File]::WriteAllText($file, $json)

Restart-Service WindowsAdminCenter

Without this, the dashboard loads, then its live connections are refused and the page falls over to “Unauthorized Access (Expired)” (see Troubleshooting).

Re-check it after every WAC update or reinstall. The installer rewrites appsettings.json.

Lock it down

Assignment is required on the app, so only the users and groups on its Users and groups page can get past Entra. Keep that list short. This is the management plane for your clusters.

Conditional Access applies to the app like any other. My Require MFA policy covers all apps, so there’s nothing app-specific to add.

Sign in

Browse to the external URL. You sign in to Entra first (with MFA), then to WAC itself. The proxy doesn’t pass your identity through to WAC, so it’s two sign-ins.

Troubleshooting

“Unauthorized Access (Expired)”

Error 403
Credentials were expired or Windows Admin Center was restarted. Navigate to Login Home and enter credentials again.
WAC's Unauthorized Access (Expired) page

You get past Entra, sign in to WAC, the dashboard starts loading, and a few seconds later this appears. The gateway’s event log (WindowsAdminCenter) shows the login succeeding, then 403s on /api/Services/EnterpriseAggregator/AgentQuery/Socket and on the dashboard’s API calls. Check both fixes:

  1. Translate URLs in headers is off on the app’s Advanced tab.
  2. The external URL is in CorsSites and WebSocketAllowedOrigins on the gateway, and the WindowsAdminCenter service was restarted afterwards.

I needed both. With only the first, the regular API calls worked but the WebSockets were still refused.

Browser warns about the certificate

Either the certificate isn’t uploaded yet (the site serves *.msappproxy.net), it was uploaded a few minutes ago and isn’t being served yet, or it comes from an internal CA your device doesn’t trust.

Connector installer fails on Server Core

See Connector on Server Core.