--- title: "Automate Windows server certificates using win-acme" slug: "certificate-winacme" updated: 2026-09-17T09:20:41Z published: 2026-09-17T09:20:41Z canonical: "guide.ncloud-docs.com/certificate-winacme" stale: true --- > ## Documentation Index > Fetch the complete documentation index at: https://guide.ncloud-docs.com/llms.txt > Use this file to discover all available pages before exploring further. # Automate Windows server certificates using win-acme Available in Classic and VPC This section explains how to use win-acme (an ACMEv2 client) to automatically issue and renew TLS certificates using the Certificate Manager ACME feature. Use the Ncloud Global DNS API for the DNS-01 challenge validation, and configure automatic renewal using Windows Task Scheduler. Caution Certificate Manager ACME only accepts RSA 2048 keys. Since the default key size for win-acme is RSA 3072, unless you change the settings, certificate issuance will fail even if the DNS-01 validation is successful. Be sure to change the key size to 2048 in `settings.json`. ## Service specifications | Item | Contents | | --- | --- | | Supported OS | Windows Server 2019, Windows Server 2022 | | Recommended clients | win-acme v2.2.9 or later (Standalone, 64-bit) | | Validation method | DNS-01 (Ncloud Global DNS API) | | Key algorithm | RSA 2048 / SHA256withRSA (required) | | Certificate Store | Windows Certificate Store (WebHosting) | | ACME endpoint | `https://acme.navercloudtrust.com/acme/directory` | ## Before you start Before you start this guide, you must have the following items ready. | Item | Items to check | | --- | --- | | Ncloud IAM API key | Access Key/Secret Key issuance complete. Global DNS permissions granted and confirmed. | | EAB key | EAB Key ID/HMAC Key issuance completed in the Certificate Manager console. | | Ncloud Global DNS | The domain for which the certificate is to be issued is registered as a zone in Global DNS. | | Administrator permissions | Local administrator privileges required to run wacs.exe and access the Windows certificate store | | IIS | Configuration of the site to which the HTTPS certificate will be applied is complete. | | PowerShell execution policy | Verify configuration of `RemoteSigned` | Note The EAB (External Account Binding) key is used only once when registering an ACME account. Once an account is registered, it is automatically reused for subsequent renewals, so there is no need to reissue the key. ## Step 1: Install and configure win-acme ### Install win-acme 1. Download the **release, trimmed, standalone, 64-bit** version of v2.2.9 or later from the [official win-acme release page](https://github.com/win-acme/win-acme/releases). 2. Extract the files to the desired directory. ### Modify settings.json Open the `settings.json` file in the win-acme installation directory and modify the following two items. #### 1. Modify ACME endpoint Modify the `DefaultBaseUri` value in the `Acme` section as shown below. The default for win-acme is Let's Encrypt, so you must modify it. ``` "Acme": { "DefaultBaseUri": "https://acme.navercloudtrust.com/acme/directory" } ``` #### 2. Modify RSA key size (required) Find the `Csr` section and modify it as shown below. ``` "Csr": { "Rsa": { "KeyBits": 2048, "SignatureAlgorithm": "SHA256withRSA" } } ``` Caution The default of `KeyBits` is `3072`. If you do not modify this value, the CA will process the order of `invalid` and refuse to issue the certificate, even if the DNS-01 validation is successful. Once the configuration is complete, the key items are as follows: | Key path | Settings | | --- | --- | | `Acme.DefaultBaseUri` | `https://acme.navercloudtrust.com/acme/directory` | | `Csr.Rsa.KeyBits` | `2048` | | `Csr.Rsa.SignatureAlgorithm` | `SHA256withRSA` | | `ScheduledTask.RenewalDays` | `45` (Example for a certificate with a validity period of 198 days. It is recommended to start the renewal at least one-third before the expiration date.) | ## Step 2: Prepare DNS hook script When performing the DNS-01 challenge, win-acme calls an external script prepared by the user to create and delete DNS TXT records. Since a DNS hook script is not provided separately, you'll need to write one yourself. For examples of how to use Ncloud Global DNS API, see [Ncloud Global DNS hook script example](/docs/en/certificate-winacme#NcloudGlobalDNS%ED%9B%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%98%88%EC%8B%9C). ### Hook script requirements During the DNS-01 challenge, win-acme calls the following two scripts in order. Each script must perform the following roles: **Create script (dns-create)** Called when win-acme starts the challenge. It must process the following tasks: 1. Search for the DNS zone corresponding to the domain provided. 2. Create `_acme-challenge.<도메인>` TXT record. 3. Apply the DNS changes. 4. Wait long enough for the DNS propagation to complete. (Recommended: 60 seconds or more) **Delete script (dns-delete)** Called when the challenge validation is complete. It must process the following tasks: 1. Delete the TXT record created by the create script. 2. Apply the DNS changes. ### Script call interface When calling the script, win-acme passes the following arguments in the order: You must follow this format when writing scripts. | Argument order | Value | Description | | --- | --- | --- | | 1 | `create` / `delete` | Action to perform | | 2 | Domain name | Domain for which the certificate will be issued (e.g., `example.com`) | | 3 | Record name | Full name of the TXT record (e.g., `_acme-challenge.example.com`) | | 4 | Token value | Challenge token issued by CA (set as the TXT record value) | ### Precautions for Ncloud Global DNS API integration When using Ncloud Global DNS API, keep the following points in mind when writing your script. - All API requests must include an HMAC-SHA256 signature header. You must include the following three headers: | Header | Value | | --- | --- | | `x-ncp-apigw-timestamp` | Request time (Unix milliseconds) | | `x-ncp-iam-access-key` | Ncloud IAM Access Key | | `x-ncp-apigw-signature-v2` | HMAC-SHA256 signature (Base64-encoded) | - After creating or deleting a record, you must call the Apply API for the change to take effect in DNS. - When issuing a wildcard certificate (`*.example.com`), you must remove `*.` from the beginning of the domain and search for to the zone. - When issuing a subdomain certificate (`sub.example.com`), you must calculate the hostname in `_acme-challenge.sub` format. Caution You must save script files using UTF-8 BOM encoding. If you save using ANSI (CP949) encoding, Korean characters will appear garbled when running PowerShell, and the script may malfunction in some environments. ## Step 3: Issue certificate Once the script is ready, run the following command in PowerShell with administrator permissions: Replace the item in parentheses (`[ ]`) with an actual value. ``` wacs.exe ` --source manual ` --host "[Domain for which the certificate will be issued]" ` --validation dnsscript ` --dnscreatescript "[Create script path]" ` --dnsdeletescript "[Delete script path]" ` --store certificatestore ` --certificatestore WebHosting ` --installation iis ` --siteid [IIS site ID] ` --eab-key-identifier "[EAB_KEY_ID]" ` --eab-key "[EAB_HMAC_KEY]" ` --emailaddress "[Contact person's email address]" ` --accepttos ` --baseuri "https://acme.navercloudtrust.com/acme/directory" ``` **Explanation of key parameters** | Parameter | Description | | --- | --- | | `--source manual` | Specify a domain manually | | `--host` | Domain for which the certificate will be issued | | `--validation dnsscript` | Use script-based DNS-01 validation | | `--dnscreatescript` | TXT record create script path | | `--dnsdeletescript` | TXT record delete script path | | `--certificatestore WebHosting` | Save certificate in WebHosting Store | | `--installation iis` / `--siteid` | Automatically bind certificates to IIS site | | `--eab-key-identifier` | EAB Key ID | | `--eab-key` | EAB HMAC Key | | `--accepttos` | Automatically agree to ACME Terms of Service | | `--baseuri` | ACME endpoint | **Issuance process** | Step | Contents | | --- | --- | | 1 | Verify ACME CA connection | | 2 | Register ACME account using EAB Key | | 3 | Create certificate order, receive DNS-01 challenge token | | 4 | Call the create script → Create a TXT record → Wait for DNS propagation | | 5 | CA verifies the TXT record → validation complete | | 6 | Call the delete script → Delete the TXT record | | 7 | Submit RSA 2048 CSR → CA signing | | 8 | Download certificate → Save in WebHosting Store | | 9 | Automatically replace IIS site Port 443 bindings | | 10 | Save renewal.json (to be used for automatic renewal later) | Note If a problem occurs, adding `--verbose` option to the command will output a detailed log, including HTTP requests and responses with the CA. We recommend using it when registering for the first time or when re-registering after replacing the EAB. ## Step 4: Set automatic renewal After registering the certificate, run the following command in PowerShell with administrator permissions to register the Task Scheduler automatic renewal job. ``` $action = New-ScheduledTaskAction ` -Execute "full path to wacs.exe" ` -Argument '--renew --baseuri "https://acme.navercloudtrust.com/acme/directory"' $trigger = New-ScheduledTaskTrigger -Daily -At "09:00" $principal = New-ScheduledTaskPrincipal ` -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet ` -ExecutionTimeLimit (New-TimeSpan -Hours 2) ` -StartWhenAvailable Register-ScheduledTask ` -TaskName "win-acme daily renew" ` -Action $action ` -Trigger $trigger ` -Principal $principal ` -Settings $settings ` -Force ``` Registered tasks run at 9:00 a.m. every day and automatically determine whether renewal is needed. If renewal is required, perform the DNS-01 challenge and replace the IIS binding without downtime. To check the renewal status, run the following command: ``` Get-ScheduledTaskInfo -TaskName "win-acme daily renew" | Select-Object LastRunTime, LastTaskResult, NextRunTime ``` | LastTaskResult | Meaning | | --- | --- | | `0` | Successfully renewed | | `267011` | No renewal required (normal) | | Other | Failed (Check the log) | The renewal log can be found in the `Log\log-YYYYMMDD.txt` file within the win-acme data directory. ## Verify certificate The issued certificate is saved in `WebHosting` Store of the Windows Certificate Store. Follow the steps below to check: ``` # Verify IIS binding certificate thumbprint Get-WebBinding | Where-Object { $_.protocol -eq "https" } | Select-Object bindingInformation, certificateHash # View certificate list in the WebHosting Store Get-ChildItem "Cert:\LocalMachine\WebHosting" | Select-Object Subject, Thumbprint, NotAfter, Issuer ``` Alternatively, you can check this in Windows Certificate Manager (`certlm.msc`) → Certificates (Local Computer) → Web Hosting → Certificates. ## Replace EAB key If you need to replace the EAB key: If an existing ACME account file remains, the new EAB key will be ignored and the existing account will be reused. Therefore, you must delete the account file first. 1. Replace the EAB value in the script or configuration file with the newly issued value. 2. Delete `Registration_v2`, `Signer_v2`, `*.renewal.json` files from the win-acme data directory. 3. Run the issuance command in Step 3 again. If successful, a certificate will be issued for the new account, and the IIS binding will be automatically replaced. You don't need to modify Task Scheduler jobs. ## Manual renewal Use the commands below when testing or responding to an emergency: ``` # Regular renewal (performed only when expiration conditions are met) wacs.exe --renew --baseuri "https://acme.navercloudtrust.com/acme/directory" # Forced renewal (performed immediately, regardless of conditions) wacs.exe --renew --baseuri "https://acme.navercloudtrust.com/acme/directory" --force # Forced renewal while outputting detailed logs wacs.exe --renew --baseuri "https://acme.navercloudtrust.com/acme/directory" --force --verbose # Run Task Scheduler jobs immediately Start-ScheduledTask -TaskName "win-acme daily renew" # View the list of registered renewals wacs.exe --list --baseuri "https://acme.navercloudtrust.com/acme/directory" ``` ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Order `invalid` (DNS validation is valid) | RSA key size mismatch | `settings.json` → `Csr.Rsa.KeyBits: 2048` Confirm | | Failed after authorization `pending` | DNS propagation delay | Increase the wait time for the create script to 90–120 seconds | | TXT record validation failed | Zone search failed | Verify in the console to see if the domain is registered as a zone in Global DNS | | API 401/403 errors | API key error or insufficient permission | Verify the validity of the Access Key and Global DNS permissions in Ncloud IAM | | Korean characters garbled in the log | Hook file encoding error | Resave the hook file with a UTF-8 BOM | | Failed quietly without errors | `--verbose` not used | Run again after adding `--verbose` option | | Reuse an existing account after replacing EAB | `Registration_v2` not deleted | See the [Replace EAB key](/docs/en/certificate-winacme#EAB%ED%82%A4%EA%B5%90%EC%B2%B4) procedure | | Task Scheduler failed | SYSTEM account lacks permissions | Task Properties → Confirm that **Run with highest permissions** is checked | | IIS binding not renewed | Certificate Store mismatch | Verify `--certificatestore WebHosting` parameter | Run the following command to view the log. ``` Get-Content "win-acme data path\Log\log-$(Get-Date -Format yyyyMMdd).txt" -Tail 100 ``` ## Security advisory - Do not include configuration files containing API keys or EAB keys in a Git repository or shared folder. - We recommend granting the Ncloud API key only the minimum permissions limited to the Global DNS service. - Restrict access to the win-acme data directory (which contains the ACME account signing key) to the administrator account. - Take care to ensure that the certificate's private key is not exposed to outside parties. - Keep win-acme and PowerShell updated to the latest versions on a regular basis. ## Ncloud Global DNS hook script example Below is an example of a DNS hook script that uses Ncloud Global DNS API. Caution The following script is provided as an example for reference. Review and test this thoroughly in your operating environment before use. The user is responsible for any modifications to the script, its configuration, and the results of its execution. ### API Key configuration file: config.ps1 ``` # ACME server (no need to change) $ACME_SERVER = "https://acme.navercloudtrust.com/acme/directory" # EAB key — Replace with the value issued by the Certificate Manager console $EAB_KID = "[EAB_KEY_ID]" $EAB_HMAC_KEY = "[EAB_HMAC_KEY]" # NCP Global DNS API key — Issued by NCP IAM console (Global DNS permissions required) $NCP_ACCESS_KEY = "[NCP_ACCESS_KEY]" $NCP_SECRET_KEY = "[NCP_SECRET_KEY]" $NCP_DNS_API = "https://globaldns.apigw.ntruss.com" # DNS propagation delay (seconds). If validation fails, increase it to 90–120. $DNS_PROPAGATION_WAIT = 60 # ACME account email $ACME_EMAIL = "[ADMIN_EMAIL]" ``` ### TXT record create hook: dns-create.ps1 ``` # dns-create.ps1 — win-acme DNS-01 create hook (NCP Global DNS) # Save using UTF-8 BOM encoding param( [string]$Action, [string]$Domain, [string]$RecordName, [string]$Token ) $ConfigPath = Join-Path $PSScriptRoot "config.ps1" . $ConfigPath function Get-NcpHeaders { param([string]$Method, [string]$Uri) $timestamp = [long]([datetimeoffset]::UtcNow.ToUnixTimeMilliseconds()) $message = "$Method $Uri`n$timestamp`n$NCP_ACCESS_KEY" $hmac = New-Object System.Security.Cryptography.HMACSHA256 $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($NCP_SECRET_KEY) $sig = [Convert]::ToBase64String( $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($message)) ) return @{ "x-ncp-apigw-timestamp" = "$timestamp" "x-ncp-iam-access-key" = $NCP_ACCESS_KEY "x-ncp-apigw-signature-v2" = $sig "Content-Type" = "application/json" } } # Wildcard processing $Domain = $Domain -replace "^\*\.", "" # Zone search $zoneUri = "/v1/ncpdns/domain?page=1&size=100" $zones = (Invoke-RestMethod -Uri "$NCP_DNS_API$zoneUri" ` -Headers (Get-NcpHeaders "GET" $zoneUri)).domainList $matchedZone = $zones | Where-Object { $Domain -eq $_.name -or $Domain.EndsWith(".$($_.name)") } | Select-Object -First 1 if (-not $matchedZone) { Write-Error "[Error] The zone corresponding to ‘$Domain’ cannot be found in NCP Global DNS." exit 1 } $zoneId = $matchedZone.domainId $zoneName = $matchedZone.name # Calculate TXT record host name if ($Domain -eq $zoneName) { $hostName = "_acme-challenge" } else { $sub = $Domain -replace "\.$([regex]::Escape($zoneName))$", "" $hostName = "_acme-challenge.$sub" } # Create TXT record $createUri = "/v1/ncpdns/record/$zoneId" $body = @{ type = "TXT"; host = $hostName; content = $Token; ttl = 60 } | ConvertTo-Json $response = Invoke-RestMethod -Uri "$NCP_DNS_API$createUri" -Method POST ` -Headers (Get-NcpHeaders "POST" $createUri) -Body $body if (-not $response.recordId) { Write-Error "[Error] Failed to create TXT record." exit 1 } # Temporarily store Record ID (used in the delete hook) $tmpFile = "$env:TEMP\ncp_sid_$($Domain -replace '\*','_').json" @{ recordId = $response.recordId; domainId = $zoneId } | ConvertTo-Json | Set-Content $tmpFile -Encoding UTF8 # Reflect DNS $applyUri = "/v1/ncpdns/domain/$zoneId/apply" Invoke-RestMethod -Uri "$NCP_DNS_API$applyUri" -Method PUT ` -Headers (Get-NcpHeaders "PUT" $applyUri) | Out-Null Write-Host "DNS TXT record creation complete. Waiting for $DNS_PROPAGATION_WAIT second(s)..." Start-Sleep -Seconds $DNS_PROPAGATION_WAIT ``` ### TXT record delete hook: dns-delete.ps1 ``` # dns-delete.ps1 — win-acme DNS-01 delete hook (NCP Global DNS) # Save using UTF-8 BOM encoding param( [string]$Action, [string]$Domain, [string]$RecordName, [string]$Token ) $ConfigPath = Join-Path $PSScriptRoot "config.ps1" . $ConfigPath function Get-NcpHeaders { param([string]$Method, [string]$Uri) $timestamp = [long]([datetimeoffset]::UtcNow.ToUnixTimeMilliseconds()) $message = "$Method $Uri`n$timestamp`n$NCP_ACCESS_KEY" $hmac = New-Object System.Security.Cryptography.HMACSHA256 $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($NCP_SECRET_KEY) $sig = [Convert]::ToBase64String( $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($message)) ) return @{ "x-ncp-apigw-timestamp" = "$timestamp" "x-ncp-iam-access-key" = $NCP_ACCESS_KEY "x-ncp-apigw-signature-v2" = $sig } } $Domain = $Domain -replace "^\*\.", "" $tmpFile = "$env:TEMP\ncp_sid_$($Domain -replace '\*','_').json" $sid = Get-Content $tmpFile -ErrorAction SilentlyContinue | ConvertFrom-Json if (-not $sid) { Write-Warning "[Warning] The record to be deleted cannot be found." exit 0 } # Delete TXT record $deleteUri = "/v1/ncpdns/record/$($sid.domainId)/$($sid.recordId)" Invoke-RestMethod -Uri "$NCP_DNS_API$deleteUri" -Method DELETE ` -Headers (Get-NcpHeaders "DELETE" $deleteUri) | Out-Null # Reflect DNS $applyUri = "/v1/ncpdns/domain/$($sid.domainId)/apply" Invoke-RestMethod -Uri "$NCP_DNS_API$applyUri" -Method PUT ` -Headers (Get-NcpHeaders "PUT" $applyUri) | Out-Null Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue Write-Host "DNS TXT record deletion complete." ```