--- title: "win-acme 기반 Windows Server 인증서 자동화" slug: "certificate-winacme" updated: 2026-09-17T09:08:15Z published: 2026-09-17T09:08:15Z 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. # win-acme 기반 Windows Server 인증서 자동화 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 공식 릴리스 페이지](https://github.com/win-acme/win-acme/releases)에서 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 훅 스크립트 예시](/docs/certificate-winacme#NcloudGlobalDNS%ED%9B%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%98%88%EC%8B%9C)를 참조해 주십시오. ### 훅 스크립트 요구 사항 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.json` → `Csr.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 키 교체](/docs/certificate-winacme#EAB%ED%82%A4%EA%B5%90%EC%B2%B4) 절차 참조 | | 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 레코드 삭제 완료." ```