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 환경에서 이용 가능합니다.
Certbot을 사용하여 Linux 서버 환경에서 인증서 발급 및 갱신을 자동화하는 방법을 안내합니다.
Ubuntu 22.04/24.04 LTS 및 Rocky Linux 8/RHEL 8 환경을 기준으로 작성되었습니다.
Ncloud Trust CA는 RSA 2048만 허용합니다. ECDSA 또는 RSA 4096으로 요청하면 인증서 발급이 실패합니다. 아래 가이드의 명령어에는 --key-type rsa --rsa-key-size 2048 옵션이 포함되어 있습니다. 임의로 변경하지 마십시오.
이 가이드는 Certbot 기준으로 제공됩니다. RFC 8555를 준수하는 다른 ACME 클라이언트도 사용할 수 있으나, 공식 기술 지원은 Certbot 기준으로만 제공됩니다.
시작하기 전에
이 가이드를 진행하기 전 ACME 사용 준비의 모든 단계를 완료해 주십시오. 다음 항목이 미리 준비되어야 합니다.
- 발급된 EAB Key ID 및 EAB HMAC Key
- 사용할 도메인 검증 방식 결정 (DNS-01 동적 방식 또는 사전 검증 방식)
- OV 인증서를 사용하는 경우, Certificate Manager > Organization에서 조직 검증 완료
1단계: Certbot 설치
운영체제 환경에 맞는 명령어를 실행하여 Certbot을 설치해 주십시오. 자세한 설치 방법은 Certbot 공식 설치 가이드를 참조해 주십시오.
Ubuntu 22.04 / 24.04 LTS
sudo apt update
sudo apt install -y curl openssl jq python3
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Rocky Linux 8 / RHEL 8
sudo dnf install -y epel-release
sudo dnf install -y curl openssl jq python3 snapd
sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap
# 재로그인 후 실행
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
설치가 완료되면 다음 명령어로 버전을 확인해 주십시오. Certbot은 2.x 이상을 권장합니다.
certbot --version
jq --version
openssl version
2단계: DNS 훅 스크립트 설정
DNS-01 챌린지 자동화를 위해 Certbot의 --manual-auth-hook 및 --manual-cleanup-hook 옵션에 DNS TXT 레코드를 생성 및 삭제하는 스크립트를 연동합니다.
작업 디렉터리는 다음과 같이 구성됩니다.
/opt/acme-ncp-dns/
├── .env # 설정 파일 (직접 생성, 외부 노출 금지)
├── ncp-auth.sh # Certbot 인증 훅 (TXT 레코드 생성)
└── ncp-cleanup.sh # Certbot 정리 훅 (TXT 레코드 삭제)
디렉터리를 생성해 주십시오.
sudo mkdir -p /opt/acme-ncp-dns
Ncloud Global DNS를 사용하는 경우, 훅 스크립트 예시는 Ncloud Global DNS 훅 스크립트 예시를 참조해 주십시오. 다른 DNS 제공자를 사용하는 경우, 해당 제공자의 API를 호출하여 동일한 역할을 수행하는 스크립트를 작성해 주십시오.
스크립트 파일을 배치한 후 다음과 같이 설정 파일을 생성하고 실행 권한을 부여해 주십시오.
sudo vi /opt/acme-ncp-dns/.env
sudo chmod 600 /opt/acme-ncp-dns/.env
sudo chmod +x /opt/acme-ncp-dns/ncp-auth.sh
sudo chmod +x /opt/acme-ncp-dns/ncp-cleanup.sh
.env 파일 내용:
# Ncloud API 인증 키 (콘솔의 My Account > 계정 및 보안 관리 > 보안 관리 > 접근 관리 > API 인증키)
NCP_ACCESS_KEY="YOUR_NCP_ACCESS_KEY"
NCP_SECRET_KEY="YOUR_NCP_SECRET_KEY"
# NCP Global DNS API 엔드포인트 (변경 불필요)
NCP_DNS_API="https://globaldns.apigw.ntruss.com"
# Global DNS 도메인 ID
# NCP 콘솔 > Global DNS > F12 개발자 도구 > 도메인 클릭 > URL의 숫자 (예: dns/domain/36019)
NCP_DOMAIN_ID="YOUR_DOMAIN_ID"
# Global DNS에 등록된 루트 도메인 (예: example.com)
NCP_ZONE_DOMAIN="example.com"
# DNS TXT 레코드 전파 대기 시간(초). 검증 실패 시 90~120으로 늘려 주십시오.
DNS_PROPAGATION_SECONDS=60
| 항목 | 설명 | 확인 방법 |
|---|---|---|
NCP_ACCESS_KEY |
Ncloud API Access Key | 콘솔의 My Account > 계정 및 보안 관리 > 보안 관리 > 접근 관리 > API 인증키 |
NCP_SECRET_KEY |
Ncloud API Secret Key | 콘솔의 My Account > 계정 및 보안 관리 > 보안 관리 > 접근 관리 > API 인증키 |
NCP_DOMAIN_ID |
Global DNS 도메인 숫자 ID | 콘솔의 Global DNS > F12 > URL 숫자 확인 |
NCP_ZONE_DOMAIN |
DNS 영역 루트 도메인 | Global DNS에 등록된 도메인 (예: example.com) |
DNS_PROPAGATION_SECONDS |
DNS 전파 대기 시간 | 기본 60초. 검증 실패 시 90~120으로 조정 |
3단계: 인증서 발급
아래 명령어의 대괄호([ ]) 항목을 실제 값으로 교체하여 실행해 주십시오.
| 파라미터 | 설명 | 예시 |
|---|---|---|
[ACME_DIRECTORY_URL] |
ACME 시작 전 준비에서 확인한 ACME 디렉터리 URL | https://acme.navercloudtrust.com/acme/directory |
[EAB_KEY_ID] |
EAB 자격증명 발급 시 제공된 Key ID | abc123... |
[EAB_HMAC_KEY] |
EAB 자격증명 발급 시 제공된 HMAC Key | xyz789... |
[ADMIN_EMAIL] |
인증서 만료 알림을 받을 이메일 주소 | admin@example.com |
단일 도메인
sudo certbot certonly \
--manual \
--preferred-challenges dns \
--key-type rsa \
--rsa-key-size 2048 \
--manual-auth-hook /opt/acme-ncp-dns/ncp-auth.sh \
--manual-cleanup-hook /opt/acme-ncp-dns/ncp-cleanup.sh \
--server [ACME_DIRECTORY_URL] \
--eab-kid "[EAB_KEY_ID]" \
--eab-hmac-key "[EAB_HMAC_KEY]" \
--agree-tos \
--email [ADMIN_EMAIL] \
--non-interactive \
-d example.com
서브도메인 포함 (SAN)
sudo certbot certonly \
--manual \
--preferred-challenges dns \
--key-type rsa \
--rsa-key-size 2048 \
--manual-auth-hook /opt/acme-ncp-dns/ncp-auth.sh \
--manual-cleanup-hook /opt/acme-ncp-dns/ncp-cleanup.sh \
--server [ACME_DIRECTORY_URL] \
--eab-kid "[EAB_KEY_ID]" \
--eab-hmac-key "[EAB_HMAC_KEY]" \
--agree-tos \
--email [ADMIN_EMAIL] \
--non-interactive \
-d example.com \
-d www.example.com \
-d api.example.com
와일드카드 도메인
sudo certbot certonly \
--manual \
--preferred-challenges dns \
--key-type rsa \
--rsa-key-size 2048 \
--manual-auth-hook /opt/acme-ncp-dns/ncp-auth.sh \
--manual-cleanup-hook /opt/acme-ncp-dns/ncp-cleanup.sh \
--server [ACME_DIRECTORY_URL] \
--eab-kid "[EAB_KEY_ID]" \
--eab-hmac-key "[EAB_HMAC_KEY]" \
--agree-tos \
--email [ADMIN_EMAIL] \
--non-interactive \
-d "*.example.com"
와일드카드(*)를 포함하는 경우, 셸의 glob 확장을 방지하기 위해 도메인을 따옴표로 감싸 주십시오.
발급 성공 후 다음 명령어로 인증서를 확인해 주십시오.
sudo certbot certificates
sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem \
-noout -subject -issuer -dates
4단계: 자동 갱신 설정
Certbot은 인증서 만료 30일 전부터 갱신을 시도합니다. 훅 스크립트 경로는 최초 발급 시 자동으로 저장되므로 certbot renew만 실행하면 DNS 검증이 자동으로 수행됩니다.
먼저 다음 명령어로 갱신 동작을 시뮬레이션하여 확인해 주십시오.
sudo certbot renew --dry-run
방법 1: systemd 타이머 (권장)
snap으로 Certbot을 설치한 경우 갱신 타이머가 자동으로 등록됩니다. 다음 명령어로 상태를 확인해 주십시오.
sudo systemctl status snap.certbot.renew.timer
# 타이머가 없는 경우 수동 등록
sudo systemctl enable --now snap.certbot.renew.timer
방법 2: Crontab
- 다음 명령어를 실행하여 crontab 편집기를 열어 주십시오.
sudo crontab -e
- 다음 줄을 추가해 주십시오. (매일 오전 3시에 갱신 시도)
0 3 * * * /usr/bin/certbot renew --quiet 2>&1 | logger -t certbot-renew
--quiet옵션은 갱신 없이 건너뛰는 경우, 출력을 억제합니다.logger -t certbot-renew는 갱신 결과를 시스템 로그에 기록합니다. (journalctl -t certbot-renew로 확인)- 갱신 로그는
/var/log/letsencrypt/letsencrypt.log에서도 확인할 수 있습니다.
웹 서버 인증서 적용
발급된 인증서 파일 위치는 다음과 같습니다.
| 파일 | 설명 |
|---|---|
/etc/letsencrypt/live/<도메인>/cert.pem |
인증서 |
/etc/letsencrypt/live/<도메인>/chain.pem |
중간 CA 체인 |
/etc/letsencrypt/live/<도메인>/fullchain.pem |
인증서 + 체인 (웹 서버 설정 권장) |
/etc/letsencrypt/live/<도메인>/privkey.pem |
개인키 (외부 노출 금지) |
Nginx
/etc/nginx/sites-available/example.com 파일에 다음 내용을 입력해 주십시오.
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
location / {
root /var/www/html;
index index.html;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
sudo nginx -t
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo systemctl reload nginx
갱신 후 자동 재로드를 위해 deploy-hook을 설정합니다.
sudo vi /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Apache
/etc/apache2/sites-available/example.com-ssl.conf 파일에 다음 내용을 입력해 주십시오. (Ubuntu 기준)
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLHonorCipherOrder off
DocumentRoot /var/www/html
</VirtualHost>
sudo a2enmod ssl
sudo a2ensite example.com-ssl
sudo apache2ctl configtest
sudo systemctl reload apache2
sudo vi /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh
#!/bin/bash
systemctl reload apache2
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh
문제 해결
| 증상 | 원인 | 조치 |
|---|---|---|
The key ID was not found |
EAB Key ID 오류 또는 이미 사용된 키 | 콘솔에서 EAB 키 재발급 후 명령어 재실행 |
unauthorized |
EAB HMAC Key 오류 | HMAC Key 값 재확인. 공백·줄바꿈 포함 여부 확인 |
Error finalizing order :: invalid CSR |
RSA 2048 외 키 타입 사용 | 명령어에 --key-type rsa --rsa-key-size 2048 옵션 추가 여부 확인 |
DNS problem: NXDOMAIN |
TXT 레코드 미전파 | .env의 DNS_PROPAGATION_SECONDS 값을 90~120으로 늘리거나 dig +short TXT _acme-challenge.example.com @8.8.8.8으로 전파 여부 확인 |
| TXT 레코드 생성 실패 | Ncloud API 키 오류 또는 도메인 ID 불일치 | .env의 NCP_ACCESS_KEY, NCP_SECRET_KEY, NCP_DOMAIN_ID 값 재확인 |
| OV 인증서 발급 실패 | 조직 사전 검증 미완료 | Certificate Manager > Organization에서 검증 상태 확인 |
| 갱신 시 훅이 실행되지 않음 | renewal 설정 파일에 훅 경로 누락 | /etc/letsencrypt/renewal/example.com.conf의 [renewalparams] 섹션에 manual_auth_hook, manual_cleanup_hook 경로 직접 추가 |
sudo tail -100 /var/log/letsencrypt/letsencrypt.log
보안 권고
.env파일은 반드시 권한을600으로 설정해 주십시오. (sudo chmod 600 /opt/acme-ncp-dns/.env).env파일을 Git 저장소나 공유 폴더에 포함하지 마십시오.- Ncloud API 키에는 Global DNS 서비스에만 최소 권한을 부여하는 것을 권장합니다.
- 인증서 개인키(
privkey.pem)가 외부에 노출되지 않도록 주의해 주십시오.
Ncloud Global DNS 훅 스크립트 예시
아래 스크립트는 Ncloud Global DNS 환경에서의 참고용 예시입니다. 스크립트의 수정, 설정, 실행 환경 및 결과에 대한 책임은 사용자에게 있으며, 네이버클라우드는 해당 스크립트의 동작에 대한 기술 지원을 제공하지 않습니다. 실제 운영 환경 적용 전 충분한 테스트를 거칠 것을 권장합니다.
ncp-auth.sh
#!/bin/bash
# ncp-auth.sh — Certbot DNS-01 인증 훅 (NCP Global DNS)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/.env"
make_signature() {
local method=$1 uri=$2 timestamp=$3
# nl 변수($) 할당을 없애고 printf로 줄바꿈을 처리하여 에디터 파싱 오류 방지
printf "%s %s\n%s\n%s" "${method}" "${uri}" "${timestamp}" "${NCP_ACCESS_KEY}" \
| openssl dgst -sha256 -hmac "${NCP_SECRET_KEY}" -binary | base64
}
# 와일드카드 제거 후 TXT 호스트 계산
CLEAN_DOMAIN=$(echo "${CERTBOT_DOMAIN}" | sed 's/^\*\.//')
if [ "${CLEAN_DOMAIN}" = "${NCP_ZONE_DOMAIN}" ]; then
HOST="_acme-challenge"
else
SUB="${CLEAN_DOMAIN%.${NCP_ZONE_DOMAIN}}"
HOST="_acme-challenge.${SUB}"
fi
# TXT 레코드 생성
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI="/dns/v1/ncpdns/record/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "POST" "${URI}" "${TIMESTAMP}")
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 -X POST \
"${NCP_DNS_API}${URI}" \
-H "Content-Type: application/json" \
-H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
-H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
-H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
-d "[{\"host\":\"${HOST}\",\"type\":\"TXT\",\"content\":\"${CERTBOT_VALIDATION}\",\"ttl\":300,\"lbRegionCode\":\"KR\"}]")
SID=$(echo "${RESPONSE}" | jq -r '.[0].sid // empty')
if [ -z "${SID}" ]; then
echo "[오류] TXT 레코드 생성에 실패했습니다: ${RESPONSE}" >&2
exit 1
fi
echo "${SID}" > "/tmp/ncp_sid_${CERTBOT_DOMAIN}.txt"
# 변경 사항 반영
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI_APPLY="/dns/v1/ncpdns/record/apply/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "PUT" "${URI_APPLY}" "${TIMESTAMP}")
curl -s --connect-timeout 10 --max-time 30 -X PUT \
"${NCP_DNS_API}${URI_APPLY}" \
-H "Content-Type: application/json" \
-H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
-H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
-H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
-d '{}' > /dev/null
echo "DNS TXT 레코드 생성 완료. ${DNS_PROPAGATION_SECONDS}초 대기 중..."
sleep "${DNS_PROPAGATION_SECONDS}"
ncp-cleanup.sh
#!/bin/bash
# ncp-cleanup.sh — Certbot DNS-01 정리 훅 (NCP Global DNS)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/.env"
make_signature() {
local method=$1 uri=$2 timestamp=$3
printf "%s %s\n%s\n%s" "${method}" "${uri}" "${timestamp}" "${NCP_ACCESS_KEY}" \
| openssl dgst -sha256 -hmac "${NCP_SECRET_KEY}" -binary | base64
}
TMP_FILE="/tmp/ncp_sid_${CERTBOT_DOMAIN}.txt"
SID=$(cat "${TMP_FILE}" 2>/dev/null || true)
if [ -z "${SID}" ]; then
echo "[경고] 삭제할 레코드 ID를 찾을 수 없습니다." >&2
exit 0
fi
# TXT 레코드 삭제 (SID를 요청 본문에 배열로 전달)
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI="/dns/v1/ncpdns/record/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "DELETE" "${URI}" "${TIMESTAMP}")
curl -s --connect-timeout 10 --max-time 30 -X DELETE \
"${NCP_DNS_API}${URI}" \
-H "Content-Type: application/json" \
-H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
-H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
-H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
-d "[${SID}]" > /dev/null
# 변경 사항 반영
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI_APPLY="/dns/v1/ncpdns/record/apply/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "PUT" "${URI_APPLY}" "${TIMESTAMP}")
curl -s --connect-timeout 10 --max-time 30 -X PUT \
"${NCP_DNS_API}${URI_APPLY}" \
-H "Content-Type: application/json" \
-H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
-H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
-H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
-d '{}' > /dev/null
rm -f "${TMP_FILE}"
echo "DNS TXT 레코드 삭제 완료."