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:
| Block | Symptom | Why |
|---|---|---|
Basic auth [Source=GPO] off | client can’t fall back to Basic | GPO-locked |
AllowUnencrypted=false GPO-locked | HTTP listener rejects unencrypted | WinRM mandates encrypted Negotiate over HTTP |
RootSDDL lacks Remote Management Users | 5985 reachable but authz-denied | shipped default SDDL; Set-Item WSMan:\...\RootSDDL refuses while any net profile is Public |
| Docker/WSL vEthernets are Public | can’t relax WinRM config via Set-Item | changing 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)
- 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 - The agent target uses
WinRmTransport.Https(port 5986) and trusts the cert (import to the agent’s trust store, or — lab only — setAllowUntrustedCertificate=trueon the target config, which skips chain and CN validation; the self-signed listener cert’s CN = the machine name, nothost.docker.internal). Adocker-usersdeploy account is the least-privilege identity; the on-hostdockerCLI only works once the account is indocker-usersand 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). - 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 andAgentTaskAssignmentMapper.MapWinRmExecute(external polling agents). The NX-3 wiring first missed the mapper, soAuthMechanism/AllowUntrustedCertificatewere silently dropped and the agent failed withUntrustedRoot. Regression-gated byWinRmExecuteTransportTests.
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
- Non-determinism on GPO-managed fleets:
RootSDDL+LocalAccountTokenFilterPolicyedits can be reverted by a GPO refresh → deploys break intermittently. WinRM config must be enforced by policy on managed hosts, not hand-edited. - Docker networking risk: changing vEthernet profiles Public→Private to unblock WinRM config can disrupt Docker Desktop networking / the running stack. Avoid it.
- Attack surface: enabling WinRM + a local deploy account widens the surface; on hardened enterprise Windows this may violate policy. WinRM must be OPT-IN with explicit, documented prerequisites — never a default.
