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.