Security

Tự động cấp và gia hạn SSL wildcard với acme.sh

Bạn đang chạy một VPS với cả chục subdomain: `admin.tenmien.com`, `blog.tenmien.com`, `api.tenmien.com`... Mỗi lần chứng chỉ SSL hết hạn là lại phải SSH vào, chạy lệnh renew, rồi reload Nginx. Làm một lần thì được, làm hàng tháng thì phát mệt. Bài này sẽ hướng dẫn bạn dùng acme.sh kết hợp DNS API để tự động cấp và gia hạn SSL wildcard, tức là một chứng chỉ `*.tenmien.com` phủ cho tất cả subdomain, và nó tự gia hạn, không cần bạn đụng tay vào. Môi trường áp dụng: Ubuntu 24.04 LTS, Nginx, tài khoản DNS của Cloudflare hoặc Google Domains (các hãng khác có cách tương tự).

Yêu cầu trước khi bắt đầu

  • VPS chạy Ubuntu 24.04 LTS (hoặc Debian 12, AlmaLinux 9) với user có quyền sudo.
  • Tên miền đã trỏ DNS về VPS, và bạn có quyền quản trị DNS (tạo, xóa record).
  • Tài khoản API của nhà cung cấp DNS bạn dùng: Cloudflare, Google, VinaHost, hoặc bất kỳ hãng nào được acme.sh hỗ trợ (https://github.com/acmesh-official/acme.sh/wiki/dnsapi).
  • Nginx đã cài đặt và hoạt động (bài này dùng Nginx làm ví dụ, nhưng acme.sh độc lập với web server).

Vì sao nên dùng DNS API thay vì HTTP challenge?

acme.sh dùng giao thức ACME của Let's Encrypt để cấp chứng chỉ. Với wildcard, bạn bắt buộc phải dùng DNS-01 challenge, vì chỉ có cách này mới xác minh bạn sở hữu toàn bộ domain mà không cần đặt file lên từng subdomain. HTTP challenge (xác minh qua đường 80) không làm được với wildcard vì nó chỉ kiểm tra một domain cụ thể, không kiểm tra `*` được.

Khi dùng DNS API, acme.sh sẽ tự động tạo một record TXT tạm thời trên DNS của bạn, chờ Let's Encrypt xác minh, rồi xóa record đó đi. Toàn bộ quá trình mất khoảng 30 giây đến 2 phút. Điểm hay nhất: bạn có thể chạy việc này trên một con VPS không mở port 80, hoặc thậm chí không có web server, vì nó không cần đường HTTP.

Một lợi ích khác: khi bạn dùng DNS API, acme.sh có thể tự động gia hạn chứng chỉ mà không cần Nginx reload. Nó chỉ cần chạy lệnh renew và cập nhật file certificate. Bạn chỉ cần thêm một hook để reload Nginx sau khi renew thành công. Đây là điểm mấu chốt để hệ thống "tự vận hành", không cần bạn can thiệp.

Bước 1 - Cài đặt acme.sh

acme.sh là một script shell thuần, không cần thư viện phụ thuộc nào. Cách cài đặt chuẩn là dùng lệnh curl từ GitHub. Nó sẽ tự tạo user riêng (nếu chạy bằng root) và cài vào thư mục `~/.acme.sh`, kèm cron job để tự kiểm tra gia hạn.

curl https://get.acme.sh | sh -s [email protected]

Thay `[email protected]` bằng email thật của bạn, Let's Encrypt dùng nó để gửi cảnh báo khi chứng chỉ sắp hết hạn. Sau khi cài xong, kiểm tra:

~/.acme.sh/acme.sh --version

Output sẽ hiện phiên bản hiện tại (ví dụ `acme.sh v3.0.8`). Nếu bạn đã có acme.sh cài trước đó, hãy cập nhật lên bản mới nhất để chắc chắn dùng đúng API:

acme.sh --upgrade

Cron job được tạo tự động, kiểm tra bằng:

crontab -l

Bạn sẽ thấy một dòng tương tự: `17 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null`. Đây là bộ não của hệ thống: mỗi ngày nó kiểm tra một lần, nếu chứng chỉ sắp hết hạn (dưới 30 ngày) thì tự động gia hạn.

Bước 2 - Lấy API token và cấu hình DNS provider

Thao tác này chỗ nào cũng phải cẩn thận vì token DNS API có quyền thay đổi record, nếu lộ ra ngoài là kẻ xấu chiếm luôn domain của bạn. Nguyên tắc: tạo token với scope tối thiểu, chỉ cho phép sửa DNS zone của đúng domain bạn cần.

Ví dụ với Cloudflare

Vào Cloudflare Dashboard, vào My Profile → API Tokens → Create Token. Chọn template Edit zone DNS, trong mục Zone Resources chọn Include → Specific zone → tenmien.com. Token này chỉ có quyền sửa DNS của đúng domain đó, không đụng được gì khác. Lưu token lại, rồi export biến môi trường:

export CF_Token="your_cloudflare_api_token"
export CF_Zone_ID="your_zone_id"

`CF_Zone_ID` bạn lấy ở trang tổng quan zone của domain, cuộn xuống bên phải sẽ thấy Zone ID. acme.sh sẽ lưu các biến này vào file config riêng của từng domain, không cần export lại mỗi lần chạy.

Ví dụ với Google Domains (hiện là Squarespace Domains)

Với Google Domains (đã chuyển sang Squarespace), bạn vào phần DNS, tạo một record TXT tạm, hoặc dùng API của Squarespace. acme.sh hỗ trợ nhiều provider, bạn kiểm tra danh sách đầy đủ tại trang wiki của dự án. Nếu dùng dịch vụ DNS của nhà cung cấp VPS trong nước, thường họ cũng có API hoặc panel hỗ trợ, bạn cần tìm trong danh sách `dnsapi` của acme.sh xem có tên nhà cung cấp đó không.

Nếu provider của bạn chưa được hỗ trợ, bạn vẫn có thể dùng cách thủ công: acme.sh cho phép bạn tự tạo record TXT, rồi báo cho nó tiếp tục (`--dns` mà không có tên provider). Nhưng hướng dẫn này tập trung vào DNS API để mọi thứ tự động.

Bước 3 - Cấp chứng chỉ wildcard đầu tiên

Sau khi đã export token, bạn chạy lệnh cấp chứng chỉ. Lưu ý: cú pháp lệnh của acme.sh trong 2026 vẫn giữ nguyên như các bản trước, nhưng bạn nên kiểm tra phiên bản của mình bằng `--version` trước khi chạy.

~/.acme.sh/acme.sh --issue --dns dns_cf -d '*.tenmien.com' -d 'tenmien.com' --server letsencrypt

Giải thích các tham số:

  • --dns dns_cf: dùng DNS API của Cloudflare. Với provider khác, thay bằng dns_google, dns_he, dns_vc... (xem wiki của acme.sh).
  • -d '*.tenmien.com': chứng chỉ wildcard cho tất cả subdomain.
  • -d 'tenmien.com': cert cũng bao gồm domain gốc, không thì truy cập `tenmien.com` sẽ báo lỗi SSL.
  • --server letsencrypt: dùng Let's Encrypt (mặc định). Có thể bỏ qua, nhưng để rõ ràng thì nên ghi.

Lần đầu chạy, acme.sh sẽ tạo record TXT tạm trên Cloudflare, chờ xác minh, rồi tải chứng chỉ về. Output sẽ có dòng `Your cert is in: /root/.acme.sh/*.tenmien.com_ecc/`. Thư mục chứa cert được đặt tên theo domain bạn khai báo.

Xác minh chứng chỉ đã được cấp:

~/.acme.sh/acme.sh --list

Bạn sẽ thấy domain `*.tenmien.com` xuất hiện trong danh sách, kèm thời gian hết hạn. Nếu lệnh báo lỗi liên quan đến DNS propagation, bạn kiểm tra lại token, hoặc chờ vài phút rồi chạy lại, vì record TXT đôi khi cần thời gian để lan truyền.

Bước 4 - Cài chứng chỉ vào Nginx và cấu hình tự động gia hạn

Chứng chỉ trong thư mục `~/.acme.sh` chỉ là bản làm việc. Bạn cần copy sang vị trí chuẩn mà Nginx đọc, và cấu hình để mỗi lần renew xong, acme.sh tự động copy lại + reload Nginx. acme.sh có cơ chế `--install-cert` để làm việc này.

~/.acme.sh/acme.sh --install-cert -d '*.tenmien.com' \
--key-file /etc/nginx/ssl/tenmien.com.key \
--fullchain-file /etc/nginx/ssl/tenmien.com.crt \
--reloadcmd "systemctl reload nginx"

Trước khi chạy, tạo thư mục `/etc/nginx/ssl`:

sudo mkdir -p /etc/nginx/ssl

Lệnh này sẽ copy private key và fullchain (bao gồm cả chuỗi trung gian) vào thư mục `/etc/nginx/ssl`, và ghi lại lệnh `systemctl reload nginx` vào config của acme.sh. Mỗi lần tự động gia hạn thành công, acme.sh sẽ chạy lại lệnh copy và reload này. Hệ thống tự vận hành từ đây.

Cấu hình server block trong Nginx trỏ tới các file này:

server {
    listen 443 ssl http2;
    server_name tenmien.com *.tenmien.com;

    ssl_certificate /etc/nginx/ssl/tenmien.com.crt;
    ssl_certificate_key /etc/nginx/ssl/tenmien.com.key;

    # Phần còn lại của cấu hình...
}

Bạn nên thêm một server block bắt buộc chuyển hướng HTTP sang HTTPS:

server {
    listen 80;
    server_name tenmien.com *.tenmien.com;
    return 301 https://$host$request_uri;
}

Sau khi sửa config, kiểm tra và reload:

sudo nginx -t
sudo systemctl reload nginx

Output `nginx -t` phải báo `syntax is ok` và `test is successful`. Nếu có lỗi, mở file config ra kiểm tra lại đường dẫn file cert, rất có thể bạn copy sai vị trí hoặc thiếu thư mục.

Bước 5 - Kiểm tra chứng chỉ và quá trình gia hạn

Sau khi cấu hình xong, bạn kiểm tra chứng chỉ đã hoạt động đúng chưa. Từ VPS, dùng lệnh sau để xem chi tiết cert:

echo | openssl s_client -servername tenmien.com -connect tenmien.com:443 2>/dev/null | openssl x509 -noout -dates -subject

Bạn sẽ thấy `notBefore` và `notAfter` của chứng chỉ. Xác minh wildcard hoạt động bằng cách truy cập thử một subdomain bất kỳ, ví dụ:

curl -I https://admin.tenmien.com

Nếu trả về `HTTP/2 200` (hoặc code tương ứng với ứng dụng của bạn) nghĩa là SSL wildcard đã phủ đúng. Bạn cũng có thể kiểm tra từ trình duyệt, vào `admin.tenmien.com`, xem icon ổ khóa có hiện đầy đủ không.

Để mô phỏng quá trình gia hạn mà không cần đợi hết hạn, bạn dùng lệnh:

~/.acme.sh/acme.sh --renew -d '*.tenmien.com' --force

Lệnh này sẽ ép acme.sh xin cấp lại chứng chỉ mới, dù chưa tới hạn. Quá trình này chạy đúng quy trình như khi cron tự động gia hạn: tạo record TXT, chờ verify, tải cert, copy vào `/etc/nginx/ssl`, và reload Nginx. Nếu lệnh này chạy thành công, bạn có thể yên tâm hệ thống sẽ tự động gia hạn trong tương lai.

Xử lý lỗi thường gặp

Nguyên tắc đầu tiên khi gặp lỗi với acme.sh: chạy lại lệnh với flag `--debug` để xem log chi tiết. Đa số lỗi đều có thể đọc được nguyên nhân từ output.

Lỗi 1: "Error creating new order" hoặc "Rate limit exceeded"

Let's Encrypt giới hạn số lần cấp chứng chỉ cho mỗi domain (5 lần mỗi tuần cho mỗi domain, 50 lần mỗi tuần cho mỗi tài khoản). Nếu bạn thử cấp đi cấp lại nhiều lần, sẽ bị chặn tạm thời. Cách xử lý: chờ hết tuần, hoặc kiểm tra xem bạn có đang request trùng lặp không bằng lệnh:

~/.acme.sh/acme.sh --list

Nếu domain đã có cert trong danh sách, không cần cấp lại, chỉ cần renew khi gần hết hạn.

Lỗi 2: "Can not find domain in the DNS" hoặc TXT record không xuất hiện

Token API sai hoặc thiếu quyền. Xác minh lại:

curl -X GET "https://api.cloudflare.com/client/v4/zones/$(echo $CF_Zone_ID)" -H "Authorization: Bearer $CF_Token"

Nếu trả về lỗi `Invalid access token` hoặc `Unauthorized`, token sai hoặc chưa được kích hoạt. Nếu trả về `zone not found`, bạn nhập sai `CF_Zone_ID`. Một khả năng khác: DNS propagation chậm, record TXT vừa tạo chưa xuất hiện trên toàn hệ thống. Chờ 1-2 phút, chạy lại lệnh issue với flag `--dnssleep` (ví dụ `--dnssleep 120`).

Lỗi 3: Nginx không reload sau khi gia hạn

Khi bạn chạy lệnh `--install-cert`, acme.sh ghi lại lệnh reload trong file config của domain. Nếu lệnh `systemctl reload nginx` bị lỗi, thường do Nginx config sai hoặc thiếu file cert. Kiểm tra bằng:

sudo nginx -t
sudo journalctl -u nginx --since "5 minutes ago"

Sửa lỗi config, rồi chạy lại lệnh install-cert một lần nữa để nó ghi đè reloadcmd, hoặc tự chạy `systemctl reload nginx` và xem log.

Nếu cron chạy nhưng Nginx không reload, mở file config của domain trong `~/.acme.sh/` và kiểm tra dòng `Le_ReloadCmd=`. Thường dòng này bị mất sau khi bạn cài lại acme.sh hoặc di chuyển thư mục. Chạy lại lệnh install-cert sẽ ghi lại đúng.

Câu hỏi thường gặp

acme.sh có hỗ trợ gia hạn chứng chỉ từ các CA khác ngoài Let's Encrypt không?

Chứng chỉ wildcard có bảo vệ được domain cấp 2 như sub.sub.tenmien.com không?

Không. Chứng chỉ wildcard `*.tenmien.com` chỉ bảo vệ subdomain cấp 1, tức `admin.tenmien.com`, `blog.tenmien.com`, không bảo vệ `sub.admin.tenmien.com`. Muốn phủ sâu hơn, bạn phải cấp thêm chứng chỉ cho `*.admin.tenmien.com` hoặc dùng chứng chỉ SAN liệt kê từng domain cụ thể.

Nếu tôi đổi nhà cung cấp DNS thì sao?

Bạn cần xóa domain khỏi acme.sh (`acme.sh --remove -d '*.tenmien.com'`), export token mới của provider mới, rồi chạy lại lệnh issue. Quá trình cấp và gia hạn sẽ tự chạy lại từ đầu. Lưu ý xóa cả record TXT tạm nếu có, và đảm bảo DNS đã trỏ đúng trước khi xin cấp chứng chỉ mới.

acme.sh có tốn RAM hoặc CPU trên VPS không?

Không. acme.sh là script shell, chỉ chạy khi cron kích hoạt, mỗi lần vài giây. Nó không có daemon chạy nền, nên không đáng kể. Nếu bạn quản lý nhiều domain, tần suất chạy tăng lên nhưng vẫn rất nhẹ, phù hợp cả với VPS Linux cấu hình thấp. Bạn chỉ cần mở đúng port 443 và 80 cho Nginx, không cần mở thêm gì cho acme.sh.

Làm sao để kiểm tra chứng chỉ còn bao nhiêu ngày hết hạn?

Dùng lệnh:

~/.acme.sh/acme.sh --list

Hoặc kiểm tra trực tiếp từ file cert:

openssl x509 -enddate -noout -in /etc/nginx/ssl/tenmien.com.crt

Output sẽ hiện `notAfter=...`. Bạn cũng có thể đặt một cron dùng lệnh này để gửi cảnh báo qua Telegram nếu cert sắp hết hạn, như đã làm với các bài giám sát hệ thống khác.

Bài viết liên quan

Lưu ý: Bài viết mang tính tham khảo, tổng hợp kiến thức chung. Mỗi hệ thống, hạ tầng và nhu cầu có đặc thù riêng, nên kiểm thử trong môi trường an toàn và tham vấn kỹ sư trước khi triển khai thực tế.