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.
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 |
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
- Download the release, trimmed, standalone, 64-bit version of v2.2.9 or later from the official win-acme release page.
- 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"
}
}
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.
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:
- Search for the DNS zone corresponding to the domain provided.
- Create
_acme-challenge.<도메인>TXT record. - Apply the DNS changes.
- 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:
- Delete the TXT record created by the create script.
- 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.subformat.
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) |
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.
- Replace the EAB value in the script or configuration file with the newly issued value.
- Delete
Registration_v2,Signer_v2,*.renewal.jsonfiles from the win-acme data directory. - 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 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.
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."