SSD Nodes Learn
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-07-23

Cách cài Certbot cho nginx trên Ubuntu 24.04

Hướng dẫn cài Certbot qua apt hoặc snap trên Ubuntu 24.04. Lưu ý quan trọng về việc gỡ gói cũ để tránh lỗi timer renewal và yêu cầu mở port 80 cho HTTP-01.

Cài đặt Certbot: apt hoặc snap

Trên Ubuntu 24.04, sudo apt install certbot python3-certbot-nginx cung cấp cho bạn một Certbot hoạt động tốt để cấp các chứng chỉ Let's Encrypt thực, được tin cậy công khai. Tài liệu chính thức của Certbot hướng bạn dùng snap; sự khác biệt là rất nhỏ — snap theo sát các bản phát hành mới nhất, còn gói archive theo bản phân phối LTS và nhận các bản vá bảo mật.

Hãy chọn một cái. Hai bản cài đặt Certbot đồng nghĩa với hai bộ đếm thời gian renewal cùng nhắm vào một cây /etc/letsencrypt, và bản mà bạn quên mất sẽ là bản gây rắc rối cho bạn.

Sử dụng apt:

sudo apt update
sudo apt install certbot python3-certbot-nginx

Lệnh này cài đặt /usr/bin/certbot, nginx plugin, một cặp certbot.service + certbot.timer, và một entry /etc/cron.d/certbot không thực hiện gì dưới systemd.

Sử 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/certbot

Snap đi kèm với timer riêng, snap.certbot.renew.timer. Hãy gỡ gói apt trước khi cài đặt snap.

Sau đó, cả hai cách cài đặt đều hoạt động như nhau. Certbot 2.x mặc định dùng key ECDSA (P-256) — chỉ truyền --key-type rsa nếu client không hỗ trợ ECDSA. Mọi trạng thái nằm trong /etc/letsencrypt: archive/ giữ các file key và certificate thật, live/ là các symlinks đến các file hiện tại, renewal/ là một file config cho mỗi certificate, accounts/ là ACME account key của bạn.

HTTP-01 thực sự làm gì, và tại sao port 80 là bắt buộc

HTTP-01 challenge là một callback. Bạn yêu cầu Let's Encrypt cấp certificate cho example.com; nó sẽ resolve tên trong public DNS, mở kết nối tới port 80 tại địa chỉ tìm được, và yêu cầu http://example.com/.well-known/acme-challenge/<token>. Server của bạn phải trả về đúng nội dung token mà Certbot vừa ghi vào disk. Đó là toàn bộ cơ chế. Có ba hệ quả từ việc này, và chúng là nguyên nhân của hầu hết các ca cấp certificate thất bại.

  • Port 80 phải có thể truy cập được từ internet, không chỉ từ laptop của bạn. Một rule ufw, một cloud-provider security group, hoặc firewall của VPS-console chỉ mở port 443 sẽ làm lỗi việc cấp certificate và mọi lần renewal sau đó.
  • DNS phải trỏ về máy này. Server xác thực sẽ tự thực hiện lookup từ bên ngoài; các entry /etc/hosts và browser cache của bạn không có ý nghĩa gì với nó.
  • Nếu bạn publish 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ỏ vào một host chấp nhận kết nối nhưng trả về nội dung khác sẽ khiến bạn bị lỗi hoàn toàn.

Redirect được cho phép: việc xác thực sẽ đi theo HTTP redirect sang HTTPS và không quan tâm việc certificate ở đầu kia bị thiếu, hết hạn hoặc tự ký. Điều nó không làm là bắt đầu từ bất kỳ đâu khác ngoài port 80. Certbot không có implementation cho TLS-ALPN-01, nên "chỉ dùng 443" không phải là giải pháp thay thế.

Chọn authenticator: --nginx, --webroot, --standalone

--nginx là lựa chọn mặc định đúng đắn khi nginx đã chạy và đang phục vụ domain đó. Certbot sẽ parse config của bạn, inject một vị trí challenge tạm thời, reload nginx, xác thực, sau đó ghi các directive TLS vào server block của bạn. Không gây downtime.

sudo certbot --nginx -d example.com -d www.example.com

Dùng script, cho một máy 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 config nginx — ví dụ config bạn tạo từ template, giữ trong git, hoặc push bằng Ansible. Certbot chỉ ghi file challenge vào một thư mục mà bạn đã cấu hình phục vụ sẵn.

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ó gì đang lắng nghe trên port 80: một mail server, một API chỉ nói chuyện qua 443, hoặc một script khởi động chạy trước khi nginx tồn tại. Certbot sẽ tự bind port 80 trong vài giây. Nếu nginx đang chạy, lệnh này sẽ lỗi — hãy stop nó trước khi chạy:

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 lại trong config renewal của certificate, nên việc stop/start tương tự sẽ tự động diễn ra khi renewal.

Một server block hoạt động được cả trước và sau khi có certificate

Vấn đề "con gà và quả trứng": nginx từ chối khởi động nếu ssl_certificate trỏ vào một file không tồn tại, và Certbot không thể xác thực khi nginx đang tắt. Hãy chạy site trên port 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, sau đó tiến hành 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;
    }
}

Tiền tố ^~ trên ACME location rất quan trọng: nó ngăn block return 301 nuốt mất request challenge. Việc giữ location đó trên port 80 giúp việc renewal vẫn hoạt động sau khi toàn bộ site đã chuyển sang HTTPS-only.

Cú pháp HTTP/2 phụ thuộc vào phiên bản nginx của bạn, và việc dùng lẫn lộn hai dạng sẽ gây lỗi khi khởi động. Ubuntu 24.04 đi kèm nginx 1.24, yêu cầu viết inline — listen 443 ssl http2;. Debian 13 đi kèm nginx mới hơn, yêu cầu directive http2 on; riêng biệt. Hãy kiểm tra nginx -v trước.

Hãy trỏ nginx vào live/, đừng trỏ vào archive/. Các symlink live/ sẽ được trỏ lại sau mỗi lần renewal; nếu dùng đường dẫn cứng vào archive/, bạn sẽ bị kẹt với một certificate sắp hết hạn.

Wildcard nghĩa là DNS-01, và DNS-01 nghĩa là cần plugin

Một 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 để fetch file challenge. DNS-01 là con đường duy nhất: bạn chứng minh quyền kiểm soát bằng cách publish một bản ghi TXT _acme-challenge.example.com. Certbot cần credentials API của DNS provider để làm việc này tự động, đó là lý do các provider plugins tồn tại. Hướng dẫn chi tiết về wildcard certificate sẽ nói về cơ chế bản ghi TXT và bẫy renewal ở chế độ manual; bản rút gọn cho Cloudflare sẽ theo sau.

sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare

Trên đường dẫn apt, nó sẽ là sudo apt install python3-certbot-dns-cloudflare. Credentials nằm trong một file chỉ dành cho root:

# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_here

Giới hạn token trong quyền chỉnh sửa DNS cho duy nhất zone đó. Đó là key cho DNS của bạn; hãy đối xử với nó như một key thật sự.

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

Hãy để wildcard trong dấu ngoặc kép để shell không thực hiện globbing. DNS-01 cũng giải quyết được những gì HTTP-01 không thể: cấp certificate cho các host không có port 80 công khai — một dịch vụ nội bộ, một máy chỉ truy cập được qua self-hosted WireGuard VPN trên một VPS, hoặc một admin panel trên interface riêng tư.

Renewal: 90 ngày, timer, và deploy hook

Các certificate của Let's Encrypt có thời hạn 90 ngày. Certbot sẽ renew khi còn ít hơn 30 ngày, giúp bạn có 30 ngày để xử lý nếu việc renewal bị lỗi, thay vì bị sập dịch vụ. Let's Encrypt không còn gửi email cảnh báo hết hạn — sẽ không có ai nhắc bạn đâu, nên việc giám sát là trách nhiệm của bạn.

Kiểm tra timer mà bản cài đặt đã thiết lập:

systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificates

certbot renew sẽ quét mọi config trong /etc/letsencrypt/renewal/, bỏ qua bất kỳ thứ gì nằm ngoài cửa sổ 30 ngày, và renew phần còn lại bằng chính xác các flag của lần chạy đầu tiên. Đó là lý do lần chạy đầu tiên rất quan trọng: nó là lần được ghi lại.

Việc renew file trên disk không tự thay đổi gì cả — nginx vẫn tiếp tục phục vụ certificate cũ từ bộ nhớ cho đến khi có lệnh reload. Hãy thiết lập một 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.sh

Bất kỳ thứ gì có thể thực thi trong renewal-hooks/deploy/ sẽ chạy sau mỗi lần renewal thành công. Flag --deploy-hook cũng làm nhiệm vụ tương tự cho một certificate, bằng cách lưu renew_hook = ... vào config renewal của nó. certbot --nginx sẽ reload giúp bạn; các thiết lập --webroot--standalone thì không. Việc thiếu hook chính là lý do tại sao một site vẫn phục vụ certificate đã hết hạn trong khi certbot certificates báo cáo một certificate mới tinh. Bất kỳ thứ gì khác đọc certificate khi khởi động đều cần hook tương tự — một ứng dụng chạy trong container như Nextcloud VPS install với Docker, TLS và backups cũng cần một bước restart hoặc reload được thiết lập tại đây.

Kiểm tra thử renewal thực tế

sudo certbot renew --dry-run

Lệnh này chạy full 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, không ghi gì vào disk. Nếu nó pass hôm nay, việc renewal tự động sau 60 ngày cũng sẽ pass, với điều kiện không có gì thay đổi trên máy.

Dry run không chứng minh được hook reload của bạn có chạy hay không — hành vi này thay đổi tùy theo phiên bản Certbot. Hãy test phần đó bằng tay: chạy trực tiếp script hook, 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 khi nginx đang giữ port 80. Hãy dùng --nginx hoặc --webroot, hoặc stop nginx khi chạy. Xác nhận bên đang giữ port bằng sudo ss -lntp | grep ':80'.

Timeout during connect (likely firewall problem) — Let's Encrypt không thể kết nối tới port 80. Hãy kiểm tra dần: sudo ufw status (mở nó bằng sudo ufw allow 'Nginx Full'), sau đó là firewall của chính nhà cung cấp VPS, rồi đến DNS. Test từ một nơi không phải server của bạn: curl -sSv http://example.com/.well-known/acme-challenge/test. Một bản ghi AAAA cũ cũng gây ra lỗi này.

unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404 — port 80 có thể truy cập được, nhưng token không được trả về. Request đã rơi vào một server block khác (kiểm tra xem cái nào sở hữu default_server), hoặc thư mục truyền vào -w không phải là thư mục nginx đang phục vụ. Thử đặt một file tại /var/www/example.com/.well-known/acme-challenge/test và fetch nó từ bên ngoài; nếu nó trả về 404, thì vấn đề không nằm ở certificate.

DNS problem: NXDOMAIN looking up A for example.com — tên không resolve được công khai. Các bản ghi mới chưa được propagate, hoặc bản ghi trong một zone mà registrar của bạn không phục vụ.

too many certificates already issued for: example.com — bị rate limit, lỗi mà mọi người hay gặp khi debug trong một vòng lặp. Let's Encrypt giới hạn các certificate trùng lặp — cùng một tập hợp các tên — tối đa 5 cái mỗi tuần, và cho phép 50 certificate mới cho mỗi domain đã đăng ký mỗi tuần; không có gì gỡ được lỗi này ngoại trừ thời gian. Hãy debug với 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 một cái đã bị xóa bằng certbot delete. Hãy comment out server block TLS, start nginx, cấp certificate, rồi khôi phục lại block đó.

open() "/etc/letsencrypt/options-ssl-nginx.conf" failed — file đó đi kèm với gói nginx plugin. Trên máy certonly không có python3-certbot-nginx, hãy thêm plugin hoặc thay thế dòng include bằng các thiết lập ssl_protocolsssl_ciphers của riêng bạn.

Quản lý ở quy mô lớn

Một certificate có thể chứa tới 100 tên, và một certbot --nginx -d a.example.com -d b.example.com ... duy nhất nghe có vẻ hấp dẫn — cho đến khi một bản ghi DNS cũ làm lỗi validation và kéo sập tất cả các tên khác trên certificate đó. Các certificate riêng biệt cho mỗi site sẽ lỗi độc lập, đó là điều bạn muốn trên một máy host nhiều hơn vài thứ. Khi có nhiều site, một "cánh cửa" (front door) hiểu ACME sẽ rất đáng giá: một Traefik reverse proxy chạy nhiều apps dưới Docker Compose sẽ tự yêu cầu và renew certificate, và Certbot sẽ không còn cần thiết nữa.

Hãy backup toàn bộ /etc/letsencryptsudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt — giữ nguyên các symlinks. Cây thư mục đó chứa accounts/, ACME account key của bạn, thứ mà bạn không thể tạo lại y hệt. Chuyển sang VPS mới khi đó chỉ còn là: rsync cây thư mục với -a, cài Certbot, trỏ lại DNS, và chạy certbot renew --dry-run trước khi chuyển đổi chính thức.

Nếu bạn rebuild máy hoặc chuyển sang một bản LTS mới, timer renewal sẽ không đi theo bạn. Sau bất kỳ đợt migration, restore snapshot, hoặc upgrade distro nào, hãy chạy systemctl list-timers 'certbot*' và một lệnh --dry-run. Bỏ qua bước này là cách khiến một site sập sau 89 ngày, vào lúc 3 giờ sáng, với một certificate mà mọi người đều tưởng là nó đã tự renew.

Tất cả những điều này giả định bạn có quyền kiểm soát máy, có IP công khai và port 80 mở cho thế giới — nói cách khác là một VPS. Các cơ chế trên là giống nhau trên bất kỳ máy nào như vậy.

Các bước làm certificate tương tự trên Apache thay vì nginx, và khi không thể dùng certificate công khai, một self-signed certificate trên Ubuntu sẽ đáp ứng được các dịch vụ nội bộ.

FAQ

Tôi có cần mở port 80 nếu site của tôi chỉ phục vụ HTTPS không?

Có, để dùng HTTP-01 challenge. Let's Encrypt luôn bắt đầu yêu cầu xác thực trên port 80, và Certbot không có implementation cho TLS-ALPN-01, nên một firewall chỉ mở port 443 sẽ chặn cả lần cấp đầu tiên và mọi lần renewal tự động sau đó. Redirect từ port 80 sang HTTPS là ổn — việc xác thực sẽ đi theo nó. Cách duy nhất để bỏ qua hoàn toàn port 80 là dùng DNS-01 với một provider plugin.

apt hay snap — tôi nên cài Certbot nào cho nginx trên Ubuntu 24.04?

Hãy dùng apt. sudo apt install certbot python3-certbot-nginx cung cấp cho bạn Certbot 2.9.0 trên Ubuntu 24.04, phiên bản đủ mới cho mọi thứ trong hướng dẫn này, nhận được các bản vá bảo mật qua unattended-upgrades, và không yêu cầu snapd. Chỉ chọn snap nếu bạn cần bản phát hành mới nhất ngay lập tức hoặc cần một DNS plugin chỉ được phân phối dưới dạng snap. Dù thế nào, hãy chọn duy nhất một cái: hai bản cài đặt nghĩa là hai bộ đếm thời gian renewal cùng nhắm vào một cây /etc/letsencrypt, và bản bị quên sẽ gây rắc rối cho bạn.

Certbot có thể cấp wildcard certificate cho nginx không?

Chỉ qua DNS-01. Một wildcard như *.example.com không có một hostname duy nhất để fetch file challenge, nên --nginx, --webroot--standalone đều không khả dụng. Hãy cài plugin cho DNS provider của bạn, đặt một API token có phạm vi giới hạn vào một file credentials chỉ dành cho root, và chạy certbot certonly --dns-cloudflare -d example.com -d '*.example.com', nhớ để wildcard trong dấu ngoặc kép để tránh shell globbing.

Tại sao nginx vẫn phục vụ certificate cũ sau khi renewal thành công?

nginx giữ certificate trong bộ nhớ và không nhận ra file mới trên disk cho đến khi nó được reload. certbot --nginx sẽ reload giúp bạn, nhưng các lệnh --webroot--standalone thì không, nên một lần renewal có thể thành công trong khi trình duyệt vẫn thấy certificate sắp hết hạn. Hãy đặt một script thực thi vào /etc/letsencrypt/renewal-hooks/deploy/ để chạy nginx -t && systemctl reload nginx, nó sẽ được kích hoạt sau mỗi lần renewal thành công.

certbot renew --dry-run có chứng minh được việc renewal sẽ hoạt động không?

Phần lớn là có. Nó chạy challenge thật với môi trường staging — cùng firewall, cùng DNS, cùng code path — không tốn rate-limit và không ghi gì vào disk, nên nếu pass nghĩa là phần mạng đã ổn. Tuy nhiên, nó không chứng minh được chắc chắn là deploy hook của bạn có chạy hay không. Hãy test riêng phần đó: chạy script hook bằng tay và kiểm tra sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.