Skip to content

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

Description

@abonillabeeche

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions