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

Prev Next

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.
  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.

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.jsonCsr.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.

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."