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 đầy đủ để 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 theo dõi các bản phát hành upstream, còn gói archive theo phiên bản được phát hành cùng LTS và nhận các bản sửa lỗi bảo mật.
Chọn một cách. Cài hai bản Certbot sẽ tạo hai renewal timer cùng tác động lên cây thư mục /etc/letsencrypt. Bản bạn quên mất sẽ là bản gây 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ỡ gói apt trước khi cài snap.
Sau đó, cả hai cách cài đặt hoạt động giống nhau, ngoại trừ thời điểm chúng quyết định gia hạn (được trình bày trong phần Gia hạn bên dưới). Theo mặc định, Certbot 2.x dùng key ECDSA (P-256). Chỉ truyền --key-type rsa cho 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/ trỏ bằng symlink đến các file hiện tại, renewal/ chứa một file cấu hình cho mỗi certificate, còn accounts/ là key tài khoản ACME của bạn.
HTTP-01 thực sự hoạt động như thế nào và vì sao không thể bỏ qua cổng 80
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 qua 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ề đúng nội dung token mà Certbot vừa ghi vào disk. Cơ chế chỉ gồm như vậy. Từ đó có 3 hệ quả, và chúng là nguyên nhân của phần lớn lần cấp chứng chỉ thất bại.
- Cổng 80 phải truy cập được từ Internet công cộng, 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 lỗi lần cấp chứng chỉ và 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/hostsvà cache của browser 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 chỉ thử lại qua IPv4 khi kết nối IPv6 timeout. Kết nối IPv6 bị từ chối sẽ không được thử lại. Bản ghi AAAA cũ trỏ đến một host vẫn chấp nhận kết nối nhưng phục vụ nội dung khác cũng khiến validation thất bại hoàn toàn.
Redirect được phép: validation sẽ follow HTTP redirect 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, validation không bắt đầu ở cổng nào khác ngoài cổng 80. Certbot không có implementation cho TLS-ALPN-01, nên “chỉ dùng 443” không phải cách xử lý 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.
Một server block hoạt động cả trước và sau khi có certificate
Vấn đề phụ thuộc vòng: nginx từ chối khởi động khi `ssl_certificate` trỏ đến một file chưa tồn tại, còn Certbot không thể xác thực khi nginx đang tắt. Hãy đưa site lên cổng 80 trước.
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 certificate. 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;
}
}Prefix ACME dài hơn đã được ưu tiên trước `location /, nên return 301 không bao giờ nhận request challenge. Modifier ^~ phát huy tác dụng ở chỗ khác: khi prefix đó khớp, nginx bỏ qua hoàn toàn các location dùng regex, vì vậy một rule được thêm sau này như location ~ /\. { deny all; }` không thể chặn challenge. Giữ location đó trên cổng 80 để quá trình renewal tiếp tục hoạt động sau khi 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 một block proxy_pass`, và server block reverse proxy, từng dòng một trình bày các header mà application đó cần, còn ACME location và các directive TLS vẫn giữ nguyên.
Cú pháp HTTP/2 phụ thuộc vào phiên bản nginx. Ubuntu 24.04 đi kèm nginx 1.24, yêu cầu khai báo inline là `listen 443 ssl http2;, và sẽ không khởi động nếu bạn dùng directive riêng http2 on;. Debian 13 đi kèm nginx 1.26, yêu cầu http2 on; và chỉ cảnh báo rằng dạng inline đã deprecated. Trước tiên, hãy kiểm tra nginx -v`.
Trỏ nginx đến `live/, không bao giờ trỏ đến archive/. Các symlink live/ được trỏ lại sau mỗi lần renewal; một đường dẫn cố định vào archive/` sẽ giữ bạn ở certificate hết hạn mà không được thay thế.
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
Chứng chỉ Let's Encrypt có hiệu lực trong 90 ngày. Gói apt (Certbot 2.9.0) sẽ gia hạn khi còn dưới 30 ngày. snap theo dõi bản Certbot hiện tại. Từ phiên bản 4.1, Certbot cũng hỏi Let's Encrypt về thời điểm gia hạn được đề xuất thông qua ACME Renewal Info (ARI), nên có thể gia hạn sớm hơn. Với chứng chỉ Let's Encrypt, thời điểm này thường rơi vào khoảng hai phần ba thời hạn sử dụng. Dù theo cách nào, bạn cũng có khoảng 30 ngày để xử lý một lần gia hạn bị lỗi trước khi nó trở thành sự cố gián đoạn dịch vụ. Từ năm sau, các lần gia hạn sẽ đến thường xuyên hơn: profile mặc định của Let's Encrypt chuyển sang chứng chỉ 64 ngày vào February 10, 2027 và chứng chỉ 45 ngày vào February 16, 2028. Let's Encrypt không còn gửi email cảnh báo sắp hết hạn. Sẽ không có ai nhắc bạn, nên từ bây giờ bạn phải tự monitor.
Kiểm tra timer đi kèm với bản cài đặt của bạn:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew duyệt qua mọi config trong /etc/letsencrypt/renewal/, bỏ qua những config chưa đến hạn và gia hạn các config còn lại bằng những tùy chọn đã lưu trong config đó. Đây là các flag của lần chạy ban đầu, trừ khi sau đó bạn đã thay đổi chúng bằng certbot reconfigure. Vì vậy lần chạy đầu tiên rất quan trọng: đó là lần được ghi lại.
Gia hạn file trên disk không tự thay đổi gì. nginx vẫn tiếp tục phục vụ chứng chỉ cũ trong 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 việc đó cho một chứng chỉ, đồng thời lưu renew_hook = ... vào renewal config của chứng chỉ. certbot --nginx tự reload cho bạn; các setup --webroot và --standalone thì không. Hook bị thiếu chính là lý do một site vẫn phục vụ chứng chỉ đã hết hạn trong khi certbot certificates vẫn báo cáo chứng chỉ mới một cách bình thường. Mọi thành phần khác đọc chứng chỉ khi khởi động cũng cần hook tương tự. Một app chạy trong container, chẳng hạn cài đặt Nextcloud VPS bằng 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ộ thử thách với môi trường staging của Let's Encrypt: cùng code path, cùng firewall, cùng DNS, không ảnh hưởng đến rate limit production của bạn (staging có rate limit riêng và rộng hơn nhiều), đồng thời không lưu certificate vào disk. Nếu hôm nay lệnh chạy thành công, lần 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 máy chủ.
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 đã 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ừ ngoài vào: 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 máy chủ của bạn bằng curl -sSv http://example.com/.well-known/acme-challenge/test. Bản ghi AAAA cũ cũng tạo 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 phục vụ. 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 phục vụ. Tạo một file tại /var/www/example.com/.well-known/acme-challenge/test rồi truy cập file đó từ bên ngoài; nếu request trả về 404, vấn đề chưa bao giờ nằm ở certificate.
DNS problem: NXDOMAIN looking up A for example.com, hostname không phân giải công khai. Có thể record mới chưa được propagate, hoặc record nằm trong zone mà registrar của bạn không phục vụ.
too many certificates (5) already issued ..., một rate limit. Đây là giới hạn người dùng thường gặp khi lặp lại quá trình debug. Thông báo sẽ nêu rõ giới hạn và kết thúc bằng thời gian retry after. Let’s Encrypt giới hạn certificate trùng lặp, tức cùng chính xác một tập hostname, ở mức 5 certificate mỗi tuần và không có cách override giới hạn này, nên chỉ có thể chờ hết thời gian. Riêng giới hạn 50 certificate mới trên mỗi registered domain mỗi tuần có thể được tăng bằng cách yêu cầu override; request cho đúng cùng tập hostname như một certificate hiện có được tính là renewal và không bị áp dụng giới hạn này; renewal được điều phối qua ARI cũng được miễn mọi giới hạn. 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 cho một certificate chưa từng được cấp hoặc đã bị xóa bằng certbot delete. Comment server block TLS, khởi động nginx, issue certificate, rồi khôi phục block.
open() "/etc/letsencrypt/options-ssl-nginx.conf" failed, file đó được cài cùng package plugin của nginx. Trên máy certonly không có python3-certbot-nginx, hãy cài thêm plugin hoặc thay dòng include bằng các thiết lập ssl_protocols và ssl_ciphers của bạn.
Quản lý ở quy mô lớn
Một chứng chỉ có thể chứa tối đa 100 tên, và dùng một certbot --nginx -d a.example.com -d b.example.com ... duy nhất nghe có vẻ tiện, cho đến khi một bản ghi DNS cũ làm validation thất bại và khiến mọi tên khác trên chứng chỉ đó cũng không còn hoạt động. Các chứng chỉ riêng cho từng site sẽ hỏng độc lập, đúng với điều bạn muốn trên một máy chủ chạy nhiều hơn vài dịch vụ. Khi có nhiều hơn một số ít site, một front door hỗ trợ ACME sẽ đáng để dùng: reverse proxy Traefik chạy nhiều ứng dụng bằng Docker Compose sẽ tự yêu cầu và gia hạn chứng chỉ, 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, và so sánh Nginx với Caddy và Traefik chủ yếu phụ thuộc vào mức độ bạn muốn proxy tự xử lý phần chứng chỉ và cấu hình cho từng app.
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/, ACME account key của bạn, và bạn không thể tạo lại một bản 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 bản LTS mới, renewal timer không được chuyển theo. Sau mỗi lần migration, khôi phục 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 là nguyên nhân khiến một site ngừng hoạt động 89 ngày sau, vào lúc 3 giờ sáng, vì mọi người đều nghĩ chứng chỉ đang tự gia hạn.
Các route HTTP-01 ở trên giả định bạn kiểm soát máy chủ, máy có public IP và port 80 mở cho Internet, nói cách khác là một VPS; DNS-01 không cần những điều kiện đó. Các bước thực hiện ở trên giống nhau trên mọi loại máy chủ này.
Các bước xử lý chứng chỉ tương tự cũng áp dụng cho Apache thay vì nginx, và khi không thể dùng chứng chỉ public, chứng chỉ self-signed trên Ubuntu phù hợp cho các service nội bộ.
FAQ
Tôi có cần mở cổng 80 nếu website chỉ phục vụ HTTPS không?
Có, vì HTTP-01 challenge cần cổng này. Let’s Encrypt luôn bắt đầu request xác thực trên cổng 80. Certbot không có triển khai TLS-ALPN-01, nên 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, thông qua plugin của nhà cung cấp hoặc --manual --preferred-challenges dns của Certbot. Khi đó bạn phải tự thêm bản ghi TXT, và việc gia hạn sẽ không còn tự động trừ khi bạn cung cấp script --manual-auth-hook.
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 toàn bộ nội dung trong hướng dẫn, nhận bản vá bảo mật qua unattended-upgrades và không cần snapd. Chỉ chọn snap nếu bạn cần ngay bản phát hành 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, hãy chỉ dùng đúng một cách: hai bản cài đặt sẽ tạo hai renewal timer cùng trỏ đến một cây /etc/letsencrypt, và bản bị bỏ quên mới 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 DNS provider của bạn, đặt API token với phạm vi quyền giới hạn vào credentials file chỉ root được phép đọc, rồi chạy certbot certonly --dns-cloudflare -d example.com -d '*.example.com'. Đặ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 nginx, nhưng các lần chạy --webroot và --standalone thì không. Vì vậy, quá trình gia hạn có thể thành công trong khi trình duyệt vẫn nhận certificate sắp hết hạn. Đặt một script có quyền thực thi vào /etc/letsencrypt/renewal-hooks/deploy/ để script chạy nginx -t && systemctl reload nginx. Script này sẽ được chạy sau mỗi lần gia hạn thành công.
certbot renew --dry-run có chứng minh việc gia hạn sẽ hoạt động không?
Phần lớn là có. Lệnh này chạy challenge thật với staging environment, dùng cùng firewall, cùng DNS và cùng code path, nhưng không ảnh hưởng đến production rate limit và không lưu certificate nào xuống disk. Vì vậy, nếu kiểm tra thành công thì phần network đã hoạt động đúng. Tuy nhiên, lệnh này không chứng minh chắc chắn deploy hook sẽ 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.