Win-ACME is a free, open-source tool designed specifically for Windows servers that makes getting and renewing SSL/TLS certificates completely automatic. You do not need to be a developer or a security expert to use it — this guide walks you through every step in plain language.
An SSL certificate is what puts the padlock icon in your browser's address bar and enables the secure HTTPS connection for your website. Without it, browsers warn visitors that your site is 'not secure'. Win-ACME takes care of obtaining, installing, and automatically renewing these certificates so you never have to worry about them expiring.
Win-ACME gives you two ways to request a certificate: using typed commands (CLI Mode — best for automation) or using a menu-based screen (Interactive GUI Mode — best for beginners).
This method uses PowerShell commands and is ideal when you want to automate certificate issuance or need to repeat the same process on multiple servers.
Right-click on the Start Menu and choose 'Windows PowerShell (Admin)'. You must run as Administrator or the commands will fail.
This command reads your saved settings (API keys, email, etc.) from the credentials file so you do not have to type them manually each time.
$config = Get-Content "C:\ProgramData\win-acme-config\acme-credentials.txt" | ConvertFrom-StringData
cd C:\Tools\win-acme
Use this command if you need a certificate for just one website, for example: demo.example.com
.\wacs.exe `
--source manual `
--host "demo.example.com" `
--validation cloudflare `
--validationmode dns-01 `
--cloudflareapitoken "$($config.CF_API_TOKEN)" `
--store pemfiles `
--pemfilespath "$($config.CERT_PATH)" `
--installation none `
--accepttos `
--emailaddress "$($config.EMAIL)" `
--baseuri "$($config.ACME_SERVER)" `
--eab-key-identifier "$($config.EAB_KEY_IDENTIFIER)" `
--eab-key "$($config.EAB_KEY)"
If you want one certificate to cover several domains at once — for example, your main domain, www version, and mail server — use this command and list all domains separated by commas.
.\wacs.exe `
--source manual `
--host "example.com,www.example.com,mail.example.com" `
--validation cloudflare `
--validationmode dns-01 `
--cloudflareapitoken "$($config.CF_API_TOKEN)" `
--store pemfiles `
--pemfilespath "$($config.CERT_PATH)" `
--installation none `
--accepttos `
--emailaddress "$($config.EMAIL)" `
--baseuri "$($config.ACME_SERVER)" `
--eab-key-identifier "$($config.EAB_KEY_IDENTIFIER)" `
--eab-key "$($config.EAB_KEY)"
A wildcard certificate covers your main domain AND all sub-domains (like blog.example.com, shop.example.com, etc.) with a single certificate. It uses *.example.com notation.
.\wacs.exe `
--source manual `
--host "*.example.com,example.com" `
--validation cloudflare `
--validationmode dns-01 `
--cloudflareapitoken "$($config.CF_API_TOKEN)" `
--store pemfiles `
--pemfilespath "$($config.CERT_PATH)" `
--installation none `
--accepttos `
--emailaddress "$($config.EMAIL)" `
--baseuri "$($config.ACME_SERVER)" `
--eab-key-identifier "$($config.EAB_KEY_IDENTIFIER)" `
--eab-key "$($config.EAB_KEY)"
If you prefer clicking through a menu rather than typing commands, Win-ACME has a built-in interactive mode. Follow these steps:
cd C:\Tools\win-acme
.\wacs.exe
After launching, you will see a text menu. Follow the selections in order:
When complete, you should see a success message similar to:
[INFO] Requesting certificate
[INFO] Validating domain(s)
[INFO] DNS validation successful
[INFO] Certificate issued successfully
[INFO] Certificate stored at: C:\ssl-certs\demo.example.com-chain.pem
After a certificate is issued, Win-ACME saves several files to the storage folder (usually C:\ssl-certs\). Each file has a different purpose. Here is what each one means:
| File Name | What It Is | When to Use It |
|---|---|---|
| *-chain.pem | Full certificate + intermediate chain | Use this for most web servers (IIS, Apache, Nginx) |
| *-crt.pem | Your certificate only (no chain) | Use when the web server needs cert and chain as separate files |
| *-key.pem | Your private key | Required by all web servers — NEVER share this file! |
| *-chain-only.pem | Intermediate certificates only | Used when the server needs the chain separately |
SSL certificates expire after 90 days. Win-ACME handles renewal automatically by creating a Windows Scheduled Task the first time you issue a certificate. This task runs daily and renews the certificate if it is within 30 days of expiry. You do not need to do anything manually.
Run this command to confirm the automatic renewal task exists:
Get-ScheduledTask -TaskName "win-acme renew*" | Format-List
# To see more details including last run time:
Get-ScheduledTaskInfo -TaskName "win-acme renew (acme-v02.api.letsencrypt.org)"
If you run the above check and no task is found, you can create one yourself using the PowerShell script below. Copy and run the entire block at once:
$action = New-ScheduledTaskAction `
-Execute "C:\Tools\win-acme\wacs.exe" `
-Argument "--renew --baseuri `"https://acme.example.com/acme/directory`" --force" `
-WorkingDirectory "C:\Tools\win-acme"
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
$principal = New-ScheduledTaskPrincipal `
-UserId "SYSTEM" `
-LogonType ServiceAccount `
-RunLevel Highest
$settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Hours 2) `
-RestartCount 3 `
-RestartInterval (New-TimeSpan -Minutes 10)
Register-ScheduledTask `
-TaskName "Win-ACME Certificate Renewal" `
-Action $action `
-Trigger $trigger `
-Principal $principal `
-Settings $settings `
-Description "Automatically renews SSL certificates using Win-ACME"
Write-Host "Scheduled task created successfully!"
Before relying on automatic renewal, test it manually to make sure everything is working. Use the --force flag to force a renewal even if the certificate is not about to expire:
cd C:\Tools\win-acme
# Force renewal (use for testing):
.\wacs.exe --renew --baseuri "https://acme.example.com/acme/directory" --force --nocache --verbose
# Normal renewal check (only renews if expiring within 30 days):
.\wacs.exe --renew --baseuri "https://acme.example.com/acme/directory"
If you are running websites through IIS (Internet Information Services), Win-ACME can automatically install certificates directly into IIS and update the HTTPS binding. This means when a certificate renews, IIS is updated too — no manual steps needed.
This is the simplest option. The command below issues the certificate AND installs it into IIS in one step.
First, find your IIS Site ID by running:
Import-Module WebAdministration
Get-Website | Select-Object Name, Id, State, Bindings
Look for your website name in the results — note the number in the Id column. The default website usually has Id = 1. Now run the certificate issuance command, replacing --siteid 1 with your actual site ID:
.\wacs.exe `
--source manual `
--host "demo.example.com" `
--validation cloudflare `
--validationmode dns-01 `
--cloudflareapitoken "$($config.CF_API_TOKEN)" `
--store certificatestore `
--installation iis `
--siteid 1 `
--accepttos `
--emailaddress "$($config.EMAIL)" `
--baseuri "$($config.ACME_SERVER)" `
--eab-key-identifier "$($config.EAB_KEY_IDENTIFIER)" `
--eab-key "$($config.EAB_KEY)"
Use this option if you already have the PEM certificate files and just want to update IIS to use them. This script converts the PEM files to PFX format (which Windows uses), imports the certificate, and updates the IIS binding.
Import-Module WebAdministration
# Set your values here
$siteName = "Default Web Site"
$domain = "demo.example.com"
$certPath = "C:\ssl-certs"
$pfxPassword = "SecurePassword123!" # Change this to something strong!
# File paths
$certFile = "$certPath\$domain-chain.pem"
$keyFile = "$certPath\$domain-key.pem"
$pfxFile = "$certPath\$domain.pfx"
$opensslPath = "C:\Program Files\Git\usr\bin\openssl.exe"
# Step 1: Convert PEM to PFX
if (Test-Path $opensslPath) {
& $opensslPath pkcs12 -export -out $pfxFile -inkey $keyFile -in $certFile -password "pass:$pfxPassword"
} else {
Write-Warning "OpenSSL not found. Install Git for Windows or OpenSSL"
}
# Step 2: Import certificate to Windows Certificate Store
$securePwd = ConvertTo-SecureString -String $pfxPassword -AsPlainText -Force
$cert = Import-PfxCertificate -FilePath $pfxFile -CertStoreLocation Cert:\LocalMachine\My -Password $securePwd
Write-Host "Certificate imported: $($cert.Thumbprint)"
# Step 3: Update IIS HTTPS binding
$binding = Get-WebBinding -Name $siteName -Protocol "https"
if ($binding) { Remove-WebBinding -Name $siteName -Protocol "https" -Port 443 }
New-WebBinding -Name $siteName -Protocol "https" -Port 443 -IPAddress "*" -HostHeader $domain
$binding = Get-WebBinding -Name $siteName -Protocol "https"
$binding.AddSslCertificate($cert.Thumbprint, "My")
Write-Host "IIS binding updated successfully!"
This option creates a script that Win-ACME will automatically run every time the certificate is issued or renewed — completely hands-free.
First, create the deployment script and save it as C:\Tools\win-acme\Scripts\Deploy-IIS.ps1:
param(
[Parameter(Mandatory)]
[string]$NewCertThumbprint
)
Import-Module WebAdministration
$siteName = "Default Web Site"
$cert = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $NewCertThumbprint }
if ($cert) {
$binding = Get-WebBinding -Name $siteName -Protocol "https" -Port 443
if ($binding) {
$binding.AddSslCertificate($NewCertThumbprint, "My")
Write-Host "IIS binding updated with new certificate"
}
} else {
Write-Error "Certificate not found: $NewCertThumbprint"
}
Then run Win-ACME with the script installation option:
.\wacs.exe `
--source manual `
--host "demo.example.com" `
--validation cloudflare `
--validationmode dns-01 `
--cloudflareapitoken "$($config.CF_API_TOKEN)" `
--store certificatestore `
--installation script `
--script "C:\Tools\win-acme\Scripts\Deploy-IIS.ps1" `
--scriptparameters "{NewCertThumbprint}" `
--accepttos `
--emailaddress "$($config.EMAIL)" `
--baseuri "$($config.ACME_SERVER)" `
--eab-key-identifier "$($config.EAB_KEY_IDENTIFIER)" `
--eab-key "$($config.EAB_KEY)"
This section covers how to deploy certificates to different types of web servers and cloud platforms. Each scenario uses a custom script that Win-ACME runs automatically after issuing or renewing a certificate.
Save the following script as C:\Tools\win-acme\Scripts\Deploy-Apache.ps1 — Win-ACME will run it automatically after each renewal:
param(
[Parameter(Mandatory)] [string]$CertificatePath,
[Parameter(Mandatory)] [string]$CertificatePassword
)
$domain = "demo.example.com"
$apachePath = "C:\Apache24"
$sslPath = "$apachePath\conf\ssl"
$opensslPath = "C:\Program Files\Git\usr\bin\openssl.exe"
$pfxFile = "C:\ssl-certs\$domain.pfx"
# Create SSL folder if it does not exist
New-Item -ItemType Directory -Path $sslPath -Force
# Extract certificate and private key from PFX
& $opensslPath pkcs12 -in $pfxFile -out "$sslPath\$domain.crt" -clcerts -nokeys -password "pass:$CertificatePassword"
& $opensslPath pkcs12 -in $pfxFile -out "$sslPath\$domain.key" -nocerts -nodes -password "pass:$CertificatePassword"
& $opensslPath pkcs12 -in $pfxFile -out "$sslPath\$domain-chain.crt" -cacerts -nokeys -password "pass:$CertificatePassword"
# Restart Apache to load the new certificate
Restart-Service -Name "Apache2.4" -Force
Write-Host "Apache certificates deployed successfully"
Save this script as C:\Tools\win-acme\Scripts\Deploy-Nginx.ps1:
param(
[Parameter(Mandatory)] [string]$CertificatePath
)
$domain = "demo.example.com"
$nginxPath = "C:\nginx"
$sslPath = "$nginxPath\conf\ssl"
$sourcePath = "C:\ssl-certs"
# Create SSL folder if it does not exist
New-Item -ItemType Directory -Path $sslPath -Force
# Copy PEM files to Nginx SSL folder
Copy-Item "$sourcePath\$domain-chain.pem" "$sslPath\$domain.crt" -Force
Copy-Item "$sourcePath\$domain-key.pem" "$sslPath\$domain.key" -Force
Write-Host "Nginx certificates copied to: $sslPath"
# Reload Nginx to apply the new certificate
$nginxExe = "$nginxPath\nginx.exe"
if (Test-Path $nginxExe) {
& $nginxExe -s reload
Write-Host "Nginx reloaded successfully"
}
If you need to push the same certificate to several servers simultaneously (for example, a load-balanced cluster), save this script as C:\Tools\win-acme\Scripts\Deploy-Remote.ps1. It connects to each server using Windows Remote Management (WinRM).
param([Parameter(Mandatory)] [string]$CertificatePath)
$domain = "demo.example.com"
$sourcePath = "C:\ssl-certs"
$remoteServers = @("server1.local", "server2.local", "server3.local")
foreach ($server in $remoteServers) {
Write-Host "Deploying to: $server"
$session = New-PSSession -ComputerName $server
# Copy the certificate files to the remote server
Copy-Item "$sourcePath\$domain-chain.pem" -Destination "C:\ssl-certs\" -ToSession $session
Copy-Item "$sourcePath\$domain-key.pem" -Destination "C:\ssl-certs\" -ToSession $session
# Restart IIS on the remote server
Invoke-Command -Session $session -ScriptBlock {
Import-Module WebAdministration
iisreset /noforce
}
Remove-PSSession $session
Write-Host "Deployment to $server completed"
}
If your organisation uses Microsoft Azure, this script uploads the certificate to Azure Key Vault after every renewal. Save as C:\Tools\win-acme\Scripts\Deploy-Azure.ps1:
param(
[Parameter(Mandatory)] [string]$CertificatePath,
[Parameter(Mandatory)] [string]$CertificatePassword
)
$domain = "demo.example.com"
$pfxFile = "C:\ssl-certs\$domain.pfx"
$vaultName = "mykeyvault"
$certName = "demo-example-com"
# Upload the certificate to Azure Key Vault
$securePwd = ConvertTo-SecureString -String $CertificatePassword -AsPlainText -Force
Import-AzKeyVaultCertificate `
-VaultName $vaultName `
-Name $certName `
-FilePath $pfxFile `
-Password $securePwd
Write-Host "Certificate uploaded to Azure Key Vault: $vaultName"
Plesk is a popular web hosting control panel. Because Plesk on Windows does not support custom ACME servers through its web interface, you must use Win-ACME directly on the server and then import the certificate using Plesk command-line tools.
# Step 1: Go to the Plesk bin directory
cd "C:\Program Files (x86)\Plesk\bin"
# Step 2: Import the certificate into Plesk
plesk bin certificate.exe -c "demoexamplecom20260209" `
-domain "demo.example.com" `
-key-file "C:\ssl-certs\demo.example.com-key.pem" `
-cert-file "C:\ssl-certs\demo.example.com-crt.pem" `
-cacert-file "C:\ssl-certs\demo.example.com-chain.pem"
# You should see: SSL/TLS certificate 'demoexamplecom20260209' was successfully created
# Step 3: Assign the certificate to your domain and enable HTTPS
plesk bin site.exe -u "demo.example.com" -ssl true -certificate-name "demoexamplecom20260209"
# You should see: SUCCESS: Update of domain 'demo.example.com' completed.
Save this script as C:\Tools\win-acme\Scripts\Deploy-Plesk.ps1:
param(
[Parameter(Mandatory=$true)] [string]$Domain,
[Parameter(Mandatory=$false)] [string]$CertPath = "C:\ssl-certs"
)
$ErrorActionPreference = "Stop"
$pleskBinPath = "C:\Program Files (x86)\Plesk\bin"
# Create a unique certificate name (letters and numbers only)
$certName = ($Domain -replace '\W', '') + "-$(Get-Date -Format 'yyyyMMdd')"
$keyFile = "$CertPath\$Domain-key.pem"
$certFile = "$CertPath\$Domain-crt.pem"
$chainFile = "$CertPath\$Domain-chain.pem"
# Check that all required files exist before proceeding
if (!(Test-Path $keyFile)) { throw "Key file not found: $keyFile" }
if (!(Test-Path $certFile)) { throw "Certificate file not found: $certFile" }
if (!(Test-Path $chainFile)) { throw "Chain file not found: $chainFile" }
Write-Host "Importing certificate for: $Domain" -ForegroundColor Cyan
try {
# Import certificate to Plesk
$importCmd = "& '$pleskBinPath\certificate.exe' -c '$certName' -domain '$Domain' -key-file '$keyFile' -cert-file '$certFile' -cacert-file '$chainFile'"
Invoke-Expression $importCmd
if ($LASTEXITCODE -ne 0) { throw "Failed to import certificate (Exit code: $LASTEXITCODE)" }
Write-Host "Certificate imported: $certName" -ForegroundColor Green
# Assign certificate to domain and enable HTTPS
$assignCmd = "& '$pleskBinPath\site.exe' -u '$Domain' -ssl true -certificate-name '$certName'"
Invoke-Expression $assignCmd
if ($LASTEXITCODE -ne 0) { throw "Failed to assign certificate (Exit code: $LASTEXITCODE)" }
Write-Host "SSL enabled for: https://$Domain" -ForegroundColor Green
} catch {
Write-Error "Deployment failed: $_"
exit 1
}
To run the script manually:
.\Deploy-Plesk.ps1 -Domain "demo.example.com"
To integrate it with Win-ACME so it runs automatically on every renewal:
.\wacs.exe `
--source manual `
--host "demo.example.com" `
--validationmode dns-01 `
--validation cloudflare `
--cloudflareapitoken "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" `
--store pemfiles `
--pemfilespath "C:\ssl-certs" `
--installation script `
--script "C:\Tools\win-acme\Scripts\Deploy-Plesk.ps1" `
--scriptparameters "-Domain '{Identifier}'" `
--baseuri "https://acme.example.com/acme/directory" `
--emailaddress "admin@example.com" `
--accepttos `
--eab-key-identifier "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" `
--eab-key "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
This all-in-one script handles both new certificate issuance and renewals. Save it as C:\Tools\win-acme\Scripts\Complete-Automation.ps1 and use it as your single entry point for all certificate operations.
param(
[Parameter(Mandatory=$false)] [string]$Domain = "demo.example.com",
[Parameter(Mandatory=$false)] [switch]$ForceRenewal
)
$wacsPath = "C:\Tools\win-acme"
$configFile = "C:\ProgramData\win-acme-config\acme-credentials.txt"
# Load saved credentials
$config = Get-Content $configFile | ConvertFrom-StringData
Set-Location $wacsPath
try {
if ($ForceRenewal) {
Write-Host "Force renewing certificate for: $Domain" -ForegroundColor Cyan
.\wacs.exe --renew --baseuri "$($config.ACME_SERVER)" --force --nocache --verbose
} else {
Write-Host "Checking certificate renewal for: $Domain" -ForegroundColor Cyan
.\wacs.exe --renew --baseuri "$($config.ACME_SERVER)"
}
if ($LASTEXITCODE -eq 0) {
Write-Host "Certificate operation completed successfully!" -ForegroundColor Green
exit 0
} else {
Write-Error "Certificate operation failed with exit code: $LASTEXITCODE"
exit 1
}
} catch {
Write-Error "Error: $_"
exit 1
}
To create a scheduled task that runs this script daily at 2 AM:
$action = New-ScheduledTaskAction `
-Execute "PowerShell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\Tools\win-acme\Scripts\Complete-Automation.ps1" `
-WorkingDirectory "C:\Tools\win-acme"
$trigger = New-ScheduledTaskTrigger -Daily -At 2am
$principal = New-ScheduledTaskPrincipal `
-UserId "SYSTEM" `
-LogonType ServiceAccount `
-RunLevel Highest
Register-ScheduledTask `
-TaskName "Win-ACME Automated Renewal" `
-Action $action `
-Trigger $trigger `
-Principal $principal
Once everything is running, you should regularly check that your certificates are healthy and renewals are happening on schedule.
cd C:\Tools\win-acme
.\wacs.exe --list
# Check certificate in Windows Certificate Store:
Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*demo.example.com*" } | Format-List
# Check PEM files in certificate folder:
Get-ChildItem C:\ssl-certs\*.pem | Select-Object Name, LastWriteTime, Length
Win-ACME writes a detailed log file every day. Logs are stored in: C:\ProgramData\win-acme\Log\
# Read last 50 lines of today's log:
Get-Content "C:\ProgramData\win-acme\Log\log-$(Get-Date -Format 'yyyy-MM-dd').txt" -Tail 50
# Watch log update in real time (useful during a renewal):
Get-Content "C:\ProgramData\win-acme\Log\log-$(Get-Date -Format 'yyyy-MM-dd').txt" -Wait -Tail 10
$cert = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*demo.example.com*" }
$daysLeft = ($cert.NotAfter - (Get-Date)).Days
Write-Host "Certificate expires in $daysLeft days" -ForegroundColor $(if ($daysLeft -lt 30) { "Red" } else { "Green" })# Open website in default browser:
Start-Process "https://demo.example.com"
# Test via PowerShell (should return status 200):
$result = Invoke-WebRequest -Uri "https://demo.example.com" -UseBasicParsing
$result.StatusCode
Win-ACME verifies that you own a domain by creating a temporary DNS record (DNS-01 validation). It supports 20+ DNS providers. Here are the most common ones:
| DNS Provider | Command Option to Add |
|---|---|
| Cloudflare | --validation cloudflare --cloudflareapitoken "your_token" |
| Azure DNS | --validation azure --azuresubscriptionid "sub-id" --azuretenantid "tenant-id" |
| GoDaddy | --validation godaddy --godaddyapikey "your_key" --godaddyapisecret "your_secret" |
| AWS Route 53 |
|