Skip to main content

Windows (WinRM) module

Deploy any file to any Windows path, then run PowerShell post-deploy scripts. The Windows sibling of the SSH module — same PathSet + ActionSet operator UX, different transport. Distinct from the IIS module, which is dedicated to IIS binding lifecycle.

Type: winrm Version: 1.0.0 Rollback: previous-version re-deploy

When to pick this module

Pick winrm when you want to:

  • Drop a PEM / PFX / DER / arbitrary file at a known Windows path (C:\inetpub\certs\site.pfx, \\fileserver\share\...).
  • Run PowerShell after — restart a service, import into a Java keystore via keytool, run a custom hook script.
  • Cover Windows targets that aren't IIS: Tomcat, JBoss, custom .NET services, ad-hoc certificate stores.

If your only goal is to update an IIS HTTPS binding, the dedicated IIS module handles binding discovery, app-pool recycling, and old-cert cleanup automatically.

Setup, step by step

Run each block on the target Windows host in an elevated PowerShell, then check the result before moving on. You don't need the big one-shot script — do it one step at a time.

Step 1 — Enable WinRM

Enable-PSRemoting -Force

Check the service is running:

Get-Service WinRM # Status should be "Running"

Step 2 — Pick your port (prefer HTTPS 5986)

See which listeners exist:

winrm enumerate winrm/config/Listener # look for Transport = HTTP (5985) / HTTPS (5986)

Recommended: use HTTPS on 5986. If there's no HTTPS listener, create one (needs a server cert whose name matches how CertAutoPilot will address the host):

# Server cert — DnsName MUST equal the host you'll enter in the CAP target (FQDN, or add the IP)
$cert = New-SelfSignedCertificate -DnsName "win01.corp.local" -CertStoreLocation Cert:\LocalMachine\My
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $cert.Thumbprint -Force

Verify:

winrm enumerate winrm/config/Listener # now shows Transport = HTTPS, Port = 5986
Test-NetConnection -ComputerName localhost -Port 5986 # TcpTestSucceeded : True

You have AD CS? Enroll a Web Server / Server Authentication cert from your CA instead of self-signed — its root is already trusted, so CertAutoPilot won't need a TLS-trust override.

Plain HTTP 5985 also works (requires AllowUnencrypted on the service — see the warning under Authentication; the auth handshake is protected but the WSMan body is not), but it's a more common source of confusing 401 errors — if you're troubleshooting, move to 5986 first. See 401 - invalid content type.

Step 3 — Open the firewall

If Windows Firewall is on, open the port you chose:

New-NetFirewallRule -DisplayName "WinRM HTTPS 5986" -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow

If the firewall is off, skip this.

Step 4 — The account must be a local Administrator

The credential CertAutoPilot connects with must be in the local Administrators group (importing a cert into LocalMachine\My and running post-scripts both require admin):

net localgroup Administrators # the account (or its group) must be listed

Step 5 — Local admin over NTLM? Set one registry value

Only if the account is a local admin (not a domain account) using NTLM, UAC filters its token over the network. Fix it once:

New-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" `
-Name LocalAccountTokenFilterPolicy -Value 1 -PropertyType DWORD -Force

Domain accounts don't need this — they get their full admin token on a network logon.

Step 6 — Configure the target in CertAutoPilot

  • Use TLS: on (port 5986) — or off if you chose 5985.
  • Auth: NTLM (recommended). Only use Basic if you deliberately enabled it (Step 2 note) — Basic on plain HTTP needs AllowUnencrypted=true and sends credentials in cleartext.
  • Username: DOMAIN\user for a domain account, .\user for a local one (NTLM). For Basic, use the bare user (no prefix).
  • TLS trust: if you used a self-signed 5986 cert, paste that cert (its public part) into the target's TLS-trust field, or CertAutoPilot won't trust the handshake.

Step 7 — Verify

Deploy a test distribution with the exact CAP account to confirm it connects and holds an administrator token. If a large-file transfer hits an envelope-size limit (WINRM_QUOTA), raise it once on the target: Set-Item WSMan:\localhost\MaxEnvelopeSizekb 8192.

Authentication methods

warning
Plain HTTP (5985) needs AllowUnencrypted — for every auth type

CertAutoPilot's WinRM clients authenticate the connection but do not encrypt the WSMan message body. Over HTTP 5985 the WinRM service only accepts such requests when AllowUnencrypted is enabled on the target — this applies to NTLM, Basic and Kerberos alike (a missing setting typically surfaces as an empty HTTP 500 after auth succeeds):

Set-Item WSMan:\localhost\Service\AllowUnencrypted $true
Restart-Service WinRM

Over HTTPS 5986 this is not needed (TLS protects the body) — prefer 5986 in production.

NTLM (default)

Works with local accounts and domain accounts. Enter just the account name: with the target's Domain field set it is sent as DOMAIN\user; with Domain empty it is sent as .\user (explicit local account). A name you qualify yourself (DOMAIN\user, user@fqdn, .\user) is used verbatim.

Basic

Username/password sent over the wire. Use only with TLS enabled — the target form warns when you mix Basic with plain HTTP.

Kerberos (first-class, zero-config)

Set auth_type=kerberos and the Domain field to the Kerberos realm (uppercase, e.g. CORP.LOCAL). Address the target by its AD FQDN (web01.corp.local) — never an IP: the SPN is auto-derived as HTTP/<hostname>, and the KDC has no service principal for a bare IP (KDC_ERR_S_PRINCIPAL_UNKNOWN).

That is the whole setup. There is no krb5.conf to create, mount, or point KRB5_CONFIG at — CertAutoPilot builds its Kerberos configuration in memory and discovers the domain controller automatically from the _kerberos._tcp.<REALM> DNS SRV record that every Active Directory domain publishes.

The remaining prerequisites are environmental, not product setup:

  1. DNS — the CertAutoPilot host must resolve AD DNS: the target's FQDN and the realm's SRV records. In a corporate network that is normally already true. If this host's DNS cannot resolve AD records, fill the optional KDC Address field on the target (host or host:port) to pin the domain controller explicitly — still a form field, never a file.
  2. FQDN targeting — connect by hostname, not IP (SPN requirement above).
  3. Time sync — Kerberos tolerates ±5 minutes of clock skew; any NTP-synced host qualifies.
  4. On plain HTTP (5985): AllowUnencrypted on the target — see the warning at the top of this section; Kerberos is not exempt. Not needed over HTTPS 5986.

Verify from the CertAutoPilot host if in doubt: getent hosts web01.corp.local and nc -z -w3 <dc> 88 both succeed.

Works against stock Windows KDCs

CertAutoPilot's Kerberos client explicitly disables PA-FX-FAST negotiation, which stock Windows KDCs do not answer — without that, a password login fails with KDC_ERR_PREAUTH_FAILED even though the password is correct. No action needed; this is handled for you.

NTLM disabled on hardened / domain-joined boxes

On a domain member locked down with Network security: Restrict NTLM: Incoming NTLM traffic (a common domain GPO), the WinRM listener rejects NTLM and every auth attempt 401s. Either allow incoming NTLM in the domain GPO (RestrictReceivingNTLMTraffic = 0 — a local registry override reverts on the next gpupdate) or switch to Kerberos (above), which needs no change on the box and no setup on the CertAutoPilot host. The IIS 401 guide has the full walkthrough.

CredSSP (multi-hop delegation) is not supported in this version. Most domain-joined scenarios are covered by Kerberos.

Worked before the upgrade, 401 now?

A bare username with an empty Domain used to be sent unqualified — a domain-joined server could resolve it as a domain account. Since 1.5.49 it is sent as .\user (explicitly local). If that was your setup, set Domain on the target or write the username as DOMAIN\user. The worker log records the assembled wire username at debug level.

PathSet — where files go

Target OS must be Windows

The PathSet (and ActionSet) you bind to a WinRM target must be created with target_kind: "windows". The form's PathSet/ActionSet dropdowns filter to Windows-scoped resources only; the backend rejects mismatched bindings at create/update time. Reusing a Linux PathSet on a WinRM target (or vice versa) is not allowed — POSIX paths and owner/mode have no Windows equivalent.

Identical shape to the SSH module's PathSet. Each entry pairs a content source with an absolute Windows path. Sources:

  • cert — leaf certificate PEM
  • chain — chain (intermediates) PEM
  • fullchain — leaf + chain PEM
  • private_key — private key PEM
  • combined — leaf + chain + key in one PEM file

Format defaults to pem; der is supported for cert and private_key sources.

Path validation rejects: relative paths, .. traversal segments, null bytes, Windows reserved device names (CON, NUL, PRN, AUX, COM1–9, LPT1–9), forbidden filename characters (< > : " | ? *), and components with trailing dots or spaces (Windows strips them silently). Drive-letter and UNC paths are both accepted.

ActionSet — what runs after

By default the ActionSet runs only when a deployed file actually changes (hash-based skip), so restart/reload actions stay idempotent on no-op redistributes. Set run_always on the ActionSet to run it on every distribution even when nothing changed — for verification/diagnostic actions that must run each time (e.g. printing the deployed certificate). When it runs despite no change the log shows no file changed, running anyway (run_always enabled).

Two modes:

command mode

List of PowerShell commands. Run one-by-one. Optional allowed_commands regex allowlist (defence in depth — operator mistypes a destructive command and the regex blocks it).

script_inline mode

Multi-line script body. Uploaded to a remote temp path ($env:TEMP\certautopilot-XXXX.ps1) via the chunked WriteFile transport, ACL-restricted to the executing user, run with -NoProfile -NonInteractive -ExecutionPolicy Bypass -File, then removed (best-effort cleanup).

Operator picks the shell: powershell.exe (Windows PowerShell 5.1, default — present on every modern Windows Server) or pwsh.exe (PowerShell 7+, must be installed on the target).

Variable substitution

Project variables expand via the ${{ NAME }} placeholder syntax. Values are PowerShell-escape-aware: a hostile project variable value $(Get-Process) is substituted as the literal string '$(Get-Process)' rather than an executable sub-expression. Same threat-model fix as the SSH module's shell-escape work; consult project variables for syntax details and escape semantics.

Worked example: cert + Tomcat reload

PathSet:

  • C:\Tomcat\conf\ssl\fullchain.pem → source fullchain
  • C:\Tomcat\conf\ssl\privkey.pem → source private_key

ActionSet (mode script_inline, shell powershell.exe):

$ErrorActionPreference = 'Stop'

# Convert PEM → JKS for Tomcat
keytool -importkeystore `
-srckeystore "C:\Tomcat\conf\ssl\fullchain.pem" `
-srcstoretype PEM `
-destkeystore "C:\Tomcat\conf\ssl\keystore.jks" `
-deststoretype JKS `
-alias tomcat `
-storepass "${{ KEYSTORE_PASS }}"

Restart-Service -Name Tomcat9
Write-Output "tomcat reloaded with new cert"

Rollback

Supported via previous-version re-deploy: a rollback re-writes a previous retained certificate version's files via the PathSet and re-runs the ActionSet — the module's normal deploy path, fed older material. Whatever the ActionSet does (restart a service, import into a JKS, custom logic) simply runs again against the older files. Eligibility, the version picker, and auto-rollback: Rollback.

Limits + performance

KnobDefaultNote
File transfer rate~60–300 KB/secWAN-RTT bound; chunked base64 + certutil decode.
Per-file size cap100 MBConfigurable on the target via max_file_size_bytes. Raise only if you genuinely need to push > 100 MB.
Per-script size cap~256 KB UTF-8 sourceUTF-16LE doubles bytes; we cap before WSMan envelope quota.
Per-target concurrency10Module-level; clamped on the upper end.
Per-target timeout5 mintarget_timeout_seconds on the module config.
Per-command timeout60 seccommand_timeout_seconds on the target; ActionSet timeout_seconds overrides per-command.
Output truncation16 KBPer-command stdout+stderr; PowerShell verbosity bounded.

Error codes

The module emits structured WINRM_* error codes for retry classification:

  • WINRM_CONNECT — dial / DNS / TLS handshake failure (network class)
  • WINRM_AUTH — 401, NTLM/Kerberos rejection, "Access denied" (auth class)
  • WINRM_TIMEOUT — context deadline exceeded (transient — retried)
  • WINRM_EXEC — non-zero PowerShell exit (transient — retried)
  • WINRM_PS_SYNTAX — ParserError detected in stderr (validation — not retried)
  • WINRM_FILE — file transfer failure (transient — retried)
  • WINRM_VALIDATION — operator config error (path, run_as)
  • WINRM_QUOTA — MaxEnvelopeSize exceeded (permanent — operator must raise the quota)

vs. the IIS module

Aspectwinrmiis
ScopeGeneric file deploy + scriptIIS binding lifecycle
PathSet / ActionSetYesNo (uses fixed IIS scripts)
ConcurrencyParallel (10 default)Sequential
AuthNTLM, Basic, KerberosNTLM, Basic
RollbackYes (previous-version re-deploy)Yes (previous-version re-deploy)
Picks IIS bindings automaticallyNo (operator scripts it)Yes

Multi-certificate hosts

Two mechanisms let one WinRM target serve many certificates:

  1. Per-distribution PathSet/ActionSet — each certificate's distribution can select its own PathSet (where files land) and ActionSet (what runs afterwards) from the certificate's Distributions → Overrides drawer (winrm { path_set_id, action_set_id }, with a * default row). A selected override must resolve — a deleted or wrong-kind resource fails the run instead of silently using the target's own libraries.
  2. Certificate-context variables in paths — PathSet paths may contain ${{ CAP_PRIMARY_DOMAIN }}, ${{ CAP_DOMAIN_SLUG }}, ${{ CAP_CERT_TOKEN }} or ${{ CAP_DEPLOY_NAME }}, rendered per certificate at distribution time: C:\\certs\\${{ CAP_DOMAIN_SLUG }}\\bundle.pfx. Use CAP_DOMAIN_SLUG, not CAP_PRIMARY_DOMAIN, in Windows paths: * is invalid in Windows file names, so a wildcard certificate's primary domain (*.example.com) always fails, while the slug renders as example-com. Project variables also render; secret variables are rejected in paths; a missing variable fails that target. Note that substitution always applies: a literal ${{ ... }} in a path or command must be escaped with a backslash (\${{ ... }}). (A rollback re-deploys the previous version through the same PathSet rendering.)

When several certificates share a host, put ${{ CAP_CERT_TOKEN }} or ${{ CAP_DEPLOY_NAME }} in the path (or use distinct PathSets) so files never collide.