Skip to content

Repair-AcmeDnsCredential: auto-recover password, clear ARI cache, offer renewal#12

Merged
andrewyager merged 4 commits into
mainfrom
feat/repair-script-auto-recover-and-renew
May 27, 2026
Merged

Repair-AcmeDnsCredential: auto-recover password, clear ARI cache, offer renewal#12
andrewyager merged 4 commits into
mainfrom
feat/repair-script-auto-recover-and-renew

Conversation

@andrewyager

Copy link
Copy Markdown
Member

Summary

Three usability gaps in scripts/Recovery/Repair-AcmeDnsCredential.ps1 surfaced while recovering a host whose certificate had silently failed to renew. This PR addresses all three.

1. Auto-recover the acme-dns password from the existing credential file

The repair script previously always prompted for the original acme-dns /register password, but operators almost never have it written down — the password is generated once and only used programmatically by win-acme thereafter. The existing credential file already contains the password (encrypted), and the user that originally registered the domain can decrypt it without supplying anything.

  • New Resolve-AcmeDnsPassword helper. When -Password is omitted:
    • StorageMethod = 'DPAPI' (CurrentUser) → unwrap with ConvertTo-SecureString -String (works when the script is run as the original registering user).
    • StorageMethod = 'DPAPI-LocalMachine' → unwrap with ProtectedData::Unprotect (makes re-runs idempotent).
    • Falls back to the interactive prompt only if auto-recovery fails, with a hint to "run as the original user."

2. Clear the win-acme ARI cache for the repaired domain

After multiple failed renewal attempts (e.g. while the DPAPI scope bug was active) Let's Encrypt rejects the next new-order with:

urn:ietf:params:acme:error:alreadyReplaced
cannot indicate an order replaces certificate with serial "…", which already has a replacement order

— because the earlier failed orders claimed the ARI replacement slot. The repair now finds renewal configs that reference the repaired domain and moves their cached *-temp.pfx files into a timestamped ari-bypass-backup-<ts> subdirectory. The next new-order request then omits the ARI replaces field and gets past the stale pending order. win-acme rebuilds the cache after a successful renewal.

  • New Clear-WinAcmeAriCache helper. Honors -WhatIf/ShouldProcess.
  • -SkipAriCleanup switch to disable.
  • -WinAcmePath parameter for non-default win-acme installs (defaults to $env:ProgramData\win-acme).

3. Offer to run the renewal at the end

  • New -RunRenewal <Prompt|Yes|No> parameter, defaulting to Prompt.
  • Non-interactive sessions are treated as No so unattended RMM invocations don't hang on Read-Host.
  • Find-WacsExecutable searches C:\Tools\win-acme\ and the Program Files locations.
  • Invoke-WinAcmeRenewal runs wacs.exe --renew --force --verbose and reports the exit code.

Version bumped to 1.1.0; comment-based help and examples updated.

Test plan

  • Run on a host with StorageMethod = 'DPAPI', as the original registering user, with no -Password. Confirm auto-recovery succeeds and the credential file is migrated to DPAPI-LocalMachine.
  • Re-run the script on the same host. Confirm idempotent auto-recovery from the now-DPAPI-LocalMachine blob.
  • Run on a host with StorageMethod = 'DPAPI' as a different admin. Confirm the script falls back to the interactive prompt with a clear message about which user to run as.
  • Run with -RunRenewal Yes and confirm wacs.exe --renew --force --verbose is invoked and its exit code reported.
  • Run with -WhatIf and confirm no destructive operations are performed (no credential file rewrite, no ARI cache file moves, no renewal invocation).
  • Run with -SkipAriCleanup and confirm the win-acme cache is left untouched.
  • On a host with stale ARI pending orders (the failure case that motivated this PR), confirm that after the repair + ARI cleanup, wacs.exe --renew --force --verbose reaches DNS validation instead of failing at new-order with alreadyReplaced.
  • Invoke-ScriptAnalyzer -Path './scripts' -Recurse -ExcludeRule PSAvoidUsingWriteHost reports no findings (already verified locally).

🤖 Generated with Claude Code

…er renewal

Three usability gaps surfaced while running the repair tool against a
host whose certificate had silently failed to renew:

1. The script always prompted for the original acme-dns /register
   password, but operators rarely have it written down anywhere -- the
   password is generated once and used only programmatically by win-acme
   thereafter. When the existing credential file is stored with the
   legacy DPAPI scope, the same user that registered the domain can
   already decrypt it via ConvertTo-SecureString; the script now does
   that automatically and only prompts when auto-recovery fails. The
   DPAPI-LocalMachine case is also handled so re-runs are idempotent.

2. After multiple failed renewal attempts (e.g. while the DPAPI scope
   bug was active) Let's Encrypt rejects the next new-order with
   "urn:ietf:params:acme:error:alreadyReplaced" because earlier orders
   claimed the ARI replacement slot. The repair now finds renewal
   configs that reference the repaired domain and moves their cached
   '-temp.pfx' files into a timestamped backup subdirectory, so the
   next new-order request omits the ARI 'replaces' field and gets past
   the stale pending order. Disabled with -SkipAriCleanup.

3. Added a -RunRenewal Prompt|Yes|No parameter so the operator can
   trigger 'wacs.exe --renew --force --verbose' immediately after the
   repair. Defaults to Prompt; non-interactive sessions are treated as
   No so unattended RMM invocations don't hang.

Version bumped to 1.1.0; comment-based help and examples updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 07:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves the scripts/Recovery/Repair-AcmeDnsCredential.ps1 recovery workflow to reduce operator friction and help unblock renewals after acme-dns credential repair, including optional post-repair renewal execution.

Changes:

  • Adds automatic password recovery from existing credential files for both legacy DPAPI (CurrentUser) and DPAPI-LocalMachine.
  • Adds win-acme ARI cache cleanup logic by moving matching *-temp.pfx files aside to avoid alreadyReplaced renewal failures.
  • Adds an optional end-of-run renewal invocation (-RunRenewal Prompt|Yes|No) and wacs.exe discovery.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/Recovery/Repair-AcmeDnsCredential.ps1
Comment thread scripts/Recovery/Repair-AcmeDnsCredential.ps1 Outdated
andrewyager and others added 3 commits May 27, 2026 18:07
Copilot review noted that the helper returned 0 in three distinct
cases (win-acme dir missing, no renewal subdirs, or ran-with-nothing),
so the caller couldn't tell "we skipped because of a precondition"
apart from "we ran and found nothing to do". Net effect was a misleading
"Nothing to clean" message stacked on top of the helper's own
Write-Warning whenever a precondition wasn't met.

Switch the return to a PSCustomObject with Status (Skipped|Ran) and
MovedCount. The caller now only prints the summary line when the helper
actually ran; the Skipped branch already explains the cause via
Write-Warning, so no follow-up message is needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@andrewyager andrewyager merged commit da02c20 into main May 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants