Cài Certbot cho nginx trên Ubuntu 24.04
Cài bằng apt hoặc snap, chạy certbot --nginx một lần để lấy chứng chỉ Let's Encrypt. Tránh trùng renewal timer và lỗi timeout HTTP-01 ở cổng 80.
Cài Certbot: apt hoặc snap
Trên Ubuntu 24.04, sudo apt install certbot python3-certbot-nginx cung cấp Certbot hoạt động được để cấp chứng chỉ Let's Encrypt thực và được các trình duyệt công khai tin cậy. Tài liệu upstream của Certbot hướng dẫn dùng snap; khác biệt không lớn: snap bám theo các bản phát hành upstream, còn package archive bám theo phiên bản được phát hành cùng LTS và chỉ nhận các bản sửa lỗi bảo mật.
Chọn một cách. Hai bản sao Certbot sẽ tạo hai renewal timer cùng nhắm đến cây thư mục /etc/letsencrypt. Bản bạn quên mất sẽ là bản gây ra sự cố bất ngờ.
Cách dùng apt:
sudo apt update
sudo apt install certbot python3-certbot-nginxLệnh này cài /usr/bin/certbot, plugin nginx, một cặp certbot.service + certbot.timer và một mục /etc/cron.d/certbot không thực hiện gì khi chạy dưới systemd.
Cách dùng 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 có timer riêng là snap.certbot.renew.timer. Hãy gỡ package apt trước khi cài snap.
Sau đó, hai cách cài đặt hoạt động giống nhau. Certbot 2.x mặc định dùng key ECDSA (P-256). Chỉ truyền --key-type rsa khi client không hỗ trợ ECDSA. Toàn bộ state nằm dưới /etc/letsencrypt: archive/ chứa các file key và certificate thực, live/ là các symlink trỏ đến file hiện tại, renewal/ chứa một file cấu hình cho mỗi certificate, còn accounts/ chứa ACME account key của bạn.
HTTP-01 thực sự làm gì và vì sao cổng 80 là bắt buộc
Challenge HTTP-01 là một callback. Bạn yêu cầu Let's Encrypt cấp chứng chỉ cho example.com; Let's Encrypt phân giải tên này trong public DNS, mở kết nối đến cổng 80 tại địa chỉ tìm được, rồi yêu cầu http://example.com/.well-known/acme-challenge/<token>. Server của bạn trả về nội dung token chính xác mà Certbot vừa ghi vào disk. Đó là toàn bộ cơ chế. Từ đó có 3 hệ quả, và chúng giải thích phần lớn các lần cấp chứng chỉ thất bại.
- Cổng 80 phải truy cập được từ public Internet, không chỉ từ laptop của bạn. Rule
ufw, security group của cloud provider hoặc firewall trong console của VPS chỉ mở cổng 443 sẽ làm việc cấp chứng chỉ thất bại, đồng thời làm hỏng mọi lần gia hạn sau đó. - DNS phải trỏ đến server này từ trước. Validation server tự thực hiện lookup từ bên ngoài; các entry
/etc/hostscủa bạn và browser cache không có ý nghĩa với nó. - Nếu bạn công bố bản ghi AAAA, IPv6 sẽ được thử trước. Let's Encrypt sẽ thử lại qua IPv4 khi kết nối IPv6 thất bại hoàn toàn, nhưng một bản ghi AAAA cũ trỏ đến host chấp nhận kết nối rồi phục vụ nội dung khác sẽ khiến validation thất bại ngay.
Redirect được phép: validation sẽ đi theo redirect HTTP sang HTTPS và không quan tâm chứng chỉ ở đầu bên kia bị thiếu, hết hạn hay là self-signed. Tuy nhiên, nó sẽ không bắt đầu ở bất kỳ cổng nào khác ngoài cổng 80. Certbot không có implementation cho TLS-ALPN-01, vì vậy “chỉ dùng 443” không phải là cách thay thế.
Chọn authenticator: --nginx, --webroot, --standalone
--nginx là lựa chọn mặc định phù hợp khi nginx đang chạy và đã phục vụ domain. Certbot phân tích cấu hình của bạn, thêm tạm thời một location để xử lý challenge, reload nginx, xác thực, rồi ghi các chỉ thị TLS vào server block. Không gây downtime.
sudo certbot --nginx -d example.com -d www.example.comDùng trong script cho một máy chủ mới:
sudo certbot --nginx \
-d example.com -d www.example.com \
--agree-tos -m ops@example.com --no-eff-email \
--redirect --non-interactive--webroot phù hợp khi bạn không muốn Certbot can thiệp vào cấu hình nginx, chẳng hạn cấu hình được tạo từ template, lưu trong git hoặc triển khai bằng Ansible. Certbot chỉ ghi file challenge vào một directory mà bạn đã cấu hình để phục vụ.
sudo certbot certonly --webroot -w /var/www/example.com \
-d example.com -d www.example.com \
--deploy-hook "systemctl reload nginx"--standalone phù hợp khi không có tiến trình nào listen trên port 80: máy chủ mail, một API chỉ dùng 443 hoặc script first-boot chạy trước khi nginx tồn tại. Certbot tự bind port 80 trong vài giây. Nếu nginx đang chạy, lệnh này sẽ fail; hãy stop nginx trước và start lại sau khi chạy xong:
sudo certbot certonly --standalone -d mail.example.com \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"Các hook này được ghi vào renewal config của certificate, nên cùng thao tác stop/start sẽ tự động được thực hiện khi gia hạn.
Server block hoạt động trước và sau khi chứng chỉ tồn tại
Vấn đề “con gà và quả trứng”: nginx từ chối khởi động khi ssl_certificate trỏ đến một file không tồn tại, còn Certbot không thể xác thực khi nginx đang dừng. Trước tiên, hãy đưa site lên cổng 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;
}
}Chạy sudo nginx -t && sudo systemctl reload nginx, xác nhận curl -I http://example.com/ trả lời từ bên ngoài máy chủ, rồi cấp chứng chỉ. Sau đó:
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;
}
}Tiền tố ^~ trên location ACME rất cần thiết: nó ngăn block return 301 bắt nhầm request challenge. Giữ location này trên cổng 80 để quá trình gia hạn tiếp tục hoạt động sau khi toàn bộ phần còn lại của site chỉ chạy qua HTTPS.
Cả hai block trên đều phục vụ file từ disk. Nếu nginx thay vào đó đứng trước một application, location / sẽ trở thành block proxy_pass. Server block reverse proxy, giải thích từng dòng trình bày các header mà application đó cần, còn location ACME và các directive TLS giữ nguyên.
Cú pháp HTTP/2 phụ thuộc vào version nginx. Dùng nhầm hai dạng sẽ gây lỗi khi khởi động. Ubuntu 24.04 cung cấp nginx 1.24, yêu cầu khai báo inline, listen 443 ssl http2;. Debian 13 cung cấp nginx mới hơn, yêu cầu directive http2 on; riêng. Hãy kiểm tra nginx -v trước.
Trỏ nginx đến live/, không bao giờ trỏ đến archive/. Các symlink live/ được trỏ lại sau mỗi lần gia hạn. Đường dẫn cố định vào archive/ sẽ khiến bạn tiếp tục dùng một chứng chỉ hết hạn mà không nhận ra.
Wildcard certificate dùng DNS-01, và DNS-01 cần plugin
Wildcard certificate (*.example.com) không thể được xác thực qua HTTP-01 vì không có một hostname duy nhất để truy cập file. DNS-01 là cách duy nhất: bạn chứng minh quyền kiểm soát bằng cách publish một _acme-challenge.example.com TXT record. Certbot cần API credentials của DNS provider để thực hiện việc này ở chế độ unattended. Đây là lý do provider plugin tồn tại. Hướng dẫn đầy đủ về wildcard certificate giải thích cơ chế TXT record và lỗi gia hạn khi dùng manual mode; bên dưới là phiên bản ngắn cho Cloudflare.
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflareVới cách cài qua apt thì sudo apt install python3-certbot-dns-cloudflare thay vào đó. Đặt credentials trong một file chỉ root được quyền đọc:
# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_hereGiới hạn token chỉ có quyền chỉnh sửa DNS trên zone đó. Đây là key dùng cho DNS của bạn; hãy bảo vệ nó như một key quan trọng.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d example.com -d '*.example.com'Đặt wildcard trong dấu nháy để shell không thực hiện glob. DNS-01 cũng giải quyết được những trường hợp HTTP-01 không xử lý được: certificate cho host không có public port 80, một service nội bộ, một máy chỉ có thể truy cập qua WireGuard VPN tự host trên VPS, hoặc một admin panel trên private interface.
Gia hạn: 90 ngày, timer và deploy hook
Certificate của Let's Encrypt có hiệu lực trong 90 ngày. Certbot sẽ gia hạn khi thời hạn còn dưới 30 ngày. Nhờ vậy, bạn có 30 ngày để xử lý lỗi gia hạn trước khi nó gây gián đoạn dịch vụ. Let's Encrypt không còn gửi email cảnh báo sắp hết hạn. Không ai tự nhắc bạn, nên việc monitoring là trách nhiệm của bạn.
Kiểm tra timer đi kèm với bản cài đặt:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew duyệt mọi cấu hình trong /etc/letsencrypt/renewal/, bỏ qua các certificate còn hơn 30 ngày mới hết hạn và gia hạn những certificate còn lại bằng đúng các flag của lần chạy ban đầu. Vì vậy, lần chạy đầu tiên rất quan trọng: đó là lần chạy được ghi lại.
Gia hạn file trên disk không tự thay đổi gì. nginx vẫn tiếp tục cung cấp certificate cũ từ memory cho đến khi có lệnh yêu cầu reload. Hãy cấu hình deploy hook một lần:
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.shMọi file có quyền thực thi trong renewal-hooks/deploy/ sẽ chạy sau mỗi lần gia hạn thành công. Flag --deploy-hook thực hiện cùng công việc cho một certificate và lưu renew_hook = ... vào renewal config của certificate đó. certbot --nginx tự reload cho bạn; các setup dùng --webroot và --standalone thì không. Hook bị thiếu là lý do chính khiến một site vẫn cung cấp certificate đã hết hạn trong khi certbot certificates vẫn báo certificate mới bình thường. Mọi thành phần khác đọc certificate khi startup cũng cần hook tương tự. Một app chạy trong container như cài đặt Nextcloud VPS với Docker, TLS và backup cũng cần có bước restart hoặc reload riêng được cấu hình tại đây.
Kiểm tra gia hạn thực tế
sudo certbot renew --dry-runLệnh này chạy toàn bộ challenge với môi trường staging của Let's Encrypt: cùng code path, cùng firewall, cùng DNS, không tốn rate limit và không ghi gì vào disk. Nếu hôm nay chạy thành công, tiến trình gia hạn tự động sau 60 ngày cũng sẽ thành công, với điều kiện không có thay đổi nào khác trên server.
Dry run không chứng minh được reload hook của bạn sẽ chạy. Hành vi này thay đổi tùy phiên bản Certbot. Hãy kiểm tra phần đó thủ công: chạy trực tiếp hook script, xác nhận systemctl reload nginx thành công và kiểm tra sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.
Các lỗi bạn sẽ thực sự gặp
Could not bind to IPv4 or IPv6., --standalone trong khi nginx đang giữ cổng 80. Dùng --nginx hoặc --webroot, hoặc dừng nginx trong lúc chạy. Xác nhận tiến trình đang giữ cổng bằng sudo ss -lntp | grep ':80'.
Timeout during connect (likely firewall problem), Let’s Encrypt không thể truy cập cổng 80. Kiểm tra từ trong ra ngoài: sudo ufw status (mở bằng sudo ufw allow 'Nginx Full'), sau đó kiểm tra firewall riêng của nhà cung cấp VPS, rồi đến DNS. Kiểm tra từ một nơi không phải server của bạn: curl -sSv http://example.com/.well-known/acme-challenge/test. Bản ghi AAAA cũ cũng gây ra cùng thông báo này.
unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404, cổng 80 có thể truy cập, nhưng token không được cung cấp. Request đã vào một server block khác (kiểm tra server block nào sở hữu default_server), hoặc thư mục truyền cho -w không phải thư mục nginx đang serve. Tạo một file tại /var/www/example.com/.well-known/acme-challenge/test rồi fetch file đó từ bên ngoài; nếu request trả về 404, vấn đề không nằm ở certificate.
DNS problem: NXDOMAIN looking up A for example.com, hostname không resolve công khai. Có thể record mới chưa propagate, hoặc record nằm trong zone mà registrar của bạn không serve.
too many certificates already issued for: example.com, đây là rate limit và là lỗi mọi người thường gặp khi lặp đi lặp lại quá trình debug. Let’s Encrypt giới hạn certificate trùng lặp, tức cùng chính xác một bộ hostname, ở mức 5 certificate mỗi tuần. Ngoài ra, hệ thống cho phép 50 certificate mới cho mỗi registered domain mỗi tuần. Không cách nào gỡ giới hạn này ngoài việc chờ. Debug trên staging bằng --dry-run.
nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory, nginx được cấu hình dùng một certificate chưa từng được cấp hoặc đã bị xóa bằng certbot delete. Comment out TLS server block, khởi động nginx, cấp certificate, rồi khôi phục block đó.
open() "/etc/letsencrypt/options-ssl-nginx.conf" failed, file này đi kèm package nginx plugin. Trên máy certonly không có python3-certbot-nginx, bạn có thể thêm plugin hoặc thay dòng include bằng các thiết lập ssl_protocols và ssl_ciphers của riêng bạn.
Quản lý ở quy mô lớn
Một certificate có thể chứa tối đa 100 tên miền, và một certbot --nginx -d a.example.com -d b.example.com ... duy nhất khá hấp dẫn, cho đến khi một bản ghi DNS cũ khiến quá trình xác thực thất bại và kéo theo mọi tên miền khác trên certificate đó cùng bị lỗi. Các certificate riêng cho từng site sẽ lỗi độc lập. Đây là cách phù hợp khi một máy chủ host nhiều hơn vài dịch vụ. Khi số site vượt quá vài cái, một front door có hỗ trợ ACME sẽ trở nên đáng dùng: Traefik reverse proxy chạy nhiều app bằng Docker Compose tự request và gia hạn certificate, còn Certbot không cần tham gia nữa. Chọn proxy nào làm front door là một quyết định riêng. So sánh Nginx với Caddy và Traefik chủ yếu phụ thuộc vào mức độ công việc về certificate và cấu hình từng app mà bạn muốn proxy tự xử lý.
Sao lưu toàn bộ /etc/letsencrypt, giữ nguyên sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt và symlink. Cây thư mục đó chứa accounts/, là ACME account key của bạn và không thể tạo lại giống hệt. Khi chuyển sang VPS mới, quy trình chỉ còn: rsync cây thư mục bằng -a, cài Certbot, trỏ lại DNS và chạy certbot renew --dry-run trước khi chuyển lưu lượng.
Khi rebuild máy chủ hoặc chuyển sang một bản LTS mới, renewal timer không đi theo bạn. Sau mỗi lần migration, restore snapshot hoặc nâng cấp distro, hãy chạy systemctl list-timers 'certbot*' và một lần --dry-run. Bỏ qua bước này có thể khiến site ngừng hoạt động 89 ngày sau, lúc 3am, do mọi người đã mặc định certificate tự gia hạn.
Tất cả nội dung trên giả định bạn kiểm soát một máy có public IP và port 80 mở cho Internet, nói cách khác là một VPS. Các bước thực hiện giống nhau trên mọi loại máy như vậy.
Các bước xử lý certificate tương tự cũng áp dụng trên Apache thay vì nginx, còn khi không thể dùng public certificate, self-signed certificate trên Ubuntu phù hợp cho các dịch vụ nội bộ.
FAQ
Tôi có cần mở cổng 80 nếu website chỉ phục vụ HTTPS không?
Có, để thực hiện challenge HTTP-01. Let’s Encrypt luôn bắt đầu yêu cầu xác thực trên cổng 80, và Certbot không có implementation cho TLS-ALPN-01. Vì vậy, firewall chỉ mở cổng 443 sẽ chặn cả lần cấp chứng chỉ đầu tiên lẫn mọi lần gia hạn tự động sau đó. Redirect từ cổng 80 sang HTTPS là được; quá trình xác thực sẽ đi theo redirect này. Cách duy nhất để bỏ hoàn toàn cổng 80 là dùng DNS-01 với plugin của nhà cung cấp DNS.
Nên cài Certbot bằng apt hay snap cho nginx trên Ubuntu 24.04?
Dùng apt. sudo apt install certbot python3-certbot-nginx cung cấp Certbot 2.9.0 trên Ubuntu 24.04. Phiên bản này đủ mới cho mọi nội dung trong hướng dẫn này, nhận bản vá bảo mật thông qua unattended-upgrades và không cần snapd. Chỉ chọn snap nếu bạn cần ngay bản release mới nhất hoặc cần một DNS plugin chỉ được phân phối dưới dạng snap. Dù chọn cách nào, chỉ dùng đúng một cách: cài cả hai sẽ tạo ra hai renewal timer cùng trỏ đến một cây /etc/letsencrypt, và bản cài bị bỏ quên sẽ là bản gây sự cố.
Certbot có thể cấp wildcard certificate cho nginx không?
Chỉ khi dùng DNS-01. Wildcard như *.example.com không có một hostname duy nhất để tải challenge file, nên --nginx, --webroot và --standalone đều không dùng được. Cài plugin cho nhà cung cấp DNS của bạn, đặt API token có scope giới hạn vào credentials file chỉ root được đọc, rồi chạy certbot certonly --dns-cloudflare -d example.com -d '*.example.com' và đặt wildcard trong dấu nháy để shell không xử lý nó như glob.
Tại sao nginx vẫn phục vụ certificate cũ sau khi gia hạn thành công?
nginx giữ certificate trong memory và không nhận biết file mới trên disk cho đến khi được reload. certbot --nginx tự reload cho bạn, nhưng các lần chạy --webroot và --standalone thì không, nên quá trình gia hạn có thể thành công trong khi browser vẫn thấy certificate sắp hết hạn. Đặt một script có quyền execute vào /etc/letsencrypt/renewal-hooks/deploy/ để script này chạy nginx -t && systemctl reload nginx; nó sẽ được thực thi sau mỗi lần gia hạn thành công.
certbot renew --dry-run có chứng minh được việc gia hạn sẽ hoạt động không?
Phần lớn là có. Lệnh này thực hiện challenge thật với staging environment, dùng cùng firewall, cùng DNS và cùng code path, không tốn rate limit và không ghi gì vào disk. Vì vậy, nếu pass thì phần network đã hoạt động đúng. Tuy nhiên, lệnh này không reliably chứng minh deploy hook sẽ được chạy. Hãy kiểm tra riêng: chạy hook script thủ công và kiểm tra sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.