Certbot을 사용하여 Linux 서버 환경에서 인증서 발급 및 갱신을 자동화하는 방법을 안내합니다.
Ubuntu 22.04/24.04 LTS 및 Rocky Linux 8/RHEL 8 환경을 기준으로 작성되었습니다.
본 가이드는 Certbot 기준으로 제공됩니다. RFC 8555를 준수하는 다른 ACME 클라이언트도 사용할 수 있으나, 공식 기술 지원은 Certbot 기준으로만 제공됩니다.
Ncloud Trust CA는 RSA 2048만 허용합니다. ECDSA 또는 RSA 4096으로 요청하면 인증서 발급이 실패합니다. 아래 가이드의 명령어에는 --key-type rsa --rsa-key-size 2048 옵션이 포함되어 있습니다. 임의로 변경하지 마십시오.
시작하기 전에
이 가이드를 진행하기 전 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 파일 내용:
# NCP 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 | Ncloud 포털 > 마이페이지 > 계정 관리 > 인증키 관리 |
NCP_SECRET_KEY |
Ncloud API Secret Key | NCP 포털 > 마이페이지 > 계정 관리 > 인증키 관리 |
NCP_DOMAIN_ID |
Global DNS 도메인 숫자 ID | Ncloud 콘솔 > 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 레코드 생성에 실패했습니다 |
NCP 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 저장소나 공유 폴더에 포함하지 마십시오.- NCP 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 레코드 삭제 완료."