Ubuntu 24.04 Nginx Certbot 설치 및 설정 방법
Ubuntu 24.04에서 Certbot을 apt 또는 snap으로 설치하는 방법을 비교합니다. 인증서 갱신 실패를 방지하기 위해 apt와 snap 중 하나만 선택해야 하며, HTTP-01 방식에서 port 80이 반드시 개방되어 있어야 함을 설명합니다.
Certbot 설치: apt 또는 snap
Ubuntu 24.04에서 sudo apt install certbot python3-certbot-nginx를 사용하면 실제 공인 Let's Encrypt 인증서를 발급하는 Certbot을 사용할 수 있습니다. Certbot 공식 문서는 snap 사용을 권장합니다. 두 방식의 차이는 미미합니다. snap은 최신 릴리스를 반영하며, apt 패키지는 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의 실제 동작 방식 및 port 80이 필수인 이유
HTTP-01 challenge는 콜백 방식입니다. 사용자가 Let's Encrypt에 example.com에 대한 인증서를 요청하면, Let's Encrypt는 공용 DNS에서 해당 이름을 조회합니다. 그 후 찾아낸 주소의 port 80으로 연결을 시도하여 http://example.com/.well-known/acme-challenge/<token>을 요청합니다. 서버는 Certbot이 디스크에 방금 기록한 토큰 내용과 정확히 일치하는 값을 응답해야 합니다. 이것이 전체 메커니즘입니다. 이 과정에서 발생하는 세 가지 결과가 인증서 발급 실패의 대부분을 차지합니다.
- port 80은 로컬 환경이 아닌 공용 인터넷에서 접속 가능해야 합니다.
ufw규칙, 클라우드 제공업체의 security group, 또는 443 포트만 허용하는 VPS 콘솔 방화벽은 인증서 발급 및 향후 모든 갱신을 실패하게 만듭니다. - DNS가 이미 이 서버를 가리키고 있어야 합니다. 검증 서버는 외부에서 직접 조회를 수행합니다. 사용자의
/etc/hosts설정이나 브라우저 캐시는 검증에 영향을 주지 않습니다. - AAAA 레코드를 게시하면 IPv6가 우선적으로 시도됩니다. IPv6 연결이 완전히 실패하면 Let's Encrypt는 IPv4로 재시도합니다. 하지만 연결은 수락되지만 다른 내용을 반환하는 잘못된 AAAA 레코드가 있다면 인증은 완전히 실패합니다.
리다이렉트는 허용됩니다. 검증 과정은 HTTPS로의 HTTP 리다이렉트를 따르며, 대상 서버의 인증서가 누락되었거나 만료되었거나 self-signed 상태여도 상관하지 않습니다. 다만, 검증은 반드시 port 80에서 시작되어야 합니다. Certbot은 TLS-ALPN-01 구현을 지원하지 않으므로, "443 포트만 사용하면 된다"는 해결책이 될 수 없습니다.
인증 방식 선택: --nginx, --webroot, --standalone
nginx가 이미 실행 중이며 해당 도메인을 서비스하고 있다면 --nginx이 가장 적합한 기본 설정입니다. Certbot이 설정 파일을 분석하고, 임시 challenge 경로를 삽입한 뒤, nginx를 reload합니다. 검증이 완료되면 TLS 지시어를 server block에 기록합니다. 서비스 중단이 발생하지 않습니다.
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-interactivenginx 설정 파일을 직접 수정하고 싶지 않다면 --webroot을 사용하십시오. 템플릿에서 생성하거나 git에 저장한 설정, 또는 Ansible로 배포하는 설정에 적합합니다. Certbot은 이미 서비스 중인 디렉토리에 challenge 파일만 작성합니다.
sudo certbot certonly --webroot -w /var/www/example.com \
-d example.com -d www.example.com \
--deploy-hook "systemctl reload nginx"80번 포트를 사용하는 서비스가 없을 때 --standalone이 적합합니다. 메일 서버, 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은 인증서 갱신 설정에 기록됩니다. 따라서 인증서 갱신 시에도 동일한 중지 및 시작 과정이 자동으로 수행됩니다.
인증서 생성 전후 모두 작동하는 server block
닭과 달걀의 문제입니다. nginx는 존재하지 않는 파일을 ssl_certificate로 가리키면 시작되지 않으며, nginx가 중지된 상태에서는 Certbot이 검증을 수행할 수 없습니다. 먼저 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 location의 ^~ 접두사가 중요한 역할을 합니다. 이 설정은 return 301 block이 challenge 요청을 가로채는 것을 방지합니다. 해당 location을 80 포트에 유지하면, 사이트 전체를 HTTPS 전용으로 전환한 후에도 갱신이 계속 작동합니다.
HTTP/2 구문은 nginx 버전에 따라 다릅니다. 두 형식을 혼용하면 시작 오류가 발생합니다. Ubuntu 24.04의 nginx 1.24 버전은 인라인 방식인 listen 443 ssl http2;를 사용합니다. Debian 13의 최신 nginx는 별도의 http2 on; 지시어를 사용합니다. 먼저 nginx -v를 확인하십시오.
nginx 설정 시 archive/ 대신 반드시 live/를 지정하십시오. live/ symlink는 갱신할 때마다 경로가 변경됩니다. archive/에 직접 경로를 지정하면 만료된 인증서에 고정되어 문제가 발생합니다.
Wildcards는 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-cloudflaresudo 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토큰의 권한을 해당 zone의 DNS 편집 권한으로 제한하십시오. 이는 DNS에 대한 키와 같으므로 보안에 유의해야 합니다.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d example.com -d '*.example.com'Shell이 와일드카드를 glob 패턴으로 해석하지 않도록 따옴표로 감싸십시오. DNS-01은 HTTP-01이 해결할 수 없는 문제도 해결합니다. 공용 80번 포트가 없는 호스트에 대한 인증서 발급이 가능합니다. 예를 들어 내부 서비스, VPS에서 자체 호스팅하는 WireGuard VPN을 통해서만 접속 가능한 서버, 또는 프라이빗 인터페이스의 관리 패널 등이 해당됩니다.
갱신: 90일의 유효기간, 타이머, 그리고 deploy hook
Let's Encrypt 인증서의 유효기간은 90일입니다. Certbot은 유효기간이 30일 미만으로 남았을 때 갱신을 수행합니다. 이를 통해 갱신 실패 시 서비스 중단이 아닌, 30일 이내에 해결 가능한 문제로 대응할 수 있는 여유 시간을 확보할 수 있습니다. Let's Encrypt는 더 이상 만료 경고 이메일을 보내지 않습니다. 별도의 알림이 없으므로 직접 모니터링해야 합니다.
설치 시 포함된 타이머를 확인하십시오:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew는 /etc/letsencrypt/renewal/ 내의 모든 설정을 검사합니다. 30일 미만으로 남은 항목은 건너뛰고, 나머지 항목은 최초 실행 시 사용한 flag를 사용하여 갱신합니다. 최초 실행이 중요한 이유는 해당 설정이 기록되기 때문입니다.
디스크의 파일을 갱신하는 것만으로는 아무것도 변하지 않습니다. nginx는 재시작 명령을 받기 전까지 메모리에 로드된 기존 인증서를 계속 서비스합니다. deploy hook를 한 번 설정하십시오:
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 flag는 단일 인증서에 대해 동일한 역할을 수행하며, renew_hook = ...을 renewal config에 저장합니다. certbot --nginx는 자동으로 reload를 수행하지만, --webroot 및 --standalone 설정은 그렇지 않습니다. hook가 누락되면 certbot certificates은 새로운 인증서로 성공했다고 보고하지만, 웹사이트는 만료된 인증서를 계속 서비스하게 됩니다. 시작 시 인증서를 읽는 다른 모든 프로그램도 동일한 hook가 필요합니다. Docker, TLS 및 백업을 포함한 Nextcloud VPS 설치와 같은 컨테이너 기반 앱도 여기에 자체적인 restart 또는 reload 단계를 설정해야 합니다.
실제 갱신 테스트 수행
sudo certbot renew --dry-run이 명령은 Let's Encrypt의 staging environment를 대상으로 전체 challenge를 실행합니다. 코드 경로, firewall, DNS 설정은 동일하지만, rate-limit 제한이 적용되지 않으며 디스크에 아무것도 기록되지 않습니다. 시스템 설정이 변경되지 않는다면, 오늘 테스트를 통과하면 60일 후의 unattended renewal도 통과합니다.
dry run은 reload hook의 실행 여부를 증명하지 않습니다. 이 동작은 Certbot version에 따라 다릅니다. 해당 부분은 수동으로 테스트하십시오. hook script를 직접 실행하여 systemctl reload nginx이(가) 성공하는지 확인하고, sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf을(를) 점검하십시오.
실제로 발생할 수 있는 오류
Could not bind to IPv4 or IPv6. — nginx가 이미 port 80을 사용 중이므로 --standalone이 발생합니다. --nginx 또는 --webroot을 사용하거나, 실행 전 nginx를 중지하십시오. sudo ss -lntp | grep ':80'를 사용하여 포트 점유 상태를 확인하십시오.
Timeout during connect (likely firewall problem) — Let's Encrypt가 port 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 — port 80에는 접속 가능하지만 토큰을 불러올 수 없습니다. 요청이 다른 server block으로 전달되었거나(default_server을 소유한 블록을 확인하십시오), -w에 전달된 디렉토리가 nginx가 서비스하는 디렉토리가 아닙니다. /var/www/example.com/.well-known/acme-challenge/test에 파일을 생성하여 외부에서 접속해 보십시오. 404 오류가 발생한다면 인증서 문제는 아닙니다.
DNS problem: NXDOMAIN looking up A for example.com — 도메인 이름이 공용 DNS에서 해석되지 않습니다. 레코드가 아직 전파되지 않았거나, 등록업체에서 제공하지 않는 zone의 레코드일 때 발생합니다.
too many certificates already issued for: example.com — 속도 제한(rate limit) 오류이며, 반복적인 디버깅 중에 자주 발생합니다. Let's Encrypt는 동일한 이름 세트에 대한 중복 인증서를 주당 5개로 제한하며, 등록된 도메인당 주당 50개의 새 인증서만 허용합니다. 시간이 지나기 전에는 해결할 수 없습니다. --dry-run를 사용하여 staging 환경에서 디버깅하십시오.
nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory — nginx가 발급된 적 없는 인증서 또는 certbot delete으로 삭제된 인증서를 사용하도록 설정되어 있습니다. TLS server block을 주석 처리한 후 nginx를 시작하고, 인증서를 발급받은 뒤 블록을 복구하십시오.
open() "/etc/letsencrypt/options-ssl-nginx.conf" failed — 해당 파일은 nginx plugin 패키지에 포함되어 있습니다. python3-certbot-nginx이 없는 certonly 환경이라면, 플러그인을 추가하거나 include 라인을 사용자 정의 ssl_protocols 및 ssl_ciphers 설정으로 교체하십시오.
대규모 환경에서의 관리
하나의 certificate에는 최대 100개의 name을 포함할 수 있습니다. 이로 인해 단일 certbot --nginx -d a.example.com -d b.example.com ...를 사용하는 방식이 매력적으로 보일 수 있습니다. 하지만 오래된 DNS record 하나가 validation에 실패하면 해당 certificate에 포함된 모든 name이 함께 무효화됩니다. 사이트별로 certificate를 분리하면 각 사이트가 독립적으로 실패하므로, 여러 서비스를 호스팅하는 서버에서는 이 방식이 적합합니다. 관리할 사이트가 많아지면 ACME-aware front door를 사용하는 것이 효율적입니다. 예를 들어 Docker Compose 환경에서 여러 앱을 실행하는 Traefik reverse proxy를 사용하면, 스스로 certificate를 요청하고 갱신하므로 Certbot이 필요하지 않습니다.
/etc/letsencrypt 전체를 백업하십시오. sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt symlink가 유지되어야 합니다. 이 디렉토리 구조에는 ACME account key인 accounts/이 포함되어 있으며, 이는 동일하게 재생성할 수 없습니다. 새로운 VPS로 이전할 때는 다음 과정을 거칩니다. -a을 사용하여 디렉토리를 rsync로 복사하고, Certbot을 설치한 뒤, DNS 설정을 변경하고, 전환 전에 certbot renew --dry-run를 실행하십시오.
서버를 재설치하거나 새로운 LTS 버전으로 업그레이드하면 renewal timer는 자동으로 따라오지 않습니다. 마이그레이션, snapshot 복구 또는 distro 업그레이드 후에는 반드시 systemctl list-timers 'certbot*'과 --dry-run을 실행하십시오. 이 과정을 생략하면, 자동 갱신될 것이라고 예상했던 certificate가 89일 후 새벽 3시에 만료되어 사이트 접속이 중단됩니다.
이 모든 과정은 공인 IP와 80 port가 외부로 개방된, 사용자가 제어할 수 있는 VPS 환경을 전제로 합니다. 위에서 설명한 메커니즘은 모든 VPS에서 동일하게 작동합니다.
동일한 certificate 절차는 nginx 대신 Apache를 사용하는 경우에도 적용됩니다. 공인 certificate를 사용할 수 없는 경우에는 Ubuntu에서 self-signed certificate를 사용하여 내부 서비스를 보호하십시오.
FAQ
HTTPS만 사용하는 사이트인데 80번 포트를 열어야 합니까?
HTTP-01 challenge를 위해 필요합니다. Let's Encrypt는 항상 80번 포트로 검증 요청을 시작합니다. Certbot은 TLS-ALPN-01을 지원하지 않으므로, 443번 포트만 열려 있는 방화벽은 최초 인증과 이후의 자동 갱신을 모두 차단합니다. 80번 포트에서 HTTPS로 리다이렉트하는 설정은 괜찮습니다. 검증 과정이 리다이렉트를 따르기 때문입니다. 80번 포트를 완전히 건너뛰는 유일한 방법은 DNS-01 방식과 provider plugin을 사용하는 것입니다.
Ubuntu 24.04의 nginx용 Certbot으로 apt와 snap 중 무엇을 설치해야 합니까?
apt를 사용하십시오. Ubuntu 24.04에서 sudo apt install certbot python3-certbot-nginx를 사용하면 Certbot 2.9.0이 설치됩니다. 이 버전은 본 가이드의 모든 내용을 수행하기에 충분하며, unattended-upgrades를 통해 보안 패치를 받으며, snapd가 필요하지 않습니다. 최신 릴리스가 즉시 필요하거나 snap으로만 배포되는 DNS plugin이 필요한 경우에만 snap을 선택하십시오. 어떤 방식을 선택하든 하나만 선택해야 합니다. 두 가지를 모두 설치하면 동일한 /etc/letsencrypt 트리를 가리키는 두 개의 갱신 타이머가 생성되며, 나중에 잊혀진 타이머가 문제를 일으킵니다.
Certbot으로 nginx용 wildcard certificate를 발급받을 수 있습니까?
DNS-01 방식으로만 가능합니다. *.example.com와 같은 wildcard는 challenge 파일을 가져올 단일 hostname이 없으므로, --nginx, --webroot, --standalone 방식은 사용할 수 없습니다. 사용 중인 DNS provider용 plugin을 설치하고, 권한이 제한된 API token을 root 전용 credentials 파일에 저장한 뒤, shell globbing을 방지하기 위해 wildcard에 따옴표를 붙여 certbot certonly --dns-cloudflare -d example.com -d '*.example.com'를 실행하십시오.
갱신에 성공했는데 왜 nginx는 여전히 이전 인증서를 사용합니까?
nginx는 인증서를 메모리에 유지하며, reload를 수행하기 전까지는 디스크의 새 파일을 인식하지 않습니다. certbot --nginx은 자동으로 reload를 수행하지만, --webroot 및 --standalone 실행 시에는 수행되지 않습니다. 이로 인해 갱신은 성공했으나 브라우저에는 만료 예정인 인증서가 계속 표시될 수 있습니다. /etc/letsencrypt/renewal-hooks/deploy/ 디렉토리에 nginx -t && systemctl reload nginx를 실행하는 실행 가능한 스크립트를 넣어두면, 모든 성공적인 갱신 후에 해당 스크립트가 실행됩니다.
certbot renew --dry-run가 갱신 성공을 보장합니까?
대부분 그렇습니다. 이 명령은 staging environment를 대상으로 실제 challenge를 실행합니다. 방화벽, DNS, 코드 경로가 동일하며, rate-limit 제한이 없고 디스크에 아무것도 기록되지 않습니다. 따라서 이 명령이 통과되면 네트워크 설정은 정상입니다. 하지만 deploy hook이 정상 작동하는지는 확실히 증명하지 못합니다. 해당 부분은 별도로 테스트하십시오. hook 스크립트를 수동으로 실행하고 sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf을 확인하십시오.