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 | |
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 | |
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 | |
--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 | |
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 | |
Then check the renewal schedule.
1 | |
Finally verify that the key and cert are paired — the md5 of the two public keys must match.
1 | |
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
- 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.
- acme.sh is installed in
~/.acme.shand depends on the logged-in user’s crontab. Changing machines or users requires migrating the whole thing, includingaccount.conf. (Which is why I later stuffed this into a k8s cronjob 😝) - 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 withgit 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.cnis 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.