Ubuntu 24.04 Certbot 설치 및 Nginx 인증서 적용 방법
Ubuntu 24.04에서 apt 또는 snap을 사용하여 Certbot을 설치하고 Nginx 인증서를 발급받는 방법을 설명합니다. apt와 snap 설치 시 주의사항, HTTP-01 챌린지 실패 원인인 포트 80 개방 문제 및 갱신 타임아웃 해결법을 상세히 안내합니다.
Certbot 설치: apt 또는 snap
Ubuntu 24.04에서 sudo apt install certbot python3-certbot-nginx를 사용하면 실제 공개적으로 신뢰받는 Let's Encrypt 인증서를 발급할 수 있는 Certbot이 설치됩니다. Certbot의 공식 문서에서는 snap 사용을 권장합니다. 두 방식의 차이는 미미합니다. snap은 업스트림 릴리스를 추적하고, 아카이브 패키지는 LTS 버전에 포함된 버전을 유지하며 보안 패치만 적용받습니다.
둘 중 하나만 선택하십시오. Certbot을 두 개 설치하면 동일한 /etc/letsencrypt 트리를 대상으로 갱신 타이머가 두 개 작동하게 되며, 잊고 있던 타이머가 문제를 일으킬 수 있습니다.
apt 방식:
sudo apt update
sudo apt install certbot python3-certbot-nginx이 명령은 /usr/bin/certbot, nginx 플러그인, certbot.service + certbot.timer 쌍, 그리고 systemd 환경에서 아무 동작도 하지 않는 /etc/cron.d/certbot 항목을 설치합니다.
snap 방식:
sudo apt remove certbot python3-certbot-nginx
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbotsnap은 자체 타이머인 snap.certbot.renew.timer을 포함합니다. snap을 설치하기 전에 apt 패키지를 삭제하십시오.
설치 후 두 방식의 동작은 동일합니다. Certbot 2.x는 기본적으로 ECDSA (P-256) 키를 사용합니다. ECDSA를 지원하지 않는 클라이언트를 위해서만 --key-type rsa 플래그를 전달하십시오. 모든 상태 정보는 /etc/letsencrypt 아래에 저장됩니다. archive/에는 실제 키와 인증서 파일이, live/에는 현재 사용 중인 파일에 대한 심볼릭 링크가, renewal/에는 인증서별 설정 파일이, accounts/에는 ACME 계정 키가 저장됩니다.
HTTP-01의 실제 동작 방식과 포트 80이 필수적인 이유
HTTP-01 챌린지는 일종의 콜백입니다. 사용자가 Let's Encrypt에 example.com에 대한 인증서를 요청하면, Let's Encrypt는 공개 DNS에서 해당 도메인 이름을 조회하고 발견된 주소의 포트 80으로 연결을 시도하여 http://example.com/.well-known/acme-challenge/<token>을 요청합니다. 이때 서버는 Certbot이 디스크에 기록한 토큰 내용을 그대로 응답해야 합니다. 이것이 전체 메커니즘의 전부입니다. 이 과정에서 발생하는 세 가지 결과가 대부분의 인증 실패 원인이 됩니다.
- 포트 80은 공용 인터넷에서 접근 가능해야 합니다. 단순히 사용자의 노트북에서 접근 가능한 것만으로는 부족합니다.
ufw규칙, 클라우드 제공업체의 보안 그룹, 또는 443 포트만 허용하는 VPS 콘솔 방화벽은 인증서 발급은 물론 향후 모든 갱신 과정을 차단합니다. - DNS는 이미 해당 서버를 가리키고 있어야 합니다. 검증 서버는 외부에서 독자적으로 DNS 조회를 수행합니다. 사용자의
/etc/hosts항목이나 브라우저 캐시는 검증 서버에 아무런 영향을 주지 못합니다. - AAAA 레코드를 게시한 경우, IPv6가 우선적으로 시도됩니다. IPv6 연결이 완전히 실패하면 Let's Encrypt는 IPv4로 재시도하지만, 연결은 수락하면서 다른 콘텐츠를 제공하는 호스트를 가리키는 오래된 AAAA 레코드가 있다면 즉시 실패 처리됩니다.
리다이렉트는 허용됩니다. 검증 과정은 HTTP 리다이렉트를 따라 HTTPS로 이동하며, 이때 상대 측의 인증서가 없거나 만료되었거나 자체 서명된 인증서여도 상관하지 않습니다. 다만, 검증은 반드시 포트 80에서 시작해야 합니다. Certbot은 TLS-ALPN-01 구현을 포함하고 있지 않으므로 "그냥 443 포트를 사용하면 되지 않나"라는 생각은 해결책이 될 수 없습니다.
인증 방식 선택: --nginx, --webroot, --standalone
--nginx은 Nginx가 이미 실행 중이고 해당 도메인을 서비스하고 있을 때 사용하는 기본 권장 방식입니다. Certbot이 설정을 분석하여 임시 챌린지 경로를 삽입하고 Nginx를 리로드한 뒤, 검증을 마치고 TLS 지시어를 서버 블록에 작성합니다. 서비스 중단은 발생하지 않습니다.
sudo certbot --nginx -d example.com -d www.example.com초기 설정 단계에서 스크립트로 실행할 경우:
sudo certbot --nginx \
-d example.com -d www.example.com \
--agree-tos -m ops@example.com --no-eff-email \
--redirect --non-interactive--webroot는 Certbot이 Nginx 설정을 직접 수정하지 않기를 원할 때 적합합니다. 템플릿으로 설정을 생성하거나, git으로 관리하거나, Ansible로 배포하는 환경에 적합합니다. Certbot은 챌린지 파일만 이미 서비스 중인 디렉터리에 생성합니다.
sudo certbot certonly --webroot -w /var/www/example.com \
-d example.com -d www.example.com \
--deploy-hook "systemctl reload nginx"--standalone은 80번 포트를 사용하는 서비스가 없을 때 적합합니다. 메일 서버, 443번 포트만 사용하는 API, Nginx 설치 전 실행되는 초기화 스크립트 등이 이에 해당합니다. Certbot이 잠시 동안 직접 80번 포트를 점유합니다. 만약 Nginx가 실행 중이라면 이 방식은 실패하므로, 실행 전후로 Nginx를 중단해야 합니다.
sudo certbot certonly --standalone -d mail.example.com \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"이러한 훅(hook)은 인증서 갱신 설정에 기록되므로, 갱신 시에도 동일한 중단/시작 과정이 자동으로 수행됩니다.
인증서 존재 여부와 관계없이 작동하는 서버 블록
닭과 달걀의 문제: nginx는 존재하지 않는 파일을 가리키는 ssl_certificate 설정이 있으면 시작을 거부하고, Certbot은 nginx가 내려가 있으면 검증을 수행할 수 없습니다. 먼저 80번 포트로 사이트를 띄우십시오.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com;
default_type "text/plain";
try_files $uri =404;
}
location / {
try_files $uri $uri/ =404;
}
}sudo nginx -t && sudo systemctl reload nginx를 실행하고 외부에서 curl -I http://example.com/ 응답이 오는지 확인한 뒤 인증서를 발급받으십시오. 그 후:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}ACME 경로에 붙은 ^~ 접두사는 제 역할을 합니다. 이는 return 301 블록이 챌린지 요청을 가로채지 못하게 막습니다. 해당 경로를 80번 포트에 유지하면 사이트 전체를 HTTPS 전용으로 전환한 뒤에도 갱신 작업이 계속 정상적으로 작동합니다.
위의 두 블록은 모두 디스크에서 파일을 제공합니다. 만약 nginx가 애플리케이션의 앞단에 있다면 location /는 proxy_pass 블록이 되며, 리버스 프록시 서버 블록, 줄 단위 설명에서 애플리케이션에 필요한 헤더를 다룹니다. 이때 ACME 경로와 TLS 지시어는 그대로 유지됩니다.
HTTP/2 문법은 nginx 버전에 따라 다르며, 두 형식을 혼용하면 시작 오류가 발생합니다. Ubuntu 24.04는 nginx 1.24를 제공하며, 이는 listen 443 ssl http2;와 같이 인라인 방식을 사용합니다. Debian 13은 더 최신 버전의 nginx를 제공하며, 이는 별도의 http2 on; 지시어를 사용합니다. 먼저 nginx -v을 확인하십시오.
nginx가 live/를 가리키게 하십시오. archive/를 직접 가리키면 안 됩니다. live/ 심볼릭 링크는 갱신될 때마다 대상이 변경됩니다. archive/ 내부의 경로를 직접 지정하면 인증서가 만료되었을 때 갱신되지 않은 이전 인증서에 고정되는 문제가 발생합니다.
와일드카드 인증서에는 DNS-01이 필요하며, DNS-01에는 플러그인이 필요합니다
와일드카드 인증서(*.example.com)는 HTTP-01 방식으로 검증할 수 없습니다. 파일을 가져올 단일 호스트 이름이 존재하지 않기 때문입니다. 유일한 방법은 DNS-01입니다. 특정 _acme-challenge.example.com TXT 레코드를 게시하여 도메인 제어권을 증명해야 합니다. Certbot이 이를 자동으로 수행하려면 DNS 공급자의 API 자격 증명이 필요하며, 이를 위해 공급자 플러그인이 존재합니다. 와일드카드 인증서 전체 가이드에서 TXT 레코드의 작동 원리와 수동 모드에서의 갱신 주의 사항을 다루며, 아래에는 Cloudflare를 사용하는 간략한 버전을 설명합니다.
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflareapt 경로에서는 대신 sudo apt install python3-certbot-dns-cloudflare을 사용합니다. 자격 증명은 root 계정만 읽을 수 있는 파일에 저장하십시오.
# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_here토큰의 범위를 해당 영역에 대한 DNS 편집 권한으로 제한하십시오. 이는 DNS에 대한 암호화 키이므로, 그에 걸맞게 취급해야 합니다.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d example.com -d '*.example.com'셸에서 와일드카드를 해석하지 못하도록 따옴표로 묶으십시오. DNS-01은 HTTP-01이 해결할 수 없는 문제도 해결합니다. 공개 포트 80이 없는 호스트, 내부 서비스, VPS 기반의 자체 호스팅 WireGuard VPN을 통해서만 접근 가능한 장비, 사설 인터페이스의 관리자 패널 등에 대한 인증서 발급이 가능합니다.
갱신: 90일 주기, 타이머, 배포 훅
Let's Encrypt 인증서의 유효 기간은 90일입니다. Certbot은 만료까지 30일 미만으로 남았을 때 갱신을 수행하며, 이 30일이라는 기간은 갱신 실패 시 서비스 중단 없이 문제를 해결할 수 있는 여유를 제공합니다. Let's Encrypt는 더 이상 만료 경고 이메일을 발송하지 않으며 아무도 이를 알려주지 않으므로, 모니터링은 전적으로 관리자의 책임입니다.
설치 시 함께 제공된 타이머를 확인하십시오:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew은 /etc/letsencrypt/renewal/에 있는 모든 설정을 순회하며, 30일 이내에 도달하지 않은 인증서는 건너뛰고 나머지는 최초 실행 시 사용된 플래그를 그대로 사용하여 갱신합니다. 이것이 바로 최초 실행이 중요한 이유입니다. 최초 실행 시의 설정이 기록되기 때문입니다.
디스크의 파일을 갱신하는 것만으로는 아무것도 변경되지 않습니다. nginx는 무언가 다시 로드하라는 신호를 줄 때까지 메모리에 있는 이전 인증서를 계속 제공합니다. 배포 훅을 한 번 설정하십시오:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shrenewal-hooks/deploy/에 있는 실행 가능한 모든 파일은 갱신이 성공할 때마다 실행됩니다. --deploy-hook 플래그는 단일 인증서에 대해 동일한 작업을 수행하며, renew_hook = ...를 해당 갱신 설정에 저장합니다. certbot --nginx은 자동으로 다시 로드하지만, --webroot 및 --standalone 설정은 그렇지 않습니다. 훅이 누락되면 certbot certificates는 인증서가 최신 상태라고 보고하지만 실제 사이트는 만료된 인증서를 제공하는 상황이 발생합니다. 시작 시 인증서를 읽는 다른 모든 서비스도 동일한 훅이 필요합니다. Docker, TLS 및 백업을 사용하는 Nextcloud VPS 설치와 같이 컨테이너화된 애플리케이션은 여기에 자체적인 재시작 또는 다시 로드 단계를 연결해야 합니다.
실제 갱신 테스트
sudo certbot renew --dry-run이 명령은 Let's Encrypt의 스테이징 환경을 대상으로 전체 챌린지를 수행합니다. 동일한 코드 경로, 동일한 방화벽, 동일한 DNS 설정을 사용하며, 속도 제한(rate-limit)에 영향을 주지 않고 디스크에 아무것도 기록하지 않습니다. 오늘 이 테스트를 통과하면, 서버 환경이 변경되지 않는다는 가정하에 60일 뒤의 자동 갱신도 정상적으로 수행됩니다.
드라이 런(dry run)만으로는 리로드 훅(reload hook)이 정상적으로 실행되는지 확인할 수 없으며, 이 동작은 Certbot 버전에 따라 다를 수 있습니다. 훅 스크립트를 직접 실행하여 이 부분을 수동으로 검증하십시오. 훅 스크립트를 직접 실행하여 systemctl reload nginx이 성공하는지 확인하고, 이어서 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf을 확인하십시오.
실제로 마주하게 될 오류들
Could not bind to IPv4 or IPv6., --standalone: nginx가 이미 80번 포트를 점유하고 있을 때 발생합니다. --nginx 또는 --webroot를 사용하거나, 실행하는 동안 nginx를 중단하십시오. sudo ss -lntp | grep ':80' 명령으로 포트 점유자를 확인하십시오.
Timeout during connect (likely firewall problem): Let's Encrypt가 80번 포트에 접근할 수 없습니다. 외부에서 내부로 순차적으로 확인하십시오: sudo ufw status (sudo ufw allow 'Nginx Full'로 개방), 그다음 VPS 제공업체의 자체 방화벽, 마지막으로 DNS 순입니다. 서버 외부의 환경에서 curl -sSv http://example.com/.well-known/acme-challenge/test 명령으로 테스트하십시오. 오래된 AAAA 레코드도 동일한 메시지를 유발합니다.
unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404: 80번 포트는 접근 가능하지만 토큰이 제공되지 않는 경우입니다. 요청이 다른 서버 블록으로 전달되었거나(어떤 블록이 default_server를 소유하는지 확인), -w에 전달된 디렉터리가 nginx가 서비스하는 경로와 다를 수 있습니다. /var/www/example.com/.well-known/acme-challenge/test에 파일을 하나 생성한 뒤 외부에서 호출해 보십시오. 여기서 404 오류가 발생한다면 인증서 문제가 아닙니다.
DNS problem: NXDOMAIN looking up A for example.com: 도메인 이름이 공개적으로 해석되지 않는 경우입니다. 새로 생성한 레코드가 전파되지 않았거나, 도메인 등록기관이 서비스하지 않는 영역의 레코드일 수 있습니다.
too many certificates already issued for: example.com: 속도 제한(rate limit) 오류로, 디버깅을 반복할 때 흔히 발생합니다. Let's Encrypt는 동일한 도메인 이름 조합에 대해 주당 5개의 중복 인증서 발급으로 제한하며, 등록된 도메인당 주당 50개의 신규 인증서를 허용합니다. 시간이 지나기 전까지는 이를 해제할 방법이 없습니다. --dry-run 플래그를 사용하여 스테이징 환경에서 디버깅하십시오.
nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory: nginx가 발급되지 않았거나 certbot delete로 삭제된 인증서를 사용하도록 설정된 경우입니다. TLS 서버 블록을 주석 처리하고 nginx를 시작한 뒤, 인증서를 발급받고 다시 블록을 복구하십시오.
open() "/etc/letsencrypt/options-ssl-nginx.conf" failed: 해당 파일은 nginx 플러그인 패키지와 함께 제공됩니다. python3-certbot-nginx가 없는 certonly 환경이라면, 플러그인을 추가하거나 include 라인을 직접 설정한 ssl_protocols 및 ssl_ciphers로 대체하십시오.
대규모 환경에서의 관리
하나의 인증서에 최대 100개의 도메인 이름을 포함할 수 있으며, 하나의 certbot --nginx -d a.example.com -d b.example.com ...을 사용하는 것이 매력적으로 보일 수 있습니다. 하지만 오래된 DNS 레코드 하나가 검증에 실패하면 해당 인증서에 포함된 모든 이름의 서비스가 중단됩니다. 사이트별로 인증서를 분리하면 독립적으로 실패하므로, 여러 서비스를 호스팅하는 서버에서는 이 방식이 권장됩니다. 사이트 수가 늘어나면 ACME를 지원하는 프론트엔드 프록시를 도입하는 것이 유리합니다. Docker Compose로 여러 앱을 운영하는 Traefik 리버스 프록시는 인증서 요청과 갱신을 스스로 처리하므로 Certbot을 별도로 실행할 필요가 없습니다. 어떤 프록시를 사용할지는 선택의 문제이며, Nginx, Caddy, Traefik 비교는 프록시가 인증서 관리와 앱별 설정을 얼마나 대신 처리해주길 원하는지에 따라 결정됩니다.
/etc/letsencrypt 전체를 심볼릭 링크가 유지되도록 sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt로 백업하십시오. 해당 디렉터리 트리에는 accounts/와 ACME 계정 키가 포함되어 있으며, 이는 동일하게 재생성할 수 없습니다. 새로운 VPS로 이전하는 과정은 -a 옵션을 사용하여 트리를 rsync로 복사하고, Certbot을 설치한 뒤, DNS를 변경하고, 전환하기 전에 certbot renew --dry-run을 실행하는 것으로 간단히 끝납니다.
서버를 재구축하거나 새로운 LTS 버전으로 이전하면 갱신 타이머는 자동으로 따라오지 않습니다. 마이그레이션, 스냅샷 복원, 배포판 업그레이드 후에는 반드시 systemctl list-timers 'certbot*'를 실행하고 --dry-run을 한 번 수행해야 합니다. 이를 건너뛰면 89일 후 새벽 3시에 모두가 자동으로 갱신될 것이라 믿었던 인증서가 만료되어 사이트가 차단되는 상황이 발생합니다.
이 모든 내용은 공인 IP를 보유하고 포트 80이 외부에 열려 있는, 즉 사용자가 직접 제어하는 VPS 환경을 가정합니다. 위에서 설명한 메커니즘은 모든 VPS 환경에서 동일하게 적용됩니다.
동일한 인증서 설정 단계가 nginx 대신 Apache를 사용하는 경우에도 적용되며, 공인 인증서를 사용할 수 없는 환경에서는 Ubuntu에서 자체 서명 인증서를 사용하여 내부 서비스를 보호할 수 있습니다.
FAQ
사이트에서 HTTPS만 서비스하는 경우에도 포트 80을 열어야 합니까?
네, HTTP-01 챌린지를 위해 필요합니다. Let's Encrypt는 항상 포트 80에서 검증 요청을 시작하며, Certbot은 TLS-ALPN-01 구현을 제공하지 않습니다. 따라서 포트 443만 개방된 방화벽은 최초 인증서 발급과 이후의 모든 자동 갱신을 차단합니다. 포트 80에서 HTTPS로 리다이렉트하는 설정은 괜찮으며, 검증 과정은 이 리다이렉트를 따라갑니다. 포트 80을 완전히 열지 않는 유일한 방법은 DNS 공급자 플러그인을 사용하는 DNS-01 방식뿐입니다.
Ubuntu 24.04의 nginx 환경에서 Certbot을 설치할 때 apt와 snap 중 무엇을 선택해야 합니까?
apt를 사용하십시오. sudo apt install certbot python3-certbot-nginx를 사용하면 Ubuntu 24.04에서 Certbot 2.9.0 버전이 설치됩니다. 이 버전은 본 가이드의 모든 내용을 수행하기에 충분하며, unattended-upgrades를 통해 보안 패치를 받을 수 있고 snapd를 설치할 필요가 없습니다. 최신 릴리스가 즉시 필요하거나 snap으로만 배포되는 DNS 플러그인이 필요한 경우에만 snap을 선택하십시오. 어떤 방식을 선택하든 하나만 사용해야 합니다. 두 가지를 모두 설치하면 동일한 /etc/letsencrypt 트리를 가리키는 갱신 타이머가 두 개 생성되며, 관리되지 않는 쪽에서 문제가 발생하게 됩니다.
Certbot으로 nginx용 와일드카드 인증서를 발급할 수 있습니까?
DNS-01 방식을 통해서만 가능합니다. *.example.com과 같은 와일드카드 도메인은 챌린지 파일을 가져올 단일 호스트 이름이 없으므로 --nginx, --webroot, --standalone 방식은 모두 사용할 수 없습니다. DNS 공급자용 플러그인을 설치하고, root 계정만 읽을 수 있는 자격 증명 파일에 제한된 권한의 API 토큰을 저장한 뒤, 셸이 와일드카드를 확장하지 않도록 따옴표로 묶어 certbot certonly --dns-cloudflare -d example.com -d '*.example.com'을 실행하십시오.
갱신에 성공했는데도 nginx가 왜 여전히 이전 인증서를 제공합니까?
nginx는 인증서를 메모리에 유지하며, 설정을 다시 불러오기(reload) 전까지는 디스크의 새 파일을 인식하지 못합니다. certbot --nginx는 자동으로 설정을 다시 불러오지만, --webroot이나 --standalone 실행 시에는 그렇지 않습니다. 따라서 갱신은 성공하더라도 브라우저에서는 여전히 만료 예정인 인증서가 보일 수 있습니다. nginx -t && systemctl reload nginx을 실행하는 스크립트를 작성하여 /etc/letsencrypt/renewal-hooks/deploy/에 배치하면, 갱신이 성공할 때마다 해당 스크립트가 실행됩니다.
certbot renew --dry-run은 갱신이 정상적으로 작동할 것임을 보장합니까?
대체로 그렇습니다. 이 명령어는 실제 환경과 동일한 방화벽, DNS, 코드 경로를 사용하여 스테이징 환경에서 실제 챌린지를 수행합니다. 속도 제한(rate-limit)에 걸리지 않으며 디스크에 아무것도 기록하지 않으므로, 통과했다면 네트워크 구성은 정상이라고 볼 수 있습니다. 다만 배포 훅(deploy hook)이 정상적으로 작동하는지까지 확실하게 보장하지는 않습니다. 훅 스크립트를 직접 실행해 보고 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf을 확인하여 별도로 테스트하십시오.