Homelab Domain Certificate Automation: acme.sh + Tencent Cloud/Qiniu CDN Deployment Pipeline

This article was last updated on: November 9, 2026 pm

Background

My DNS runs on Tencent Cloud DNSPod.

My blog actually uses 3-4 domains, spread across multiple clouds:

  • ewhisper.cn — the blog site (deployed on Tencent Cloud CDN)
  • www.ewhisper.cn — also points to the blog site (deployed on Tencent Cloud CDN)
  • pic-cdn.ewhisper.cn — the image CDN domain for the blog (deployed on Qiniu Cloud CDN)
  • pic.ewhisper.cn — the image origin domain for the blog (deployed on Qiniu Cloud CDN, apparently unused)

📝Note:

All for historical reasons. Convenience or cost, or whatever it was at the time, produced the architecture I have today.
I’ve been planning to refactor it forever, but never had the time 🤣

When it comes to renewing certificates for these domains, one manual round was enough to make me never want a second.

One certificate has to be uploaded to two CDNs, each CDN takes at least two operations, and after clicking through the console you still have to wait for it to take effect. Four domains per round means sixteen repetitions.

I really didn’t want to click through it manually anymore, so I turned it into a pipeline: acme.sh issues the certificate, the --reloadcmd hook deploys it automatically, and cron checks every 6 hours.

📝Notes:
This article covers a homelab + personal blog scenario. The domains and cloud resources are all under a personal account, which is not the same as multi-tenancy, multi-cluster, and multi-approval in a company’s production environment.

Overall Pipeline

Split by issue, deploy, and verify — one pipeline manages a single multi-SAN certificate covering 4 domains.

1
2
3
4
5
6
7
cron(every 6h)
└─ acme.sh --cron (includes ARI check)
└─ Let's Encrypt DNS-01 via dns_tencent
└─ renewal success → --reloadcmd
├─ tccli ssl UploadCertificate → DeployCertificateInstance (Tencent CDN)
├─ qiniu-deploy.py → /sslcert + /domain/<name>/httpsconf (Qiniu CDN)
└─ log → ~/.acme.sh/deploy.log

Step 1: Reuse tccli Credentials for DNS-01

acme.sh’s dns_tencent plugin reads environment variables by default, but tccli is already installed on this machine, and ~/.tccli/default.credential already holds a ready-made SecretId/SecretKey.

Reusing them directly saves me from applying for a separate API Token from DNSPod.

1
2
3
4
5
export Tencent_SecretId="..."
export Tencent_SecretKey="..."
acme.sh --issue --dns dns_tencent \
-d a.ewhisper.cn -d b.ewhisper.cn -d c.ewhisper.cn -d pic.ewhisper.cn \
--keylength 2048

After the first successful issuance, the SecretId/SecretKey used by the dns_tencent plugin land in ~/.acme.sh/account.conf with permissions 600. This is the DNS API credential; the TLS private key lives in the certificate’s own directory.

Step 2: One Multi-SAN Certificate or Four Separate Certificates

Four domains, two options:

Dimension 4 separate certificates 1 multi-SAN certificate
Quota consumed per renewal 4 times 1 time
Expiry time Possibly staggered Naturally unified
acme.sh --list 4 rows 1 row
Deployment script input Loop 4 times 1 cert path

Considering the time cost of network IO and the complexity of certificate management, I ultimately chose to issue a single multi-SAN certificate. For the CA I picked Let’s Encrypt rather than ZeroSSL, for a very simple reason: LE doesn’t require email registration, ZeroSSL does.

📝Notes:
The cost of multi-SAN is “one fails, all fail”: if DNS-01 validation fails for any single domain, the whole certificate can’t be renewed. On a personal blog these domains share the same DNSPod DNS setup, so the risk is acceptable.

Step 3: Tencent CDN Deployment, the Key Is Alias

--reloadcmd calls a self-written script; the logic is upload first, then deploy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env bash
set -euo pipefail
CERT_DIR="$1"
ALIAS="ewhisper.cn-acme"

CERT_ID=$(tccli ssl UploadCertificate \
--CertificatePublicKey "$(cat "$CERT_DIR/fullchain.cer")" \
--CertificatePrivateKey "$(cat "$CERT_DIR/ewhisper.cn.key")" \
--Alias "$ALIAS" \
--Repeatable true \
--output json | jq -r '.CertificateId')

tccli ssl DeployCertificateInstance \
--CertificateId "$CERT_ID" \
--ResourceType cdn \
--InstanceIdList "a.ewhisper.cn|on" "b.ewhisper.cn|on"

--Repeatable true does not deduplicate. Upload the same PEM over and over and each call returns a new CertificateId, while RepeatCertId in the response is always empty.

“Repeatable upload” only permits repetition; it does not compare content.

The countermeasure is to fix the Alias (ewhisper.cn-acme) and periodically batch-clean old certificates by Alias, rather than automatically deleting old certificates inside the renewal hook. If the latter deletes the wrong one, the certificate currently in use on the CDN is gone.

The InstanceIdList format is “domain|billing switch”. off means turning off the CDN’s HTTPS feature, so you have to write on. Writing a bare domain directly reports a parameter error.

Step 4: Qiniu’s Dual-Domain, Dual-Authentication Setup

Qiniu is a bit more troublesome: certificate management and domain HTTPS configuration belong to two different Hosts and two different authentication schemes.

Purpose Host Auth method Signing key points
Certificate upload /sslcert fusion.qiniuapi.com (api.qiniu.com also works) QBox Signing string path + "\n", body not included
Domain HTTPS config /domain/<name>/httpsconf api.qiniu.com Qiniu Must include X-Qiniu-Date (UTC, skew ≤30s), body included in signing

Mixing the two up results in errors: 401 or 404, and the error message won’t tell you that you used the wrong signing method.

1
2
3
4
5
6
7
8
9
10
11
# qiniu-deploy.py excerpt
import requests, time

def qbox_token(ak, sk, path):
sign = hmac_sha1(sk, f"{path}\n") # body not included
return f"QBox {ak}:{sign}"

def qiniu_token(ak, sk, host, path, body):
data = f"{host}\n{path}\n{body}" # body included
return f"Qiniu {ak}:{hmac_sha1(sk, data)}", time.strftime(
"%Y%m%dT%H%M%SZ", time.gmtime())

These are the two error codes I ran into:

  • 612: the domain is not under that AK account, or it is not a CDN domain.
  • 404001: the route exists, but the domain isn’t managed by CDN (for example, it’s actually a Kodo domain).

Retiring a Domain

Making the Kodo custom origin domain pic.ewhisper.cn automated got stuck on this path.

I filed a ticket, and a Qiniu engineer confirmed on 2026-11-02: “This scenario currently does not support binding or replacing certificates via API.” Officially, only the console path is provided.

Two options: switch to manual maintenance in the console, or retire it outright.

Before retiring, I checked the dependencies first: origin fetch goes directly to the bucket, PicGo (which auto-uploads images to the image host when I write blog posts with Typora) is configured against it, and nearly 5000 image references across blog post markdown all point to pic-cdn. Nothing consumes pic.ewhisper.cn.

So I retired it: unbind the origin domain, delete the DNS CNAME, and remove it from the script’s DOMAINS and acme.sh’s Le_Alt.

SANs went from 4 down to 3.

Verification

A successful renewal only means the certificate was issued; deployment still needs to be verified separately.

First compare fingerprints, command as follows:

1
2
3
IP=$(dig +short A a.ewhisper.cn | head -1)
openssl s_client -connect "$IP:443" -servername a.ewhisper.cn </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256

Then check the renewal schedule.

1
acme.sh --list

Finally verify that the key and cert are paired — the md5 of the two public keys must match.

1
2
openssl x509 -in cert.pem -noout -pubkey | openssl md5
openssl pkey -in key.pem -pubout | openssl md5

The current certificate is valid from 2026-11-01 to 2027-01-30, with the next renewal on 2026-12-31.

A 514: Config Enabled, Service Not

During setup I hit a live HTTP Status Code: 514, so I’m noting down the troubleshooting order.

After the script updated the certificate, visiting the blog — ewhisper.cn and www.ewhisper.cn — both returned 514. 😱

I opened F12 in the browser and got two lines of evidence: HTTP/1.1 514 Frequency Capped and X-Cache-Lookup: Return Directly.

Then I went through the vendor’s status code documentation and falsified each item one by one: IP allow/deny lists, IP access rate limiting, certificate operations, egress IP, domain scheduling. Among them, “did I just break it myself” was ruled out by rolling back the old certificate and restoring the original state within ten minutes.

The real cause was that the CDN HTTPS service for both domains was itself in a disabled state; the first script’s parameter was off: --InstanceIdList "a.ewhisper.cn|off", and off directly disables the CDN HTTPS feature. I re-enabled it in the console and it immediately went back to 200.

Remaining Risks

  1. The certificate list keeps growing. Neither Tencent nor Qiniu deduplicates certificates, so each renewal adds one. Periodic cleanup by a fixed Alias or name is required.
  2. acme.sh is installed in ~/.acme.sh and depends on the logged-in user’s crontab. Changing machines or users requires migrating the whole thing, including account.conf. (Which is why I later stuffed this into a k8s cronjob 😝)
  3. Key leak prevention. The script directory is git-initialized and pushed to a private repo, tracking only a whitelist of scripts and docs that contain no keys; account.conf, *.key, *.cer, deploy.log, and config snapshots are never tracked, and before committing I scan with git grep.

Summary

  • Reuse credentials instead of applying for new Tokens: saves effort, but the credential permissions are too broad — if it leaks, you’re done for.
  • One multi-SAN certificate instead of four: saves network IO time; simpler certificate management
  • Fixed Alias with periodic cleanup instead of auto-deleting old certificates in the hook: for example, once pic.ewhisper.cn is no longer needed, just delete it separately
  • Domains that can’t be automated via API: check dependencies and retire them outright rather than switching to manual maintenance — save effort where you can, automate where you can

That’s all.

📚️References


Homelab Domain Certificate Automation: acme.sh + Tencent Cloud/Qiniu CDN Deployment Pipeline
https://e-whisper.com/posts/48136/
Author
east4ming
Posted on
November 6, 2026
Licensed under