WinRM deploy target — prerequisites, the GPO wall, and revert (NX-3)

Currency: the host-state observations below are investigation-derived (LES-007, 2026-07-02) on the Stella build host, not live-verified state. Re-probe before relying (winrm get winrm/config, Get-NetConnectionProfile, Get-Item WSMan:\localhost\Service\Auth\*). WinRM is the lowest-priority Windows deploy path — prefer docker-over-SSH or a local ComposeHost (both proven; see the live-deploy-operator playbook). Use WinRM only when an operator explicitly needs it and can meet the prerequisites below.

Decision (NX-3)

Use WinRM over HTTPS (5986) + Negotiate. TLS provides the transport encryption, so a GPO-locked AllowUnencrypted=false is satisfied without the client needing to implement SPNEGO message-sealing over HTTP. This is the lowest-code-risk option that works against a GPO-hardened host. Alternatives considered + rejected for the general case: (B) agent-side encrypted-Negotiate over HTTP — larger .NET WinRM client change, only if HTTPS is unavailable; © Kerberos + domain account — domain-joined targets only (this host is a workgroup box); (D) don’t use WinRM — use the docker-over-SSH engine or local ComposeHost (the recommended default).

The GPO wall observed on this host (LES-007)

The Stella agent’s .NET WinRM client CONNECTS to host.docker.internal:5985 but 401s on NTLM — it does not do encrypted-NTLM-over-HTTP against a hardened WinRM listener. Root causes stacked:

BlockSymptomWhy
Basic auth [Source=GPO] offclient can’t fall back to BasicGPO-locked
AllowUnencrypted=false GPO-lockedHTTP listener rejects unencryptedWinRM mandates encrypted Negotiate over HTTP
RootSDDL lacks Remote Management Users5985 reachable but authz-deniedshipped default SDDL; Set-Item WSMan:\...\RootSDDL refuses while any net profile is Public
Docker/WSL vEthernets are Publiccan’t relax WinRM config via Set-Itemchanging profiles Public→Private risks Docker Desktop networking (“do not screw it up”)

RootSDDL workaround actually used (registry, since Set-Item refused under Public profiles):

# HKLM WSMAN RootSDDL — add Remote Management Users, then restart the service
$k = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Service'
# (record the ORIGINAL value first: Get-ItemProperty $k -Name RootSDDL)
Set-ItemProperty -Path $k -Name RootSDDL -Value '<sddl-including-S-1-5-32-580>'
Restart-Service WinRM

Enabling WinRM-over-HTTPS (the supported path)

  1. Create a server cert for the host (CN = the name the agent connects to) and an HTTPS listener:
    $cert = New-SelfSignedCertificate -DnsName '<host-cn>' -CertStoreLocation Cert:\LocalMachine\My
    New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * `
      -CertificateThumbPrint $cert.Thumbprint -Force
    New-NetFirewallRule -DisplayName 'WinRM-HTTPS-in' -Protocol TCP -LocalPort 5986 `
      -Direction Inbound -Action Allow  # scope RemoteAddress to the container subnet
    
  2. The agent target uses WinRmTransport.Https (port 5986) and trusts the cert (import to the agent’s trust store, or — lab only — set AllowUntrustedCertificate=true on the target config, which skips chain and CN validation; the self-signed listener cert’s CN = the machine name, not host.docker.internal). A docker-users deploy account is the least-privilege identity; the on-host docker CLI only works once the account is in docker-users and the WinRM session is a fresh logon (the agent pools sessions for 5 min — docker restart <agent> to force a new logon after a group change).
  3. Keep AllowUnencrypted=false — HTTPS satisfies the encryption requirement; no GPO relaxation needed.

Agent-side prerequisites (Linux agent-core) — REQUIRED for Negotiate

The .NET WinRM client authenticates with HttpClientHandler.Credentials (Negotiate/SPNEGO). On the Linux agent, SPNEGO delegates NTLM to GSSAPI, so the container must have the NTLM mechanism plugin:

apt-get install -y gss-ntlmssp   # registers /etc/gss/mech.d/mech.ntlmssp.conf

Without it, TLS succeeds but auth returns 401 Unauthorized (SPNEGO tries Kerberos — no KDC in a workgroup — then can’t fall back to NTLM). Durability OWED: bake gss-ntlmssp into devops/docker/Dockerfile.agent-core[.runtime] (today it’s an ephemeral apt shim, lost on --force-recreate, kept across docker restart).

Transport-mapper gotcha (fixed e25d8fafeb): the auth override + cert opt-in must cross both dispatch paths — the in-process adapter and AgentTaskAssignmentMapper.MapWinRmExecute (external polling agents). The NX-3 wiring first missed the mapper, so AuthMechanism/AllowUntrustedCertificate were silently dropped and the agent failed with UntrustedRoot. Regression-gated by WinRmExecuteTransportTests.

Proven live 2026-07-03: a gated deploy (operator-approved through the console UI) dispatched a winrm.execute task; the agent authenticated over HTTPS(5986)+Negotiate, bypassed the self-signed cert, and ran PowerShell on the host → winrm-host=AW-36152 user=aw-36152\stella-winrm docker-server=29.5.3, deployment succeeded.

Fail-fast preflight (owed — needs a Windows runner to validate)

The agent WinRM client should run a preflight that detects the blocks above (Basic off / AllowUnencrypted locked / RootSDDL missing Remote Management Users / Public profiles) and returns a clear GPO-prerequisite finding, not a cryptic 401. Status: OWED — the code + its live proof need a Windows runner (none in the Gitea fleet); the WinRM path is opt-in, so this is deprioritized behind docker-over-SSH / local ComposeHost.

Revert the host changes already made (LES-007)

If the host was modified during WinRM investigation, restore it:

# 1. RootSDDL — restore the ORIGINAL value you recorded before editing (do NOT leave the widened SDDL)
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Service' -Name RootSDDL -Value '<original>'
# 2. LocalAccountTokenFilterPolicy — if set, remove it
Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name LocalAccountTokenFilterPolicy -ErrorAction SilentlyContinue
# 3. Any HTTPS listener / firewall rule added above
Get-ChildItem WSMan:\localhost\Listener | Where-Object Keys -match 'Transport=HTTPS' | Remove-Item -Recurse
Remove-NetFirewallRule -DisplayName 'WinRM-HTTPS-in' -ErrorAction SilentlyContinue
Restart-Service WinRM
# 4. Do NOT flip Docker/WSL vEthernet profiles Public->Private just for WinRM — it risks the stack.

Risks to document for operators