Sign inSign up

poynt2005/acme

By poynt2005

•Updated 26 days ago

Image
0

44

poynt2005/acme repository overview

⁠yourdomain.com 內網 HTTPS 憑證簽發器

這個專案使用 Docker、acme.sh⁠ 和 Cloudflare DNS-01,為以下名稱申請 Let's Encrypt 公開受信任憑證:

yourdomain.com
*.yourdomain.com

憑證可供家中 NAS、容器服務或反向代理使用。只要手機與電腦使用 AdGuard Home 作為 DNS,就能讓不同 hostname 指向不同內網 IP,同時在瀏覽器中正常使用 HTTPS。

⁠最終效果

例如在 AdGuard Home 建立以下 DNS Rewrite:

nas.yourdomain.com   -> 192.168.1.10
app.yourdomain.com   -> 192.168.1.20
cam.yourdomain.com   -> 192.168.1.30

使用者就可以透過以下網址連線:

https://nas.yourdomain.com
https://app.yourdomain.com
https://cam.yourdomain.com

這些服務即使位於不同 IP,仍可使用同一張 wildcard 憑證。憑證驗證的是 hostname,不是 IP。

直接使用 https://192.168.1.20 不會通過憑證名稱驗證,必須使用憑證涵蓋的網域名稱。

⁠運作原理

排程或手動執行 docker run
              |
              v
       acme.sh 簽發容器
              |
              | 使用 Cloudflare API 建立暫時的 TXT 記錄
              v
_acme-challenge.yourdomain.com
              |
              | Let's Encrypt 從公開 DNS 驗證網域控制權
              v
        簽發 wildcard 憑證
              |
              v
 /certs/cert.key + /certs/cert.cer
              |
              v
       nginx / Caddy / NPM
              |
              v
         內網應用程式

DNS-01 驗證只檢查 Cloudflare 上的 _acme-challenge TXT 記錄,不會連線到 NAS,也不要求服務具有公開 IP、開放 80/443 port 或進行路由器 port forwarding。

AdGuard Home 則負責內網的 Split DNS:內網裝置查詢 app.yourdomain.com 時,AdGuard Home 直接回答私人 IP,不需要從 Cloudflare 取得 A 記錄。

⁠Wildcard 的涵蓋範圍

*.yourdomain.com 可以涵蓋:

app.yourdomain.com
nas.yourdomain.com
anything.yourdomain.com

但不涵蓋:

x.app.yourdomain.com

本專案同時申請 yourdomain.com,所以根網域本身也在憑證涵蓋範圍內。

⁠專案檔案

Dockerfile       建立固定版本的執行環境
entrypoint.sh    驗證設定、申請憑證並輸出憑證檔案
.dockerignore    限制 Docker build context
README.md        使用說明

容器內的重要路徑:

路徑用途是否需要持久化
/usr/local/lib/acme.sh固定版本的 acme.sh 程式否,包含在 image 中
/opt/acme.shACME 帳號、私鑰、憑證狀態及 Cloudflare 設定是
/certs給反向代理使用的憑證輸出是
/tmp容器暫存空間否,使用 tmpfs

程式本體與狀態目錄刻意分開,因此將 volume 掛載到 /opt/acme.sh 不會覆蓋 image 中的 acme.sh 程式。

⁠前置條件

  • yourdomain.com 的權威 DNS 已交由 Cloudflare 管理。
  • NAS 或伺服器已安裝 Docker。
  • 用戶端的 DNS 指向 AdGuard Home。
  • 反向代理可以讀取產生的憑證。
  • NAS 時間及時區正確,且可連線至 Cloudflare、Let's Encrypt 和公開 DNS。

⁠1. 建立 Cloudflare API Token

登入 Cloudflare Dashboard:

  1. 前往 My Profile -> API Tokens。
  2. 選擇 Create Custom Token。
  3. 加入以下權限:
    • Zone -> DNS -> Edit
    • Zone -> Zone -> Read
  4. Zone Resources 選擇:
    • Include -> Specific zone -> yourdomain.com
  5. 建立 Token 並立即保存,Cloudflare 不會再次完整顯示該 Token。
  6. 從 Cloudflare Dashboard 首頁取得 Account ID。

不要使用 Global API Key,也不要將 Token 寫進 Dockerfile 或提交到 Git。

⁠2. 建立秘密設定檔

在 NAS 建立只有管理者可讀取的檔案,例如:

sudo mkdir -p /volume1/docker/acme-secrets
sudo vi /volume1/docker/acme-secrets/cf.env

內容如下:

CF_Account_ID=你的_Cloudflare_Account_ID
CF_Token=你的_Cloudflare_API_Token

限制檔案權限:

sudo chmod 600 /volume1/docker/acme-secrets/cf.env

acme.sh 的 Cloudflare DNS plugin 會把 API 資訊保存在 /opt/acme.sh/account.conf,所以 acme_home volume 也必須視為敏感資料,不應公開或任意複製。

⁠3. 準備憑證輸出目錄

容器使用 UID/GID 1000:1000 執行。建立輸出目錄並賦予寫入權限:

sudo mkdir -p /volume1/docker/certs
sudo chown 1000:1000 /volume1/docker/certs
sudo chmod 700 /volume1/docker/certs

如果 NAS 的儲存路徑不同,後續命令中的 /volume1/docker/certs 必須一起修改。

⁠4. 建立 Docker image

在包含 Dockerfile 和 entrypoint.sh 的目錄執行:

docker build -t home-acme:1.0 .

Dockerfile 目前固定使用:

  • Alpine Linux 3.20.10
  • acme.sh 3.1.4
  • acme.sh 原始碼 SHA-256 驗證
  • 非 root 使用者 acme,UID/GID 為 1000:1000

確認 image 已建立:

docker image inspect home-acme:1.0

⁠5. 先使用 Let's Encrypt Staging 測試

Staging 憑證不受瀏覽器信任,但不會快速消耗正式環境的簽發額度,適合先驗證 Cloudflare Token、DNS 和檔案權限。

建立獨立的測試輸出目錄:

sudo mkdir -p /volume1/docker/certs-staging
sudo chown 1000:1000 /volume1/docker/certs-staging
sudo chmod 700 /volume1/docker/certs-staging

執行:

docker run --rm \
  --cap-drop ALL \
  --read-only \
  --tmpfs /tmp \
  --env-file /volume1/docker/acme-secrets/cf.env \
  -e CERT_DOMAIN=yourdomain.com \
  -e ACME_SERVER=letsencrypt_test \
  -v acme_home_staging:/opt/acme.sh \
  -v /volume1/docker/certs-staging:/certs:rw \
  home-acme:1.0

測試與正式環境務必使用不同的 ACME volume 和輸出目錄。若共用狀態,測試憑證的續期時間可能讓後續正式簽發被判定為「尚未到期」而跳過。

成功後應看到:

/volume1/docker/certs-staging/cert.key
/volume1/docker/certs-staging/cert.cer

⁠6. 正式簽發

Staging 測試成功後執行:

docker run --rm \
  --cap-drop ALL \
  --read-only \
  --tmpfs /tmp \
  --env-file /volume1/docker/acme-secrets/cf.env \
  -e CERT_DOMAIN=yourdomain.com \
  -v acme_home:/opt/acme.sh \
  -v /volume1/docker/certs:/certs:rw \
  home-acme:1.0

參數說明:

參數作用
--rm執行完成後移除一次性容器
--cap-drop ALL移除所有 Linux capabilities
--read-only將 image 的 root filesystem 設為唯讀
--tmpfs /tmp提供不持久化的可寫入暫存空間
--env-file在執行時注入 Cloudflare 秘密
CERT_DOMAIN指定根網域,程式會同時加入 wildcard
acme_home:/opt/acme.sh保存帳號、金鑰及續期狀態
/certs:rw輸出私鑰及完整憑證鏈

產出檔案:

檔案內容
cert.key私鑰,權限固定為 0600
cert.cer伺服器憑證及中繼 CA 的 full chain

⁠7. 驗證憑證

不需要在 NAS 額外安裝 OpenSSL,可以使用 image 中的 OpenSSL:

docker run --rm \
  --entrypoint openssl \
  -v /volume1/docker/certs:/certs:ro \
  home-acme:1.0 \
  x509 -in /certs/cert.cer -noout -subject -issuer -dates -ext subjectAltName

輸出中的 Subject Alternative Name 應包含:

DNS:yourdomain.com
DNS:*.yourdomain.com

⁠8. 設定 AdGuard Home

進入 AdGuard Home 管理介面:

Filters -> DNS rewrites -> Add DNS rewrite
⁠方法 A:不同 hostname 指向不同 IP

適合每台主機自行提供 HTTPS:

DomainAnswer
nas.yourdomain.com192.168.1.10
app.yourdomain.com192.168.1.20
cam.yourdomain.com192.168.1.30

每一個 IP 上的 HTTPS server 都必須取得並載入相同的 cert.key 和 cert.cer。

⁠方法 B:全部指向一台反向代理(建議)

如果反向代理位於 192.168.1.10,可以加入:

*.yourdomain.com -> 192.168.1.10

反向代理再依照 hostname 將流量送到不同後端:

app.yourdomain.com -> http://192.168.1.20:8080
cam.yourdomain.com -> http://192.168.1.30:8080
nas.yourdomain.com -> http://192.168.1.10:5000

這種架構只需要在反向代理部署一份私鑰,安全性與維護性都較好。

⁠測試 DNS

假設 AdGuard Home IP 是 192.168.1.2:

nslookup app.yourdomain.com 192.168.1.2

結果應顯示設定的私人 IP。如果沒有:

  • 確認用戶端 DHCP DNS 指向 AdGuard Home。
  • 檢查手機 Private DNS、瀏覽器 DNS-over-HTTPS 或 VPN 是否繞過 AdGuard Home。
  • 清除裝置的 DNS cache 或重新連接 Wi-Fi。
  • 確認 AdGuard Home 的 DNS Rewrite 已啟用。

內網名稱不一定要在 Cloudflare 建立公開 A 記錄。DNS-01 只需要 Cloudflare 能建立驗證用 TXT 記錄,而內網 A 記錄可完全交由 AdGuard Home 回答。

⁠9. 設定反向代理

⁠nginx 範例

將憑證目錄唯讀掛載至 nginx 容器,例如 /certs:ro:

server {
    listen 443 ssl;
    server_name app.yourdomain.com;

    ssl_certificate     /certs/cert.cer;
    ssl_certificate_key /certs/cert.key;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://192.168.1.20:8080;
    }
}

憑證私鑰權限是 0600,因此 nginx master process 必須以 root 或 UID 1000 讀取;其他非 root 反向代理若使用不同 UID,會遇到 Permission denied。不要為了省事將私鑰改成全體可讀。

憑證更新後必須 reload 反向代理,否則它可能繼續使用記憶體中的舊憑證:

docker exec nginx nginx -s reload

容器名稱不是 nginx 時請替換成實際名稱。

⁠10. 自動續期

acme.sh 會保存憑證狀態。尚未進入續期窗口時會跳過簽發;本專案將 acme.sh 的「跳過」狀態碼 2 視為正常成功,因此可以安全地週期執行。

建議每 6 小時檢查一次,並只在簽發容器成功時 reload nginx:

17 */6 * * * /usr/bin/docker run --rm --cap-drop ALL --read-only --tmpfs /tmp --env-file /volume1/docker/acme-secrets/cf.env -e CERT_DOMAIN=yourdomain.com -v acme_home:/opt/acme.sh -v /volume1/docker/certs:/certs:rw home-acme:1.0 >>/volume1/docker/acme-renew.log 2>&1 && /usr/bin/docker exec nginx nginx -s reload

Synology 可在 Control Panel -> Task Scheduler 建立 root 使用者的週期工作,命令使用上面相同內容。

不要把輸出全部導向 /dev/null。保留日誌才能發現 Token 失效、DNS 驗證失敗或憑證接近到期等問題,並應搭配 log rotation 避免檔案無限成長。

⁠11. 多 IP 的憑證部署方式

Wildcard 憑證與 IP 數量無關。可採用兩種方式:

⁠集中式反向代理
所有 hostname -> 反向代理 IP -> 各內網服務 IP

只有反向代理需要持有私鑰,這是建議方式。

⁠每台主機自行終止 HTTPS
nas.yourdomain.com -> NAS IP
app.yourdomain.com -> App Server IP
cam.yourdomain.com -> Camera Server IP

每台 HTTPS server 都要安全地取得同一張憑證。若要同步私鑰,應使用受限制的 SSH、部署工具或唯讀共享,不應讓所有服務都能寫入 /certs。本專案只負責簽發及輸出,不包含跨主機私鑰同步。

⁠12. 環境變數

名稱必填預設值說明
CERT_DOMAIN是無根網域,例如 yourdomain.com;不可填入 *.
CF_Token是無Cloudflare API Token
CF_Account_ID是無Cloudflare Account ID
ACME_SERVER否letsencrypt測試時設為 letsencrypt_test
ACME_DNS_SLEEP否20建立 TXT 後等待 DNS 傳播的秒數

如果 DNS 傳播較慢,可嘗試:

-e ACME_DNS_SLEEP=60

⁠13. 常見問題

⁠/opt/acme.sh is not writable by UID 1000

持久化 volume 權限不正確。檢查 volume 或 bind mount 的 owner 是否允許 UID 1000 寫入。

⁠/certs is not writable by UID 1000

重新設定 NAS 輸出目錄:

sudo chown 1000:1000 /volume1/docker/certs
sudo chmod 700 /volume1/docker/certs
⁠Cloudflare 驗證失敗

確認:

  • Token 仍然有效。
  • Token 具有 Zone:DNS:Edit 和 Zone:Zone:Read。
  • Token 資源範圍包含 yourdomain.com。
  • CF_Token 與 CF_Account_ID 名稱及大小寫完全正確。
  • NAS 可連線到 api.cloudflare.com。
⁠DNS TXT 傳播逾時

將 ACME_DNS_SLEEP 從 20 增加至 60 或更高,並確認沒有其他 DNS provider 或舊 NS 設定。

⁠瀏覽器顯示不受信任
  • 確認目前不是使用 Staging 憑證。
  • 確認反向代理載入的是 /certs/cert.cer full chain。
  • reload 或重新啟動反向代理。
  • 確認網址是 something.yourdomain.com,不是直接使用 IP。
  • 查看實際服務送出的憑證,而不只檢查磁碟上的檔案。
⁠DNS 指到正確 IP,但網站無法開啟

DNS、TLS 和應用程式連線是三個不同層次。另行檢查:

  • 443 port 是否有服務監聽。
  • NAS firewall 是否允許內網連線。
  • 反向代理 upstream IP/port 是否正確。
  • 後端服務是否正常運行。
⁠第二次執行顯示 Skipping

這是正常行為,表示憑證尚未進入續期窗口。容器仍會將目前有效憑證安裝到 /certs,並以成功狀態結束。

⁠14. 安全注意事項

  • Cloudflare Token 只授權 yourdomain.com,不要授權帳號中的所有 zones。
  • cf.env、acme_home volume 和 cert.key 都是敏感資料。
  • 反向代理只應以唯讀方式掛載 /certs。
  • 不要把 Docker socket 掛載進簽發容器。
  • 不要將 80/443 port 對外開放,除非確實需要外網存取。
  • 不要將私鑰提交到 Git、雲端同步或未加密備份。
  • 定期更新 image 中的 Alpine 與 acme.sh,更新 acme.sh 時也要同步更新 SHA-256。
  • 定期備份 acme_home,但備份位置必須受到與私鑰相同等級的保護。

⁠參考資料

Tag summary

Content type

Image

Digest

sha256:318c3a9b2…

Size

10.2 MB

Last updated

26 days ago

docker pull poynt2005/acme