Trong mô hình nhiều website trên Cloud VPS, mọi site đều chạy HTTPS và chứng chỉ nằm trên tầng balancer. Với hàng trăm domain, mỗi chứng chỉ Let's Encrypt chỉ sống 90 ngày, gia hạn bằng tay là không thể. Công cụ phù hợp là Certbot, client ACME chính thức do EFF phát triển. Tài liệu này giải thích HTTP-01 hoạt động thế nào, chọn chế độ Certbot nào cho balancer có file cấu hình theo khuôn, cách gia hạn tự động và reload nginx đúng lúc, theo dõi chứng chỉ sắp hết hạn, các lỗi hay gặp và checklist khi thêm domain.

1. Let's Encrypt và HTTP-01 hoạt động thế nào

Let's Encrypt cấp chứng chỉ miễn phí qua giao thức ACME. Trước khi cấp, CA phải được chứng minh rằng người xin thực sự kiểm soát domain. Với kiểu xác thực HTTP-01, luồng diễn ra như sau:

  1. Certbot trên balancer tạo đơn xin chứng chỉ cho example.com.
  2. Let's Encrypt trả về một token ngẫu nhiên.
  3. Certbot làm cho URL http://example.com/.well-known/acme-challenge/<token> trả về đúng nội dung CA yêu cầu.
  4. Máy chủ của Let's Encrypt, từ nhiều điểm trên Internet, phân giải DNS của example.com và gọi URL đó qua cổng 80.
  5. Nội dung khớp thì CA ký chứng chỉ, Certbot lưu vào /etc/letsencrypt/live/example.com/.

Từ đó suy ra ba điều kiện bắt buộc: bản ghi DNS của domain phải trỏ về đúng balancer đang chạy Certbot, cổng 80 phải mở ra Internet, và request tới /.well-known/acme-challenge/ không được bị chặn hay chuyển hướng sang nơi khác. Khi không đáp ứng được, ví dụ server không nhận được kết nối từ CA, có thể dùng kiểu xác thực DNS-01 qua bản ghi TXT, sẽ được trình bày ở một bài riêng.

2. Chọn chế độ Certbot: --nginx, certonly hay webroot

LệnhXác thựcSửa file cấu hình nginx?Hợp khi
certbot --nginxPlugin nginx tự chèn location tạmCó: tự thêm dòng SSL, có thể thêm redirectMáy ít site, cấu hình viết tay
certbot certonly --nginxPlugin nginx tự chèn location tạmKhôngCấu hình sinh từ khuôn, muốn file luôn đúng khuôn
certbot certonly --webroot -w <dir>Ghi file token vào thư mụcKhông, nhưng mỗi site phải có sẵn location cho challengeKhông muốn Certbot đụng vào nginx

Trên balancer, file cấu hình mỗi site nên đi theo một khuôn cố định như trong bài tầng Balancer với Nginx reverse proxy. Để Certbot không tự sửa các file này, nên chọn certonly --nginx: plugin nginx lo phần xác thực (tạm chèn cấu hình phục vụ token rồi gỡ đi), còn đường dẫn chứng chỉ trong file cấu hình do người vận hành hoặc template tự viết.

certbot certonly --nginx \
    -d example.com -d www.example.com \
    --non-interactive --agree-tos -m <email-van-hanh>

Thứ tự thao tác khi thêm một site mới vì thế quan trọng. Nếu file cấu hình có sẵn khối listen 443 trỏ tới /etc/letsencrypt/live/example.com/fullchain.pem trong khi chứng chỉ chưa tồn tại, nginx -t sẽ báo lỗi và mọi lần reload sau đó đều thất bại. Quy trình đúng:

  1. Tạo file cấu hình chỉ có khối listen 80 cho domain, nginx -t && systemctl reload nginx.
  2. Chạy Certbot cấp chứng chỉ.
  3. Thêm khối listen 443 và redirect HTTP sang HTTPS, kiểm tra và reload lần nữa.

Với -m (email vận hành), Let's Encrypt có thể gửi thông báo liên quan tới tài khoản. Không nên coi email là lớp cảnh báo hết hạn chính; việc theo dõi nên do hệ thống của mình đảm nhận (xem mục 4).

3. Gia hạn tự động với certbot.timer

Gói Certbot trên Debian cài sẵn một systemd timer chạy certbot renew hai lần mỗi ngày, có độ trễ ngẫu nhiên để không dồn tải về CA cùng một thời điểm. Lệnh renew chỉ gia hạn những chứng chỉ còn dưới 30 ngày, nên chạy thường xuyên cũng không tốn gì.

# timer đang bật và lần chạy kế tiếp
systemctl list-timers | grep certbot

# thử toàn bộ quy trình gia hạn trên môi trường staging, không cấp thật
certbot renew --dry-run

# danh sách chứng chỉ, domain và ngày hết hạn
certbot certificates

Mỗi chứng chỉ có một file /etc/letsencrypt/renewal/<tên>.conf ghi lại cách nó được cấp (plugin nào, domain nào). Lần gia hạn dùng lại đúng cách đó, nên chứng chỉ cấp bằng certonly --nginx thì cũng gia hạn bằng plugin nginx.

Deploy hook: reload nginx sau khi gia hạn

Nginx đọc chứng chỉ vào bộ nhớ lúc khởi động hoặc reload. Certbot ghi file mới xuống đĩa, nhưng nếu nginx không được reload, nó vẫn phục vụ chứng chỉ cũ cho tới khi hết hạn. Cách chắc chắn nhất là một deploy hook, chạy mỗi khi có chứng chỉ thực sự được gia hạn:

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
nginx -t && systemctl reload nginx

chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Script trong thư mục renewal-hooks/deploy/ áp dụng cho mọi chứng chỉ trên máy, không phải khai báo lại cho từng domain. Dòng nginx -t đứng trước để một file cấu hình hỏng không biến lần gia hạn thành một lần sập nginx. Tuỳ chọn và thứ tự chạy các loại hook có trong tài liệu sử dụng Certbot.

4. Theo dõi chứng chỉ sắp hết hạn

Timer và hook lo được phần lớn trường hợp, nhưng không nên tin tuyệt đối vào một cơ chế. Nên có một lớp kiểm tra độc lập, nhìn từ phía người dùng, chạy định kỳ:

#!/bin/bash
# liệt kê domain có SSL Let's Encrypt trên balancer, cảnh báo chứng chỉ còn dưới 14 ngày
for f in /etc/nginx/conf.d/*.conf; do
    grep -q 'letsencrypt' "$f" || continue
    d=$(basename "$f" .conf)
    end=$(echo | openssl s_client -servername "$d" -connect 127.0.0.1:443 2>/dev/null \
          | openssl x509 -noout -enddate | cut -d= -f2)
    left=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
    [ "$left" -lt 14 ] && echo "CANH BAO $d: con $left ngay ($end)"
done

Script này hỏi chính nginx đang phục vụ chứng chỉ nào (qua openssl s_client với SNI), chứ không đọc file trên đĩa. Nhờ vậy nó bắt được cả trường hợp file đã được gia hạn nhưng nginx chưa reload. Domain nào xuất hiện trong danh sách cảnh báo, chạy certbot renew --cert-name <domain> để xem lỗi cụ thể.

Một nguồn cảnh báo giả rất thường gặp: domain đã ngừng dùng hoặc đã trỏ sang nơi khác nhưng file cấu hình và chứng chỉ vẫn còn trên balancer, Certbot cố gia hạn và thất bại mãi. Khi gỡ một site, hãy gỡ luôn chứng chỉ bằng certbot delete --cert-name example.com.

5. Giới hạn của Let's Encrypt cần biết

Let's Encrypt áp nhiều giới hạn tần suất; con số cụ thể có thể thay đổi nên luôn đối chiếu trang chính thức. Ba nhóm giới hạn cần để ý nhất khi làm với nhiều domain:

  • Xác thực thất bại: chỉ được vài lần mỗi giờ cho một tài khoản và một hostname. Chạy lại Certbot liên tục khi DNS chưa trỏ đúng sẽ chạm giới hạn này rất nhanh.
  • Chứng chỉ trùng lặp: số chứng chỉ mỗi tuần cho cùng một tập domain bị giới hạn ở mức thấp.
  • Chứng chỉ theo domain gốc: số chứng chỉ mỗi tuần cho một registered domain có trần. Đáng chú ý khi tạo hàng loạt subdomain demo dưới cùng một domain.

Khi thử nghiệm quy trình mới, dùng --dry-run hoặc --staging. Môi trường staging có giới hạn rộng hơn nhiều và không tính vào hạn mức của môi trường thật.

6. Lỗi thường gặp khi cấp SSL bằng Certbot

Domain chưa trỏ về balancer

Thông báo thường gặp là Invalid response from http://example.com/.well-known/acme-challenge/...: 404 hoặc Timeout during connect (likely firewall problem). CA đã gọi tới một máy không phải balancer chạy Certbot, hoặc không kết nối được cổng 80. Luôn kiểm tra trước khi chạy:

dig +short example.com A
dig +short www.example.com A
curl -sI http://example.com/.well-known/acme-challenge/test

Cả domain chính lẫn www đều phải trỏ đúng, vì chỉ một tên trong -d sai là cả đơn thất bại. Nếu chủ domain vừa đổi DNS, đợi bản ghi cũ hết TTL rồi mới chạy, thay vì thử liên tục và chạm giới hạn xác thực thất bại.

Domain đi qua Cloudflare proxy

Khi bản ghi bật proxy (đám mây cam), dig trả về IP của Cloudflare chứ không phải IP balancer, nên đừng dựa vào kết quả đó để kết luận DNS sai. Request xác thực đi qua Cloudflare rồi mới tới balancer. Phần lớn trường hợp vẫn chạy được, nhưng các thiết lập như ép HTTPS, luật tường lửa hay chế độ chống bot có thể chặn hoặc chuyển hướng request tới /.well-known/acme-challenge/. Cách chắc chắn nhất cho lần cấp đầu tiên là tạm chuyển bản ghi sang DNS only, cấp xong mới bật proxy lại.

Chứng chỉ trùng: example.com-0001

Chạy Certbot cho một domain đã có chứng chỉ nhưng với danh sách -d khác đi (thêm hoặc bớt www) có thể tạo chứng chỉ mới tên example.com-0001. File cấu hình nginx vẫn trỏ thư mục cũ, cả hai chứng chỉ cùng được gia hạn, và người vận hành không biết bản nào đang được dùng. Cách làm đúng:

# mở rộng chứng chỉ hiện có thay vì tạo bản mới
certbot certonly --nginx --cert-name example.com --expand \
    -d example.com -d www.example.com -d shop.example.com

# xoá bản trùng sau khi chắc chắn không còn file cấu hình nào trỏ tới nó
grep -r 'example.com-0001' /etc/nginx/conf.d/ || certbot delete --cert-name example.com-0001

7. Checklist Certbot khi thêm và gỡ domain

  • DNS của domain và www đã trỏ đúng balancer (hoặc tạm để DNS only nếu dùng Cloudflare).
  • Cổng 80 mở, /.well-known/acme-challenge/ không bị chặn hay redirect.
  • File cấu hình chỉ có khối port 80 trước khi cấp; thêm khối 443 sau khi có chứng chỉ.
  • Cấp bằng certonly --nginx; mở rộng bằng --cert-name kèm --expand.
  • certbot.timer đang bật, deploy hook có quyền chạy, certbot renew --dry-run thành công.
  • Script kiểm tra hạn chứng chỉ qua SNI chạy định kỳ và có người nhận cảnh báo.
  • Gỡ site thì certbot delete chứng chỉ tương ứng.

Đặt Certbot trên từng balancer giữ mọi thứ đơn giản: chứng chỉ nằm ngay nơi dùng, không cần đồng bộ giữa các máy. Cái giá là chứng chỉ gắn với balancer: chuyển site sang balancer khác thì phải cấp lại trên máy mới sau khi DNS đã trỏ sang, và theo dõi phải làm trên từng máy. Các bài trước của series có tại chuyên mục blog kỹ thuật của Sudo.

Bài tiếp theo

Deploy tự động đầu tiên: GitLab Runner, SSH và nginx template. Từ thêm site bằng tay sang một pipeline tạo database, kéo code, sinh file cấu hình nginx từ khuôn và cấp SSL trong một lần chạy.