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.

Windows 서버

Prev Next

The latest service changes have not yet been reflected in this content. We will update the content as soon as possible. Please refer to the Korean version for information on the latest updates.

Classic/VPC 환경에서 이용 가능합니다.

win-acme(ACMEv2 클라이언트)를 사용하여 Certificate Manager ACME 기능으로 TLS 인증서를 자동으로 발급·갱신하는 방법을 설명합니다. DNS-01 챌린지 검증에 Ncloud Global DNS API를 사용하며, Windows Task Scheduler로 자동 갱신을 구성합니다.

주의

Certificate Manager ACME는 RSA 2048 키만 허용합니다. win-acme의 기본 키 크기는 RSA 3072이므로, 설정을 변경하지 않으면 DNS-01 검증에 성공하더라도 인증서 발급이 실패합니다. 반드시 settings.json에서 키 크기를 2048로 변경하십시오.

서비스 사양

항목 내용
지원 OS Windows Server 2019, Windows Server 2022
권장 클라이언트 win-acme v2.2.9 이상 (Standalone, 64-bit)
검증 방식 DNS-01 (Ncloud Global DNS API)
키 알고리즘 RSA 2048 / SHA256withRSA (필수)
인증서 저장소 Windows Certificate Store (WebHosting)
ACME 엔드포인트 https://acme.navercloudtrust.com/acme/directory

시작하기 전에

이 가이드를 진행하기 전에 다음 항목이 준비되어 있어야 합니다.

항목 확인 내용
Ncloud IAM API 키 Access Key/Secret Key 발급 완료, Global DNS 권한 부여 확인
EAB 키 Certificate Manager 콘솔에서 EAB Key ID/HMAC Key 발급 완료
Ncloud Global DNS 인증서를 발급할 도메인이 Global DNS에 존으로 등록되어 있음
관리자 권한 wacs.exe 실행 및 Windows 인증서 저장소 접근을 위한 로컬 관리자 권한
IIS HTTPS 인증서를 적용할 사이트 구성 완료
PowerShell 실행 정책 RemoteSigned 이상 설정 확인
참고

EAB(External Account Binding) 키는 ACME 계정 등록 시 1회만 사용됩니다. 한 번 등록된 계정은 이후 갱신에 자동 재사용되므로 키를 재발급할 필요가 없습니다.

1단계: win-acme 설치 및 설정

win-acme 설치

  1. win-acme 공식 릴리스 페이지에서 v2.2.9 이상의 release, trimmed, standalone, 64-bit 버전을 다운로드합니다.
  2. 원하는 경로에 압축을 해제합니다.

settings.json 수정

win-acme 설치 디렉터리의 settings.json 파일을 열어 아래 두 항목을 반드시 수정합니다.

1. ACME 엔드포인트 변경

Acme 섹션의 DefaultBaseUri 값을 아래와 같이 변경합니다. win-acme 기본값은 Let's Encrypt이므로 반드시 변경해야 합니다.

"Acme": {
  "DefaultBaseUri": "https://acme.navercloudtrust.com/acme/directory"
}

2. RSA 키 크기 변경 (필수)

Csr 섹션을 찾아 아래와 같이 수정합니다.

"Csr": {
  "Rsa": {
    "KeyBits": 2048,
    "SignatureAlgorithm": "SHA256withRSA"
  }
}
주의

KeyBits 기본값은 3072입니다. 이 값을 변경하지 않으면 DNS-01 검증이 성공하더라도 CA가 order를 invalid 처리하며 인증서 발급을 거부합니다.

설정 완료 후 핵심 항목은 다음과 같습니다.

키 경로 설정값
Acme.DefaultBaseUri https://acme.navercloudtrust.com/acme/directory
Csr.Rsa.KeyBits 2048
Csr.Rsa.SignatureAlgorithm SHA256withRSA
ScheduledTask.RenewalDays 45 (인증서 유효기간 198일 일 때 예시. 만료 시점 1/3 전부터 갱신 시도 권장)

2단계: DNS 훅 스크립트 준비

win-acme는 DNS-01 챌린지 수행 시 사용자가 준비한 외부 스크립트를 호출하여 DNS TXT 레코드를 생성·삭제합니다. DNS 훅 스크립트는 별도로 제공되지 않으므로 직접 작성해야 합니다. Ncloud Global DNS API를 사용하는 예시는 Ncloud Global DNS 훅 스크립트 예시를 참조해 주십시오.

훅 스크립트 요구 사항

win-acme는 DNS-01 챌린지 시 아래 두 스크립트를 순서대로 호출합니다. 각 스크립트는 다음 역할을 수행해야 합니다.

생성 스크립트 (dns-create)
win-acme가 챌린지를 시작할 때 호출됩니다. 다음을 처리해야 합니다.

  1. 전달받은 도메인에 해당하는 DNS zone을 탐색합니다.
  2. _acme-challenge.<도메인> TXT 레코드를 생성합니다.
  3. DNS 변경 사항을 반영합니다.
  4. DNS 전파가 완료될 때까지 충분히 대기합니다. (권장: 60초 이상)

삭제 스크립트 (dns-delete)
챌린지 검증이 완료된 후 호출됩니다. 다음을 처리해야 합니다.

  1. 생성 스크립트에서 만든 TXT 레코드를 삭제합니다.
  2. DNS 변경 사항을 반영합니다.

스크립트 호출 인터페이스

win-acme는 스크립트를 호출할 때 아래 인수를 순서대로 전달합니다. 스크립트 작성 시 이 형식을 따라야 합니다.

인수 순서 설명
1 create / delete 수행할 동작
2 도메인명 인증서를 발급할 도메인 (예: example.com)
3 레코드명 TXT 레코드 전체 이름 (예: _acme-challenge.example.com)
4 토큰 값 CA가 발행한 챌린지 토큰 (TXT 레코드 값으로 설정)

Ncloud Global DNS API 연동 시 유의 사항

Ncloud Global DNS API를 사용하는 경우 아래 사항을 고려하여 스크립트를 작성하십시오.

  • 모든 API 요청에는 HMAC-SHA256 서명 헤더가 필요합니다. 아래 세 헤더를 포함해야 합니다.
헤더
x-ncp-apigw-timestamp 요청 시각 (Unix 밀리초)
x-ncp-iam-access-key Ncloud IAM Access Key
x-ncp-apigw-signature-v2 HMAC-SHA256 서명값 (Base64 인코딩)
  • 레코드 생성·삭제 후에는 반드시 Apply API를 호출해야 DNS에 반영됩니다.
  • 와일드카드 인증서(*.example.com) 발급 시 도메인 앞의 *.를 제거하고 zone을 탐색해야 합니다.
  • 서브도메인 인증서(sub.example.com) 발급 시 _acme-challenge.sub 형태로 호스트명을 계산해야 합니다.
주의

스크립트 파일은 반드시 UTF-8 BOM 인코딩으로 저장하십시오. ANSI(CP949) 인코딩으로 저장하면 PowerShell 실행 시 한글이 깨지고 일부 환경에서 오동작합니다.

3단계: 인증서 발급

스크립트 준비가 완료되면 관리자 권한 PowerShell에서 아래 명령을 실행합니다. 대괄호([ ]) 항목은 실제 값으로 교체하십시오.

wacs.exe `
    --source manual `
    --host "[인증서를 발급할 도메인]" `
    --validation dnsscript `
    --dnscreatescript "[생성 스크립트 경로]" `
    --dnsdeletescript "[삭제 스크립트 경로]" `
    --store certificatestore `
    --certificatestore WebHosting `
    --installation iis `
    --siteid [IIS 사이트 ID] `
    --eab-key-identifier "[EAB_KEY_ID]" `
    --eab-key "[EAB_HMAC_KEY]" `
    --emailaddress "[담당자 이메일]" `
    --accepttos `
    --baseuri "https://acme.navercloudtrust.com/acme/directory"

주요 파라미터 설명

파라미터 설명
--source manual 도메인을 직접 지정
--host 인증서를 발급할 도메인
--validation dnsscript 스크립트 기반 DNS-01 검증 사용
--dnscreatescript TXT 레코드 생성 스크립트 경로
--dnsdeletescript TXT 레코드 삭제 스크립트 경로
--certificatestore WebHosting 인증서를 WebHosting 스토어에 저장
--installation iis / --siteid IIS 사이트에 인증서 자동 바인딩
--eab-key-identifier EAB Key ID
--eab-key EAB HMAC Key
--accepttos ACME 약관 자동 동의
--baseuri ACME 엔드포인트

발급 흐름

단계 내용
1 ACME CA 연결 확인
2 EAB 키로 ACME 계정 등록
3 인증서 order 생성, DNS-01 챌린지 토큰 수신
4 생성 스크립트 호출 → TXT 레코드 생성 → DNS 전파 대기
5 CA가 TXT 레코드 확인 → 검증 완료
6 삭제 스크립트 호출 → TXT 레코드 삭제
7 RSA 2048 CSR 제출 → CA 서명
8 인증서 다운로드 → WebHosting 스토어 저장
9 IIS 사이트 443 바인딩 자동 교체
10 renewal.json 저장 (이후 자동 갱신에 사용)
참고

문제가 발생하는 경우 명령어에 --verbose 옵션을 추가하면 CA와의 HTTP 요청·응답을 포함한 상세 로그가 출력됩니다. 최초 등록 또는 EAB 교체 후 재등록 시 사용을 권장합니다.

4단계: 자동 갱신 설정

인증서 등록 후 관리자 권한 PowerShell에서 아래 명령을 실행하여 Task Scheduler 자동 갱신 작업을 등록합니다.

$action = New-ScheduledTaskAction `
    -Execute "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

등록된 작업은 매일 09:00에 실행되며 갱신 여부를 자동으로 판단합니다. 갱신이 필요한 경우, DNS-01 챌린지를 수행하고 IIS 바인딩을 무중단으로 교체합니다.

갱신 상태를 확인하려면 아래 명령을 실행합니다.

Get-ScheduledTaskInfo -TaskName "win-acme daily renew" |
    Select-Object LastRunTime, LastTaskResult, NextRunTime
LastTaskResult 의미
0 갱신 성공
267011 갱신 불필요 (정상)
그 외 실패 (로그 확인 필요)

갱신 로그는 win-acme 데이터 디렉터리 하위 Log\log-YYYYMMDD.txt 파일에서 확인할 수 있습니다.

인증서 확인

발급된 인증서는 Windows Certificate Store의 WebHosting 저장소에 저장됩니다. 아래 방법으로 확인합니다.

# IIS 바인딩 인증서 thumbprint 확인
Get-WebBinding | Where-Object { $_.protocol -eq "https" } |
    Select-Object bindingInformation, certificateHash

# WebHosting 스토어 인증서 목록 조회
Get-ChildItem "Cert:\LocalMachine\WebHosting" |
    Select-Object Subject, Thumbprint, NotAfter, Issuer

또는 Windows 인증서 관리자 (certlm.msc) → 인증서(로컬 컴퓨터) → 웹 호스팅 → 인증서에서 확인할 수 있습니다.

EAB 키 교체

EAB 키를 교체해야 하는 경우 아래 순서대로 진행합니다. 기존 ACME 계정 파일이 남아 있으면 새 EAB 키가 무시되고 기존 계정이 재사용되므로, 반드시 계정 파일을 먼저 삭제해야 합니다.

  1. 스크립트 또는 설정 파일에서 EAB 값을 새로 발급받은 값으로 교체합니다.
  2. win-acme 데이터 디렉터리에서 Registration_v2, Signer_v2, *.renewal.json 파일을 삭제합니다.
  3. 3단계의 발급 명령을 다시 실행합니다.

성공하면 새 계정으로 인증서가 발급되고 IIS 바인딩이 자동으로 교체됩니다. Task Scheduler 작업은 별도 수정이 불필요합니다.

수동 갱신

테스트 또는 긴급 대응이 필요한 경우 아래 명령을 사용합니다.

# 일반 갱신 (만료 조건 충족 시에만 실행)
wacs.exe --renew --baseuri "https://acme.navercloudtrust.com/acme/directory"

# 강제 갱신 (조건 무관하게 즉시 실행)
wacs.exe --renew --baseuri "https://acme.navercloudtrust.com/acme/directory" --force

# 상세 로그 출력하며 강제 갱신
wacs.exe --renew --baseuri "https://acme.navercloudtrust.com/acme/directory" --force --verbose

# Task Scheduler 작업 즉시 실행
Start-ScheduledTask -TaskName "win-acme daily renew"

# 등록된 renewal 목록 확인
wacs.exe --list --baseuri "https://acme.navercloudtrust.com/acme/directory"

문제 해결

증상 원인 조치
order invalid (DNS 검증은 valid) RSA 키 크기 불일치 settings.jsonCsr.Rsa.KeyBits: 2048 확인
Authorization pending 후 실패 DNS 전파 지연 생성 스크립트의 대기 시간을 90~120초로 증가
TXT 레코드 검증 실패 Zone 탐색 실패 콘솔에서 해당 도메인이 Global DNS에 Zone으로 등록되어 있는지 확인
API 401/403 오류 API 키 오류 또는 권한 부족 Ncloud IAM에서 Access Key 유효 여부 및 Global DNS 권한 재확인
한글 로그 깨짐 훅 파일 인코딩 오류 훅 파일을 UTF-8 BOM으로 재저장
오류 없이 조용히 실패 --verbose 미사용 --verbose 옵션 추가 후 재실행
EAB 교체 후 기존 계정 재사용 Registration_v2 미삭제 EAB 키 교체 절차 참조
Task Scheduler 실패 SYSTEM 계정 권한 부족 작업 속성 → 최고 권한으로 실행 체크 확인
IIS 바인딩 미갱신 인증서 저장소 불일치 --certificatestore WebHosting 파라미터 확인

로그를 확인하려면 아래 명령을 실행해 주십시오.

Get-Content "win-acme 데이터 경로\Log\log-$(Get-Date -Format yyyyMMdd).txt" -Tail 100

보안 권고

  • API 키와 EAB 키가 포함된 설정 파일은 Git 저장소나 공유 폴더에 포함하지 마십시오.
  • Ncloud API 키에는 Global DNS 서비스에만 최소 권한을 부여하는 것을 권장합니다.
  • win-acme 데이터 디렉터리(ACME 계정 서명 키 포함)에 대한 접근 권한을 관리자 계정으로 제한하십시오.
  • 인증서 개인키가 외부에 노출되지 않도록 주의하십시오.
  • win-acme 및 PowerShell을 정기적으로 최신 버전으로 유지하십시오.

Ncloud Global DNS 훅 스크립트 예시

아래는 Ncloud Global DNS API를 사용하는 DNS 훅 스크립트 예시입니다.

주의

아래 스크립트는 참고용 예시입니다. 운영 환경에 맞게 충분히 검토하고 테스트한 후 사용하십시오. 스크립트의 수정, 설정, 실행 결과에 대한 책임은 사용자에게 있습니다.

API 키 설정 파일: config.ps1

# ACME 서버 (변경 불필요)
$ACME_SERVER = "https://acme.navercloudtrust.com/acme/directory"

# EAB 키 — Certificate Manager 콘솔에서 발급받은 값으로 교체
$EAB_KID      = "[EAB_KEY_ID]"
$EAB_HMAC_KEY = "[EAB_HMAC_KEY]"

# NCP Global DNS API 키 — NCP IAM 콘솔에서 발급 (Global DNS 권한 필요)
$NCP_ACCESS_KEY = "[NCP_ACCESS_KEY]"
$NCP_SECRET_KEY = "[NCP_SECRET_KEY]"
$NCP_DNS_API    = "https://globaldns.apigw.ntruss.com"

# DNS 전파 대기 시간(초). 검증 실패 시 90~120으로 늘리십시오.
$DNS_PROPAGATION_WAIT = 60

# ACME 계정 이메일
$ACME_EMAIL = "[ADMIN_EMAIL]"

TXT 레코드 생성 훅: dns-create.ps1

# dns-create.ps1 — win-acme DNS-01 생성 훅 (NCP Global DNS)
# UTF-8 BOM 인코딩으로 저장할 것

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

# 와일드카드 처리
$Domain = $Domain -replace "^\*\.", ""

# Zone 탐색
$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 "[오류] NCP Global DNS에서 '$Domain'에 해당하는 zone을 찾을 수 없습니다."
    exit 1
}

$zoneId   = $matchedZone.domainId
$zoneName = $matchedZone.name

# TXT 레코드 호스트명 계산
if ($Domain -eq $zoneName) {
    $hostName = "_acme-challenge"
} else {
    $sub      = $Domain -replace "\.$([regex]::Escape($zoneName))$", ""
    $hostName = "_acme-challenge.$sub"
}

# TXT 레코드 생성
$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 "[오류] TXT 레코드 생성에 실패했습니다."
    exit 1
}

# 레코드 ID 임시 저장 (삭제 훅에서 사용)
$tmpFile = "$env:TEMP\ncp_sid_$($Domain -replace '\*','_').json"
@{ recordId = $response.recordId; domainId = $zoneId } | ConvertTo-Json |
    Set-Content $tmpFile -Encoding UTF8

# 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 레코드 생성 완료. $DNS_PROPAGATION_WAIT 초 대기 중..."
Start-Sleep -Seconds $DNS_PROPAGATION_WAIT

TXT 레코드 삭제 훅: dns-delete.ps1

# dns-delete.ps1 — win-acme DNS-01 삭제 훅 (NCP Global DNS)
# UTF-8 BOM 인코딩으로 저장할 것

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 "[경고] 삭제할 레코드 정보를 찾을 수 없습니다."
    exit 0
}

# TXT 레코드 삭제
$deleteUri = "/v1/ncpdns/record/$($sid.domainId)/$($sid.recordId)"
Invoke-RestMethod -Uri "$NCP_DNS_API$deleteUri" -Method DELETE `
    -Headers (Get-NcpHeaders "DELETE" $deleteUri) | Out-Null

# 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 레코드 삭제 완료."