acmesh-official / acmesh-official/acme.sh
Manual DNS renewal downloads previous cert due to stale Le_LinkOrder/Le_LinkCert reuse
- Dominant language
- Shell
- Stars
- 47.6k
- Forks
- 5.7k
- Avg merge
- 7d 1h
- Merged PRs (30d)
- 16
Description
## Version
v3.1.2 confirmed reproducing; v3.1.4 still contains the same code paths.
## Environment
- macOS 15.5 (Darwin 25.5.0), bash
- CA: ZeroSSL (acme.zerossl.com/v2/DV90)
- Cert type: ECC wildcard (`*.example.com`)
- Renewal mode: manual DNS (`--dns --yes-I-know-dns-manual-mode-enough-go-ahead-please`)
## Reproduction
1. Issue a wildcard cert in manual DNS mode. `Le_LinkOrder` and `Le_LinkCert` get written to the domain's `.conf`.
2. ~90 days later, run `--renew ... --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please`.
3. First invocation prints the TXT challenge and exits (expected).
4. Add the TXT record; second invocation reports \"Cert success\" and writes the cert files.
**Actual result:** the written cert is byte-for-byte identical to the previous cert (same serial, fingerprint, notAfter — still expired). acme.sh only reissues on the very first `--issue`; every subsequent manual-DNS `--renew` silently re-downloads the previous cert.
## Log excerpt showing the bug
\`\`\`
Le_OrderFinalize='https://acme.example/v2/.../order/NEW-ORDER-ID/finalize'
Order status is 'processing', let's sleep and retry.
Polling order status: https://acme.example/v2/.../order/OLD-ORDER-ID <-- previous run's order
Le_LinkCert='https://acme.example/v2/.../cert/OLD-CERT-ID' <-- previous run's cert
\`\`\`
## Root cause
In \`_issue()\`:
**acme.sh:4862-4866 (v3.1.4)** — the reset is intentionally skipped for DNS manual mode (to preserve state across the two-invocation flow), but this preserves state from the *previous cert's lifetime*, not just the current run:
\`\`\`sh
elif ! _hasfield \"\$_web_roots\" \"\$W_DNS\"; then
Le_OrderFinalize=\"\"
Le_LinkOrder=\"\"
Le_LinkCert=\"\"
fi
\`\`\`
**acme.sh:5689-5691 (v3.1.4)** — after finalizing a fresh order, \`Le_LinkOrder\` is only pulled from response headers when the variable is empty. On renewal it isn't, so the subsequent poll (\`_info \"Polling order status: \$Le_LinkOrder\"\`) hits the old order, which is still \`status:valid\` and returns the old certificate URL:
\`\`\`sh
if [ -z \"\$Le_LinkOrder\" ]; then
Le_LinkOrder=\"\$(echo \"\$responseHeaders\" | grep -i '^Location.*\$' | ...)\"
fi
\`\`\`
## Workaround
Manually strip the three variables from the domain \`.conf\` before running \`--renew\`:
\`\`\`sh
sed -i '' \"/^Le_LinkOrder=/d;/^Le_LinkCert=/d;/^Le_OrderFinalize=/d\" ~/.acme.sh/DOMAIN_ecc/DOMAIN.conf
\`\`\`
Then \`--issue --force\` produces a genuinely new cert.
## Suggested fixes (any one)
1. Overwrite unconditionally after finalize: drop the \`[ -z \"\$Le_LinkOrder\" ]\` guard and always pull from response headers.
2. Reset \`Le_LinkOrder\`/\`Le_LinkCert\` at the top of the finalize step (not the top of \`_issue\`), so the manual-DNS state preservation doesn't include them.
3. Reset these three variables whenever a new order is created in this run.
Happy to submit a PR if a preferred approach is confirmed.
Contributor guide
Assessment
This issue has not been assessed yet.