Lỗi gặp phải
Chạy certbot renew và nhận thông báo lỗi:
open() "/var/www/html/.well-known/acme-challenge/xxxxxxxxxxxxxxxx" failed (2: No such file or directory)
Let's Encrypt đặt một file token vào URL đã biết, rồi tải nó qua HTTP cổng 80 để xác nhận bạn kiểm soát domain. Nginx trả về 404. Xác thực thất bại. Chứng chỉ không được gia hạn.
Nguyên nhân
Ba nguyên nhân chính, sắp xếp theo mức độ phổ biến:
- Redirect HTTP→HTTPS chặn request challenge — ACME server luôn truy cập cổng 80. Nếu Nginx redirect toàn bộ traffic cổng 80 sang HTTPS trước, lệnh 302 được kích hoạt trước khi file challenge được kiểm tra. Đây là nguyên nhân phổ biến nhất.
- Sai đường dẫn webroot — Certbot ghi token vào một thư mục, nhưng chỉ thị
rootcủa Nginx lại trỏ đến nơi khác. Token tồn tại trên đĩa nhưng Nginx không thể tìm thấy. - Thiếu thư mục — Thư mục
.well-known/acme-challenge/hoàn toàn không tồn tại trên đĩa.
Cách sửa nhanh
Bước 1: Kiểm tra webroot mà Certbot đang trỏ đến
cat /etc/letsencrypt/renewal/yourdomain.com.conf
Tìm dòng webroot_path. Nó phải khớp với thư mục mà Nginx thực sự phục vụ file. Không khớp? Hãy sửa đường dẫn trong file này hoặc cập nhật chỉ thị root của Nginx.
Bước 2: Tạo thư mục challenge nếu chưa tồn tại
mkdir -p /var/www/html/.well-known/acme-challenge
chmod 755 /var/www/html/.well-known/acme-challenge
Bước 3: Thêm location block rõ ràng trước mọi redirect
Mở server block Nginx cho cổng 80. Thêm location challenge phía trên dòng return 301 hoặc rewrite bất kỳ:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# ACME challenge — phải đặt trước redirect HTTPS
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
try_files $uri =404;
}
# Mọi request còn lại chuyển sang HTTPS
return 301 https://$host$request_uri;
}
Modifier ^~ yêu cầu Nginx dừng xử lý regex khi block này khớp. Redirect sẽ không bao giờ kích hoạt cho các URL challenge.
Bước 4: Kiểm tra config và reload
nginx -t && systemctl reload nginx
Bước 5: Thử lại với dry run
certbot renew --dry-run
Khắc phục vĩnh viễn: Snippet dùng chung cho mọi domain
Quản lý nhiều domain? Tạo một snippet có thể tái sử dụng để không server block nào bỏ sót phần này:
# Tạo file: /etc/nginx/snippets/acme-challenge.conf
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
try_files $uri =404;
}
Sau đó include vào mọi server block HTTP:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
include snippets/acme-challenge.conf;
return 301 https://$host$request_uri;
}
Chỉ một dòng include. Mọi domain mới tự động kế thừa config challenge đúng.
Trường hợp đặc biệt: Nhiều domain với webroot khác nhau
Gia hạn chứng chỉ bao gồm các domain nằm trong thư mục riêng biệt? Kiểm tra phần [[webroot_map]] trong config gia hạn:
# /etc/letsencrypt/renewal/yourdomain.com.conf
[renewalparams]
authenticator = webroot
webroot_path = /var/www/html,
[[webroot_map]]
yourdomain.com = /var/www/html
www.yourdomain.com = /var/www/html
api.yourdomain.com = /var/www/api
Mỗi domain ánh xạ đến thư mục mà Nginx phục vụ nó. Một đường dẫn sai sẽ làm thất bại toàn bộ quá trình gia hạn — không chỉ riêng domain đó.
Kiểm tra
Đừng đoán mò. Tạo file test và tải nó qua HTTP trực tiếp:
# Tạo file token thử nghiệm
echo "ok" > /var/www/html/.well-known/acme-challenge/healthcheck
# Tải qua HTTP — phải trả về 200, không phải redirect
curl -v http://yourdomain.com/.well-known/acme-challenge/healthcheck
Bạn muốn thấy HTTP/1.1 200 OK với body là ok. Vẫn nhận 404 hoặc redirect? Chạy các lệnh sau:
# Kiểm tra location block trong config đang hoạt động
nginx -T | grep -A5 'well-known'
# Xem đường dẫn root mà cổng 80 đang dùng
nginx -T | grep root
Khi curl trả về 200, chạy dry run:
certbot renew --dry-run
Dry run thành công nghĩa là lịch gia hạn tự động từ certbot.timer cũng sẽ hoạt động. Dọn dẹp sau đó:
rm /var/www/html/.well-known/acme-challenge/healthcheck

