Scroll Top

Automating Windows with Ansible: A Secure Approach Using HTTPS and Certs

Hand touching infographic cloud computing and technology icons ,

Overview

In environments where Windows machines need to be managed remotely—especially via automation tools like Ansible—it’s crucial to use secure methods for remote communication. While WinRM is a powerful protocol for remote management, it must be configured properly to avoid becoming a security risk.

This post walks you through setting up WinRM over HTTPS with certificate-based authentication on Windows, which is far more secure than basic authentication and unencrypted traffic.

WINRM Authentication

Windows Remote Management (WinRM) supports several authentication methods, each with its own characteristics. Here’s a concise comparison:

Why Use Certificate-Based Authentication?

By default, WinRM uses basic authentication, which sends credentials in plain text (unless wrapped in HTTPS). Certificate-based authentication:

  • Enables secure communication without exposing passwords
  • Supports automation use cases like Ansible connecting to Windows
  • Avoids dependency on Active Directory or Kerberos in some cases

Implementing certificate authentication requires additional setup, such as configuring HTTPS listeners and managing certificates, but the security benefits often outweigh these complexities.

Prerequisites

  1. A certificate is generated for the user ansiblerunner’, with the username set as the CN.
  2. A user key file is available for authenticating the certificate.
  3. All certificates are stored in an S3 bucket.
  4. A PowerShell standup script runs after the Windows machine is created to configure the WinRM settings.
  5. The credentials for the ansiblerunner’ user are stored in the SSM Parameter Store, and the Windows Server is configured with an instance profile for SSM Parameter Store access.
  6. A trusted CA certificate is available to act as the CA for the WinRM HTTPS SSL certificate.
  7. WinRM ports are opened on the Windows Server and accessible from the Ansible controller.

Example PowerShell Script to Generate a Certificate for a User

$username = "tester"  $password = ConvertTo-SecureString -String "Ansible123#" -AsPlainText -Force    Check if the user exists    $userExists = Get-LocalUser -Name $username -ErrorAction SilentlyContinue    if (-not $userExists) {      Write-Output "User '$username' does not exist. Creating..."      New-LocalUser -Name $username -AccountNeverExpires -Description "Ansible Remote Management" -Password $password      Write-Output "User '$username' does not exist. Created!!"  } else {      Write-Output "User '$username' already exists."  }    Add user to Administrators group    $adminGroup = Get-LocalGroupMember -Group "Administrators" | Where-Object { $_.Name -match $username }  if (-not $adminGroup) {      Write-Output "Adding '$username' to Administrators group..."      Add-LocalGroupMember -Member $username -Group "Administrators"      Write-Output "Added '$username' to Adminstrators group!!"  } else {      Write-Output "User '$username' is already in the Administrators group."  }    $userExists = Get-LocalUser -Name $username -ErrorAction SilentlyContinue  if($userExists){      # Generate a self-signed certificate  $clientParams = @{      CertStoreLocation = 'Cert:\CurrentUser\My'      NotAfter          = (Get-Date).AddYears(5)      Provider          = 'Microsoft Software Key Storage Provider'      Subject           = "CN=$username"      TextExtension     = @("2.5.29.37={text}1.3.6.1.5.5.7.3.2","2.5.29.17={text}upn=$username@localhost")      KeyExportPolicy   = 'Exportable'  }    $cert = New-SelfSignedCertificate @clientParams    Export certificate to PFX    $pfxPath = "$pwd\cert.pfx"  $certPfxBytes = $cert.Export('Pfx', '')  # Set a password for the PFX  [System.IO.File]::WriteAllBytes($pfxPath, $certPfxBytes)    Export certificate to PEM format    $certPemPath = "$pwd\cert.pem"  Set-Content -Path $certPemPath -Value @(      "-----BEGIN CERTIFICATE-----"      [Convert]::ToBase64String($cert.RawData) -replace ".{64}", "$&`n"      "-----END CERTIFICATE-----"  )    Write-Output "Certificates exported: $certPemPath and $pfxPath"

If you don’t have an existing local user and certificate (optional), copy the certificate and PFX files to your local machine. You can extract the key from the PFX file and use it for further tasks.

Follow the steps below only if you don’t have a trusted root CA to generate a CA and server certificate issued by the CA:

It is recommended to have a certificate issued by a trusted CA in the Windows certificate store; the following is a workaround.

Execute the following commands in a Bash shell with the latest version of OpenSSL installed.

  1. Generate a Certificate Authority (CA)

To create a CA for signing server and client certificates, generate a private key and a CA certificate:

openssl genrsa -out myCA.key 2048 openssl req -x509 -new -nodes -key myCA.key -sha256 -days 825 -out myCA.pem 

This creates:

  • myCA.key → The private key for the CA
  • myCA.pem → The CA certificate

Important: Remember the password used for the CA certificate, as it will be required when generating other certificates.

  1. Generating Server Certificates

2.1: Generate a Server Key
openssl genrsa -out myserver.key 2048 
2.2: Create a Certificate Signing Request (CSR)
openssl req -new -key myserver.key -out myserver.csr 

Note: The Common Name (CN) should match the hostname of the Windows server. Other fields can be customized or left as default.

2.3: Create the Server Certificate Configuration File

Create a file named myserver.ext and add the following contents:

authorityKeyIdentifier=keyid,issuer  basicConstraints=CA:FALSE  keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment  extendedKeyUsage = serverAuth  subjectAltName = @alt_names    [alt_names]  DNS.1 = ec2-18-213-150-124.compute-1.amazonaws.com  DNS.2 = 18.213.150.124

Explanation of Fields:

  • authorityKeyIdentifier → Links the certificate to the issuing CA.
  • basicConstraints → Specifies this is not a CA certificate.
  • keyUsage → Defines allowed cryptographic operations.
  • extendedKeyUsage → Specifies server authentication use.
  • subjectAltName → Defines valid DNS names and IPs for the certificate.
2.4: Sign the Server Certificate
openssl x509 -req -in myserver.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial \ -out myserver.pem -days 825 -sha256 -extfile myserver.ext 

This generates:

  • myserver.pem → The signed certificate.
2.5: Convert the Certificate to PFX Format (for Windows)

Windows requires the certificate and private key to be combined into a PKCS#12 file for import:

openssl pkcs12 -export -inkey myserver.key -in myserver.pem -out myserver.pfx

Tip: When prompted for an export password, press Enter to leave it empty.

  1. Importing Certificates in Windows

3.1: Add CA Certificate to Trusted Root Store

To trust certificates issued by the CA, add the CA certificate to the Trusted Root Certification Authorities store:

Import-Certificate -FilePath "C:\path\to\myCA.pem" -CertStoreLocation Cert:\LocalMachine\Root 

This ensures Windows recognizes certificates issued by this CA as trusted.

3.2: Import Server Certificate

To import the server certificate (PFX) into the Personal (My) store, run:

Import-PfxCertificate -FilePath "C:\path\to\myserver.pfx" -CertStoreLocation Cert:\LocalMachine\My

Steps to Configure Certificate Authentication

The PowerShell script executes the following steps.

  1. Certificate Retrieval and Validation

  • Retrieve the user certificate from a designated S3 bucket.
  • Validate the presence of the certificate file and verify its contents.
  • Convert the certificate from PEM format to binary format and extract the thumbprint.
  • Ensure that the extracted certificate and thumbprint are valid.
  1. Certificate Installation

  • Add the retrieved certificate to the appropriate system certificate stores, including:
    • Trusted Root Certification Authorities
      • Add the certificate to this store only if it is self-signed; otherwise, the next two are enough.
      • Adding this to the root store makes it a trusted CA.
    • Trusted People
    • Personal Certificate Store
  • Ensure the certificate is correctly added to the system to facilitate authentication.
  1. Secure Credential Retrieval

  • Obtain the necessary credentials from the AWS SSM Parameter Store or whichever credential store is compatible with the Windows machines for the ‘ansiblerunner’ user.
  • Convert the retrieved credentials into a secure format for authentication.
  • Create a credential object for user authentication.
  1. Mapping Certificate to User Authentication

  • Use the certificate thumbprint to map it to a user in WinRM.
  • Configure WinRM to recognize the user’s certificate for authentication.
  • Ensure that the mapping is successfully applied for remote authentication.
  1. Configuring WinRM over HTTPS

  • Stop the WinRM service before making configuration changes.
  • Identify the appropriate SSL certificate issued by a trusted CA in the server’s certificate store.
  • Set up WinRM to use certificate-based authentication.
  • Configure WinRM to allow encrypted communication over HTTPS.
  1. Creating a Secure HTTPS Listener

  • Remove any existing HTTPS listeners to prevent conflicts. (Validate before removing)
  • Create a new listener on the designated port (e.g., 5986) using the identified certificate.
  • Validate that the listener is successfully created.
  1. Configuring Firewall Rules

  • Add firewall rules to allow incoming traffic on the specified WinRM HTTPS port.
  • Ensure that the firewall configuration does not block remote management traffic.
  1. Restarting and Verifying WinRM Configuration

  • Set the WinRM service to start automatically when booting.
  • Restart the WinRM service to apply changes.
  • Verify that the HTTPS listener is properly configured.
  • Test the remote management connection using PowerShell commands:
    • Test-WSMan -ComputerName  -Port  -UseSSL
    •  
      Enter-PSSession -ComputerName  -Port  -UseSSL -Credential (Get-Credential)
  1. Standup script for performing the configurations

configure_winrm_certificate.ps1

param (      [string]$username = "ansiblerunner"  )  function Configure-User-Auth{  #******************** Download the certificate from S3 bucket ****************************#      $certPemPath = "$pwd\$username.pem"        try {          $sourceUrl = "https://s3.us-east-1.amazonaws.com/s3_bucket_path/ansiblerunner.pem"                      Invoke-WebRequest -Uri $sourceUrl -OutFile $certPemPath            Write-Host "Certificate downloaded successfully from S3."      } catch {          Write-Host "Error: Failed to download certificate from S3 - $($_.Exception.Message)"          exit 1      }        #******************** retrive content from certificate for use ****************************#      # Verify if the certificate file exists      if (-not (Test-Path $certPemPath)) {          Write-Host "Error: Certificate file not found at $certPemPath!"          exit 1      }        # Read and Validate Certificate Content      $certPemContent = Get-Content -Raw -Path $certPemPath | Out-String      if (-not $certPemContent -or $certPemContent.Length -lt 50) {  # Basic validation for PEM content          Write-Host "Error: Empty or invalid certificate content!"          exit 1      }        # Convert PEM to Binary and Import Certificate      try {          $certBytes = [Convert]::FromBase64String(($certPemContent -split "`n" | Where-Object { $_ -notmatch "-----(BEGIN|END) CERTIFICATE-----" }) -join "")          $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(,$certBytes)          $caThumbprint = $cert.Thumbprint      } catch {          Write-Host "Error: Failed to parse the certificate!"          exit 1      }        if (-not $cert -or -not $caThumbprint) {          Write-Host "Error: CA Thumbprint could not be generated and certificate is invalid!"          exit 1      }        Write-Host "Certificate successfully parsed. CA Thumbprint: $caThumbprint"        #******************** Add certificate to cert store ****************************#      $stores = @("Cert:\LocalMachine\Root", "Cert:\LocalMachine\TrustedPeople", "Cert:\LocalMachine\My")      foreach ($storePath in $stores) {          try {              $store = Get-Item -LiteralPath $storePath -ErrorAction Stop              $store.Open('ReadWrite')              $store.Add($cert)              $store.Close()              Write-Host "Certificate added to $storePath."          } catch {              Write-Host "Error: Failed to add certificate to $storePath - $($_.Exception.Message)"          }      }        #******************** Certificate mapping for the user ****************************#      try {          $ssmParamterName = "ansiblerunnerPassword"          $ssmParamterNameVal = Get-SSMParameterValue -Name $ssmParamterName          $securePassword = ConvertTo-SecureString -String $ssmParamterNameVal -AsPlainText -Force          $credential = New-Object System.Management.Automation.PSCredential ($username, $securePassword)      } catch {          Write-Host "Error: Failed to create credentials for $username - $($_.Exception.Message)"          exit 1      }        # Map certificate to user in WinRM      if ($credential -and $cert -and $caThumbprint) {          try {              $certMapping = @{                  Path       = 'WSMan:\localhost\ClientCertificate'                  Subject    = $cert.GetNameInfo('UpnName', $false)                  Issuer     = $caThumbprint                  Credential = $credential                  Force      = $true              }              New-Item @certMapping -ErrorAction Stop              Write-Host "Certificate successfully mapped to WinRM for user $username."          } catch {              Write-Host "Error: Failed to map certificate to WinRM - $($_.Exception.Message)"              exit 1          }      } else {          Write-Host "Error: Certificate mapping failed. Required components are missing."          exit 1      }      }      function Configure-WinRMHttps {      try {          if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {              Write-Host "Please run this script as Administrator" -ForegroundColor Red              Exit 1          }            # Stop WinRM service if running          Write-Host "Stopping WinRM service..."          Stop-Service WinRM -Force -ErrorAction Stop            $dnsname = [System.Net.Dns]::GetHostByName($env:computerName).HostName          Write-Output "Resolved DNS Name: $dnsname"                    $hostname = $env:COMPUTERNAME          if((-not $hostname) -or (-not $dnsname)){              throw "Hostname and DNS name cannot be resolved!"          }                    # Find certificate issued by CA          $cert = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {               $_.Subject -match $dnsname -and               $_.Issuer -match "YourCA"          }            if (-not $cert) {              throw "No matching certificate found! WinRM HTTPS configuration failed."          }                    $thumbprint = $cert.Thumbprint          Write-Output "Certificate Thumbprint: $thumbprint"                    # Configure WinRM          Write-Host "Configuring WinRM..."          winrm quickconfig -q -Force          winrm set winrm/config/service '@{AllowUnencrypted="true"}'          winrm set winrm/config/service/auth '@{Basic="true"}'          winrm set winrm/config/service/auth '@{Certificate="true"}'                    # Remove existing HTTPS listener          Write-Host "Removing existing HTTPS listeners..."          Get-ChildItem -Path WSMan:\localhost\Listener\ | Where-Object { $_.Keys -like "*HTTPS*" } | Remove-Item -Recurse -Force -ErrorAction Stop            # Add new HTTPS listener          Write-Host "Creating new HTTPS listener..."          $port = 5986          New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint $thumbprint -Force -ErrorAction Stop            # Configure firewall rule          Write-Host "Configuring firewall rule..."          $ruleName = "Windows Remote Management (HTTPS-In)"          $existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue            if (-not $existingRule) {              New-NetFirewallRule -DisplayName $ruleName `                  -Direction Inbound `                  -LocalPort $port `                  -Protocol TCP `                  -Action Allow `                  -Program System -ErrorAction Stop              Write-Host "Firewall rule created."          } else {              Write-Host "Firewall rule already exists."          }            # Start WinRM service          Write-Host "Starting WinRM service..."          Set-Service -Name WinRM -StartupType Automatic          Start-Service WinRM -ErrorAction Stop            # Verify configuration          $listener = Get-ChildItem -Path WSMan:\localhost\Listener\ | Where-Object { $_.Keys -like "*HTTPS*" }          if ($listener) {              Write-Host "`nWinRM HTTPS Configuration Complete!" -ForegroundColor Green              Write-Host "Certificate Thumbprint: $thumbprint"              Write-Host "HTTPS Listener Port: $port"              Write-Host "`nTo test the connection from another machine, use:"              Write-Host "Test-WSMan -ComputerName $dnsname -Port $port -UseSSL"              Write-Host "`nTo connect using PowerShell remoting:"              Write-Host "Enter-PSSession -ComputerName $dnsname -Port $port -UseSSL -Credential (Get-Credential)"          } else {              throw "WinRM HTTPS Configuration Failed!"          }      } catch {          Write-Host "Error: $_" -ForegroundColor Red          Exit 1      }  }  # exit if ansiblerunner user doesn't exist  try {      $userExists = Get-LocalUser -Name $username -ErrorAction Stop  } catch {      Write-Host "User '$username' does not exist. Exiting..."      exit 1  }  # Call the function  Configure-User-Auth  Configure-WinRMHttps

 

  1. Testing from the Ansible Controller Machine

  • Create a Python virtual environment in the Ansible controller machine.
  • Check the OpenSSL version and ensure compatibility.
  • Use the openssl s_client command to validate if required:
  1. Ansible inventory file

windows:    hosts:      your_server_hostname:    vars:      ansible_user: ansiblerunner      ansible_port: 5986      ansible_connection: winrm      ansible_winrm_transport: certificate      ansible_winrm_ca_trust_path: CAcert.pem      ansible_winrm_cert_pem: ansiblerunner.pem      ansible_winrm_cert_key_pem: ansiblerunner.key

 

  1. Steps to Configure Ansible Automation Platform for Windows Server

12.1 Create a Custom Credential Type

A custom credential type is required to securely store certificates and keys for Windows authentication.

  1. Navigate to Ansible Automation Controller.
  2. Go to Infrastructure > Credential Types.
  3. Click Add to create a new credential type.
  4. Define the following fields:
    • Name: Windows Certificate Authentication
    • Description: Credential type to store certificates and keys for Windows authentication.
    • Input Configuration (YAML):
      fields:    - id: cert      type: string      label: Certificate      multiline: true    - id: key      type: string      label: Key      secret: true      multiline: true    - id: cacert      type: string      label: cacert      secret: true      multiline: true
    • Injector Configuration (YAML):
      env:    MY_KEY_FILE: '{{ tower.filename.key_file }}'    MY_CERT_FILE: '{{ tower.filename.cert_file }}'    MY_CACERT_FILE: '{{ tower.filename.key_file }}'  file:    template.key_file: '{{ key }}'    template.cert_file: '{{ cert }}'    template.cacert_file: '{{ cacert }}'
  5. Click Save to create the credential type.
12.2 Create a Credential for Windows Authentication
  1. Navigate to Credentials.
  2. Click Add to create a new credential.
  3. Fill in the following details:
    • Name: Windows Server Credential
    • Credential Type: Select Windows Certificate Authentication (created in the previous step).
    • Organization: Select the appropriate organization.
    • Certificates: Upload or paste the certificate contents (best to upload them to avoid formatting issues).
    • Private Key: Upload or paste the private key content (best to upload it to avoid formatting issues).
  4. Click Save.
12.3 Create an Inventory with Windows Hosts
  1. Navigate to Inventories.
  2. Click Add and select Inventory.
  3. Enter the following details:
    • Name: Windows Server Inventory
    • Organization: Select the appropriate organization.
  4. Click Save.
  5. Click Hosts and add a new host with:
    • Hostname: The IP or DNS name of the Windows server.
  6. Click Save.
  7. Or optionally, you can import hosts from your project’s inventory file.
12.4 Create a Job Template
  1. Navigate to Templates.
  2. Click Add and select Job Template.
  3. Fill in the following details:
    • Name: Windows Server Automation
    • Job Type: Run
    • Inventory: Select Windows Server Inventory
    • Project: Select the project containing the playbook.
    • Playbook: Select the playbook to execute.
      • Sample Playbook to ping the Windows machine:
      • - name: Windows Test    hosts: windows    gather_facts: false    vars:      ansible_user: ansiblerunner      ansible_port: 5986      ansible_connection: winrm      ansible_winrm_transport: certificate      ansible_winrm_ca_trust_path: "{{ lookup('env', 'MY_CACERT_FILE') }}"      ansible_winrm_cert_pem: "{{ lookup('env', 'MY_CERT_FILE') }}"      ansible_winrm_cert_key_pem: "{{ lookup('env', 'MY_KEY_FILE') }}"    tasks:      - name: Ping Server        ansible.windows.win_ping:
    • Credentials: Select Windows Server Credential.
  4. Click Save.
12.5 Run the Job Template
  1. Navigate to Templates.
  2. Locate the Windows Server Automation job template.
  3. Click Launch to execute the playbook.
  4. Monitor the job execution logs for any issues.

 

13. Common Issues & Troubleshooting

Conclusion

By following this guide, you have successfully configured certificate-based authentication for WinRM on Windows and secured the connection using HTTPS. This approach strengthens your security by encrypting all WinRM communication and ensuring that only trusted clients with valid certificates can authenticate—eliminating the need for password-based logins.

For additional debugging, check:

  • certlm.msc (Local Machine Certificates)
  • winrm get winrm/config (View WinRM configuration)
  • Windows Event Logs under Applications and Services Logs → Microsoft → Windows → WinRM

If you encounter issues, recheck the certificate trust, WinRM listener configuration, and firewall rules.

References

Chandra Mohan S

+ posts
Privacy Preferences
When you visit our website, it may store information through your browser from specific services, usually in form of cookies. Here you can change your privacy preferences. Please note that blocking some types of cookies may impact your experience on our website and the services we offer.