Repository navigation
security/acme-client: fix false "validation failed" status on skipped renewal - #5719
Open
daemonhorn wants to merge 1 commit into
Open
daemonhorn wants to merge 1 commit into
daemonhorn wants to merge 1 commit into
Conversation
… renewal acme.sh's renew() returns exit code 2 (RENEW_SKIP) when it decides on its own that a certificate is not due for renewal yet (e.g. the configured renewal interval or a CA-provided ACME Renewal Information window has not been reached). LeValidation\Base::run() treated any non-zero exit code as a failure, so this legitimate skip was logged as "domain validation failed" and LeCertificate::issue() set statusCode 400, which the Certificates GUI renders as "validation failed" - even though nothing actually failed and the existing certificate is still valid. Add LeCommon::ACME_RENEW_SKIP and special-case it in LeValidation\Base::run(): reset the flag at the top of run() (so a reused validation object can't carry a stale skip into a later real failure), log a notice instead of an error on a skip, and expose the outcome via a new public $skipped property. LeCertificate::issue() now treats a skip like its existing needsRenewal() no-op path - log and return, without calling setStatus() - so the certificate's last real status is left untouched instead of being overwritten. Fixes opnsense#4908. Validated on real OPNsense hardware over several days, observing all four status transitions: ok->skip (renewal attempted before it was due), ok->renew (on-time renewal), ok->validation failed (a forced, genuine failure, to confirm real failures still report correctly), and failed->ok (recovery once the forced failure was cleared). AI tools disclosure: root cause analysis, patch, and this commit message were prepared with assistance from Claude Code (Claude Sonnet 5, Anthropic), reviewed, tested, and verified by the submitter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MVmeD5j4rEfTLjzWAQmbiF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important notices
If AI was used, please disclose:
LeUtils::run_shell_command(),LeValidation\Base::run(),LeCertificate::issue(), and thecertificates.voltstatus formatter, and confirming acme.sh'sRENEW_SKIP=2exit code in the upstream acme.sh source), the code patch itself, and this PR description were prepared with AI assistance. All changes were reviewed and verified against the actual acme.sh and opnsense/plugins source, PHP-linted, packaged into a test.pkg, and validated on real OPNsense hardware over several days before this PR was opened.Describe the problem
On Services > ACME Client > Certificates, the "Last ACME Status" column
shows "validation failed" even when nothing actually failed — the
certificate simply wasn't due for renewal yet and acme.sh correctly
declined to touch it:
This misleads admins into thinking their ACME setup is broken when it's
working exactly as intended.
Root cause: acme.sh's
renew()function returns its own$RENEW_SKIPconstant (exit code
2) when it decides a cert isn't due yet. OPNsense'sLeValidation\Base::run()treats any non-zero exit code as a failure:LeCertificate::issue()then callssetStatus(400), and thecertificates.voltstatus formatter rendersstatusCode == 400as theliteral string "validation failed".
Note that
LeCertificate::issue()already has its ownneedsRenewal()guard (based on the certificate's
validFrom_time_t+ configuredrenewInterval) meant to avoid calling acme.sh at all when renewal isn'tdue. In practice it can still diverge from acme.sh's own internal
scheduling — acme.sh's randomized-renewal jitter, and/or a CA-supplied
ACME Renewal Information (ARI) window — which is how the skip path gets
reached at all. That divergence is acme.sh's own logic working as
designed; this PR does not attempt to change it, only to stop
mis-reporting a legitimate skip as a failure.
Describe the proposed solution
Minimal, targeted fix that mirrors the existing
needsRenewal()no-oppattern already in the codebase (log + return, without calling
setStatus(), so the certificate's last real status is left untouchedinstead of being overwritten):
LeCommon.php: addACME_RENEW_SKIP = 2, documenting acme.sh'sRENEW_SKIPexit-code contract.LeValidation\Base::run(): special-case that exit code — log a noticeinstead of an error, set a new public
$skippedflag, and returnfalse(unchanged failure handling for every other non-zero code).LeCertificate::issue(): when validation reports$skipped, log andreturn without calling
setStatus(), just like the existingneedsRenewal()early-return a few lines above.Deliberately out of scope: no new status code, no changes to the
certificates.voltformatter or thesetStatus()status-code table, andno changes to
remove()/revoke()'s own exit-code handling (differentsemantics, no legitimate "skip" outcome there).
Verified:
php -lon all changed files; traced the fix end-to-end againstthe actual acme.sh source (
renew()returning$RENEW_SKIPon thesingle-domain
--renewpath OPNsense uses); confirmed noLeValidationsubclass overrides
run(), so every challenge type is covered; built atest package (real published
os-acme-clientpatched with only thesefiles) and validated it on real OPNsense hardware over several days,
observing all four relevant status transitions:
ok->skip(renewalattempted before it was due),
ok->renew(on-time renewal),ok->validation failed(a forced, genuine failure, confirming real failures still reportcorrectly), and
failed->ok(recovery once the forced failure wascleared).
Related issue
Fixes #4908 — reported independently by three users with matching logs,
auto-closed by the stale-bot as
not_planneddespite the reporter'sunanswered request to reopen it. One of the reporters
(
stephanjahn-xapio) also tied the same symptom to acme.sh's newer ARIextension; since both triggers produce the same acme.sh exit code, this
fix covers both without special-casing either.
🤖 Generated with Claude Code
https://claude.ai/code/session_01MVmeD5j4rEfTLjzWAQmbiF