acmesh-official / acmesh-official/acme.sh

Manual DNS renewal downloads previous cert due to stale Le_LinkOrder/Le_LinkCert reuse

Open
#7,105 2 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.