Skip to content

Support custom ACME providers - #620

Merged
dmercuriali merged 4 commits into
diennea:masterfrom
NiccoMlt:536-acme-certificate-support-custom-acme-providers
Aug 5, 2026
Merged

Support custom ACME providers#620
dmercuriali merged 4 commits into
diennea:masterfrom
NiccoMlt:536-acme-certificate-support-custom-acme-providers

Conversation

@NiccoMlt

@NiccoMlt NiccoMlt commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Implementation

Certificates can now be issued by any RFC 8555 compliant CA, not just Let's Encrypt.

Closes #536.

Note

Stacked on #619 so merge that first; this PR should get retargeted to master afterwards automatically.

Caution

CI mocks the ACME boundary, so external account binding and custom directory URLs are not exercised end-to-end; QA will verify against a real provider. A local run against Pebble) is a meaningful smoke check.

Configuration

Providers are configured with a new configuration family and referenced per certificate:

acme.1.name=digicert
acme.1.url=https://acme.digicert.com/v2/acme/directory
# external account binding credentials (both or neither), as provided by the CA
acme.1.kid=your-key-identifier
acme.1.hmac=your-base64-encoded-hmac-key

certificate.3.hostname=myhostname.example.com
certificate.3.mode=acme
certificate.3.provider=digicert

If no provider is specified, Let's Encrypt is used, as before. Therefore, the letsencrypt name is reserved for the built-in provider.

kid and hmac are optional, but both-or-neither; those allow logging in with EAB (External Account Binding) credentials, if required by the provider.

Built-in letsencrypt provider still stores the account key under the legacy _acmeuserkey entry, while custom providers use _acmeuserkey_<name>, e.g. _acmeuserkey_digicert.

Important

carapace.acme.testmode JVM system property keeps affecting only the built-in Let's Encrypt provider (staging vs production); custom providers always use their configured URL.

REST APIs

  • POST /api/certificates accepts provider too.
  • POST /api/certificates/{domain}/upload accepts provider query param too (ACME type only);
  • GET /api/certificates exposes each certificate's provider
    • it also exposes acmeProviders, that feeds the UI select.

Web UI

  • added provider select in the certificate creation form;
  • added provider column in the list;
  • added provider row in the detail view.

@NiccoMlt NiccoMlt self-assigned this Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 656b4d78-153b-4af0-88c5-7b0bdb6be27d

📥 Commits

Reviewing files that changed from the base of the PR and between f9d80aa and bd5f456.

📒 Files selected for processing (22)
  • carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/CertificateData.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java
  • carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/ACMEClient.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/AcmeProviderConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java
  • carapace-server/src/main/resources/conf/server.dynamic.properties
  • carapace-server/src/test/java/org/carapaceproxy/configstore/ConfigurationStoreTest.java
  • carapace-server/src/test/java/org/carapaceproxy/core/RuntimeServerConfigurationTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/ACMEClientTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java
  • carapace-ui/src/main/webapp/src/components/certificates/Certificate.vue
  • carapace-ui/src/main/webapp/src/components/certificates/CertificateForm.vue
  • carapace-ui/src/main/webapp/src/components/certificates/Certificates.vue
🚧 Files skipped from review as they are similar to previous changes (21)
  • carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java
  • carapace-server/src/test/java/org/carapaceproxy/configstore/ConfigurationStoreTest.java
  • carapace-ui/src/main/webapp/src/components/certificates/Certificate.vue
  • carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java
  • carapace-server/src/main/resources/conf/server.dynamic.properties
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/ACMEClientTest.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java
  • carapace-ui/src/main/webapp/src/components/certificates/Certificates.vue
  • carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java
  • carapace-ui/src/main/webapp/src/components/certificates/CertificateForm.vue
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/ACMEClient.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/AcmeProviderConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/CertificateData.java
  • carapace-server/src/test/java/org/carapaceproxy/core/RuntimeServerConfigurationTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java
  • carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java
  • carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java

📝 Walkthrough

Walkthrough

The change adds configurable ACME providers with directory URLs and optional External Account Binding credentials. Certificate APIs and forms accept provider selection and expose provider data. Configuration stores maintain separate ACME account keys per provider. Dynamic certificate renewal routes orders and challenges to the selected provider. The certificate UI selects and displays providers.

Assessment against linked issues:

Objective Addressed Explanation
Support multiple configured ACME providers and provider-specific certificates, with Let's Encrypt as the default [#536]
Update the API and UI for ACME provider selection and display [#536]

Suggested reviewers: dmercuriali

Poem

A rabbit maps providers bright,
Keys sleep safely through the night.
Orders follow names with care,
Forms and tables show them there.
Hop, hop—certificates renew!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@NiccoMlt
NiccoMlt force-pushed the 536-acme-certificate-support-custom-acme-providers branch from 8d2198e to a696ea2 Compare July 23, 2026 13:27
@NiccoMlt
NiccoMlt force-pushed the 536-acme-certificate-support-custom-acme-providers branch from a696ea2 to 7bf8548 Compare August 3, 2026 08:26
@NiccoMlt
NiccoMlt changed the base branch from 536-bump-acme4j to master August 3, 2026 09:01
@NiccoMlt NiccoMlt linked an issue Aug 3, 2026 that may be closed by this pull request
@NiccoMlt
NiccoMlt changed the base branch from master to 536-bump-acme4j August 3, 2026 09:01
@dmercuriali
dmercuriali self-requested a review August 4, 2026 14:31
Any RFC 8555 CA via acme.<n>.* config (EAB kid/hmac supported,
validated at parse time), per-certificate provider selection,
per-provider account keys. Let's Encrypt stays default with the
legacy account key.

Closes diennea#536
@NiccoMlt
NiccoMlt changed the base branch from 536-bump-acme4j to master August 4, 2026 14:40
@NiccoMlt
NiccoMlt force-pushed the 536-acme-certificate-support-custom-acme-providers branch from 7bf8548 to 4970f11 Compare August 4, 2026 14:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java (1)

51-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a lazy-wrapped transient failure case.

isTransient unwraps AcmeLazyLoadingException before it classifies the cause. Line 45 tests that unwrapping only for a non-transient cause. No test proves that a transient cause survives the unwrapping. A change that drops the unwrap would still pass every current assertion in this class, because the non-transient path returns false either way.

The lazily fetched order is exactly the path testPendingOrderPollFailure exercises in DynamicCertificatesManagerTest, so the case is worth pinning here.

💚 Proposed additional assertions
         // a 5xx without a problem document, e.g., from a load balancer
         assertTrue(AcmeFailureClassifier.isTransient(new AcmeException("HTTP 503")));
+        // a bound resource defers the round-trip and wraps the transient failure
+        assertTrue(AcmeFailureClassifier.isTransient(new AcmeLazyLoadingException(
+                Order.class,
+                URI.create("https://localhost/order").toURL(),
+                new AcmeNetworkException(new IOException("connection reset")))));
+        // a message-less failure must not blow up the message match
+        assertFalse(AcmeFailureClassifier.isTransient(new AcmeException(null, null)));
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java`
around lines 51 - 63, Add a lazy-wrapped transient case to testTransientFailures
using AcmeLazyLoadingException around a transient cause such as
AcmeNetworkException, then assert AcmeFailureClassifier.isTransient returns
true. This should specifically verify unwrapping preserves transient
classification, complementing the existing non-transient lazy-loading test.
carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java (4)

224-225: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the client injection into a helper.

These two lines repeat at four sites, including the "acmeClients" field name as a string literal. A rename of the field would break all four silently, because Whitebox.setInternalState resolves the name at runtime.

A small private helper centralizes the field name and the ordering comment:

♻️ Proposed helper
    // must run after reloadConfiguration, which rebuilds the client map;
    // by-name, because there are other map fields
    private static void injectAcmeClients(DynamicCertificatesManager man, Map<String, ACMEClient> clients) {
        Whitebox.setInternalState(man, "acmeClients", clients);
    }

Each site then becomes:

         man.reloadConfiguration(conf);
-        // after the reload, which rebuilds the client map; by-name, because there are other map fields
-        Whitebox.setInternalState(man, "acmeClients", Map.of(DEFAULT_PROVIDER_NAME, ac));
+        injectAcmeClients(man, Map.of(DEFAULT_PROVIDER_NAME, ac));

Also applies to: 403-404, 544-545, 656-657

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java`
around lines 224 - 225, Extract the repeated Whitebox injection into a private
static injectAcmeClients helper accepting DynamicCertificatesManager and
Map<String, ACMEClient>, centralizing the "acmeClients" field name and ordering
comment. Replace all four direct Whitebox.setInternalState calls in
DynamicCertificatesManagerTest with this helper, preserving the existing client
maps and call ordering after reloadConfiguration.

776-785: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the transient branch persists nothing.

The transient case checks that the state stays ORDERING and the attempts count stays 0. It does not check that the manager skipped the write. Add a direct assertion, because "do not touch the persisted state" is the contract under test:

💚 Proposed additional assertion
         } else {
             // state untouched, will be retried at the next cycle
             assertCertificateState(domain, ORDERING, 0, man);
+            verify(store, never()).saveCertificate(any());
         }

For the stale branch, consider asserting the recorded message as well, so a regression in the ex.getMessage() fallback on DynamicCertificatesManager line 400 is caught:

assertEquals("unknown order", man.getCertificateDataForDomain(domain).getMessage());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java`
around lines 776 - 785, Extend the transient branch of the failure-case test
around assertCertificateState to verify that no certificate data was persisted,
using the manager’s existing persisted-state accessor. In the stale branch, also
assert that getCertificateDataForDomain(domain).getMessage() records “unknown
order” after the fallback path.

728-731: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that neither client received the other provider's order.

verify(letsencryptClient, times(1)).createOrderForDomain(eq(leNames)) counts only the calls that match leNames. It still passes if letsencryptClient also received custom.local. The same holds for customClient. The test therefore proves each client got its own order, but not that routing is exclusive, which is the property the test is named for.

💚 Proposed additional assertions
         verify(letsencryptClient, times(1))
                 .createOrderForDomain(eq(new CertificateData("le.local", null, WAITING).getNames()));
         verify(customClient, times(1))
                 .createOrderForDomain(eq(new CertificateData("custom.local", null, WAITING).getNames()));
+        // routing is exclusive: neither client sees the other provider's certificate
+        verify(letsencryptClient, never())
+                .createOrderForDomain(eq(new CertificateData("custom.local", null, WAITING).getNames()));
+        verify(customClient, never())
+                .createOrderForDomain(eq(new CertificateData("le.local", null, WAITING).getNames()));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java`
around lines 728 - 731, Strengthen the routing assertions in
DynamicCertificatesManagerTest around the letsencryptClient and customClient
createOrderForDomain verifications by also verifying that each client never
received the other provider’s domain names. Keep the existing positive
exactly-once assertions and add negative verifications for the cross-provider
orders.

92-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate DNS challenge limit property sets.

DynamicCertificatesManager.DNS_CHALLENGE_REACHABILITY_CHECKS_LIMIT is initialized once when the class loads, so the later System.setProperty(...) calls in testWildcardCertificateStateManagement and testSanCertificateStateManagement do not change the tested limit. Remove those calls to avoid relying on stale test setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java`
around lines 92 - 98, Remove the later System.setProperty calls for
carapace.acme.dnschallengereachabilitycheck.limit from
testWildcardCertificateStateManagement and testSanCertificateStateManagement;
retain the class-level setup executed before DynamicCertificatesManager loads so
both tests use the intended limit.
carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java (1)

61-72: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider bounding retries after AcmeRateLimitedException.

isTransient returns true for AcmeRateLimitedException. The caller then leaves the certificate state untouched and does not increment the attempts counter. The next manager cycle therefore repeats the same request against a CA that already rejected it for rate limiting, with no cap and no backoff. Let's Encrypt rate limits can last hours, so this loop can extend the limit window.

Two options:

  • Keep the rate-limited failure transient, but record AcmeRateLimitedException.getRetryAfter() and skip the certificate until that instant passes.
  • Count the rate-limited failure like other failures, so the configured maxattempts caps it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java`
around lines 61 - 72, Update the handling of AcmeRateLimitedException in
isTransient and its caller so rate-limited certificates cannot retry
indefinitely without backoff or attempt limits. Either persist getRetryAfter()
and skip requests until that time, or process the exception as a counted failure
governed by maxattempts; preserve transient handling for other retryable ACME
failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java`:
- Around line 271-285: At the anchor location (lines 271-285), ensure that the
call to updateDynamicCertificateForDomain occurs within the same configuration
lock transaction where provider ownership and domain conflicts are validated, OR
implement rollback to restore the previous certificate state if the
configuration apply fails after the call to updateDynamicCertificateForDomain.
At the sibling location (lines 388-411), apply the same atomic mutation pattern
to the certificate upload flow so that failed configuration applications do not
leave persisted certificate data orphaned.

In
`@carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java`:
- Around line 343-345: Update saveAcmeUserKey and its saveKeyPair interaction so
false is returned only when the key already exists; propagate connection,
storage, and other non-duplicate SQLException failures according to the
ConfigurationStore.saveAcmeUserKey contract. Preserve the existing success
behavior and identify duplicate-key errors explicitly rather than treating every
SQLException as a duplicate.

In
`@carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java`:
- Around line 368-389: The configuration save flow must prevent raw
acme.<n>.hmac values from appearing in logs or persisted configuration. Update
HerdDBConfigurationStore.commitConfiguration() to redact or mask each HMAC
property before its value is logged or stored, while preserving other properties
unchanged, and add a regression test asserting the logged output does not
contain the raw MAC key.

In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java`:
- Around line 61-72: Update AcmeFailureClassifier.isTransient to preserve
AcmeRateLimitedException.getRetryAfter() for the caller instead of reducing it
to a boolean; in DynamicCertificatesManager’s transient-failure path, track
consecutive failures per domain, delay retries with backoff or the rate-limit
retry time, and record the failure on the certificate after a threshold so
maxattempts and the UI observe it. Apply changes in
carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java
lines 61-72 and
carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java
lines 395-399.

In `@carapace-ui/src/main/webapp/src/components/certificates/CertificateForm.vue`:
- Around line 67-82: Update the CertificateForm submit flow around handleOk()
and handleSubmit() to reject ACME submissions when form.provider is empty,
preventing the incomplete form from being sent to /api/certificates/. Also keep
the Create action disabled until a valid provider selection is available, while
preserving submission behavior for non-ACME certificate types.

---

Nitpick comments:
In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java`:
- Around line 61-72: Update the handling of AcmeRateLimitedException in
isTransient and its caller so rate-limited certificates cannot retry
indefinitely without backoff or attempt limits. Either persist getRetryAfter()
and skip requests until that time, or process the exception as a counted failure
governed by maxattempts; preserve transient handling for other retryable ACME
failures.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java`:
- Around line 51-63: Add a lazy-wrapped transient case to testTransientFailures
using AcmeLazyLoadingException around a transient cause such as
AcmeNetworkException, then assert AcmeFailureClassifier.isTransient returns
true. This should specifically verify unwrapping preserves transient
classification, complementing the existing non-transient lazy-loading test.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java`:
- Around line 224-225: Extract the repeated Whitebox injection into a private
static injectAcmeClients helper accepting DynamicCertificatesManager and
Map<String, ACMEClient>, centralizing the "acmeClients" field name and ordering
comment. Replace all four direct Whitebox.setInternalState calls in
DynamicCertificatesManagerTest with this helper, preserving the existing client
maps and call ordering after reloadConfiguration.
- Around line 776-785: Extend the transient branch of the failure-case test
around assertCertificateState to verify that no certificate data was persisted,
using the manager’s existing persisted-state accessor. In the stale branch, also
assert that getCertificateDataForDomain(domain).getMessage() records “unknown
order” after the fallback path.
- Around line 728-731: Strengthen the routing assertions in
DynamicCertificatesManagerTest around the letsencryptClient and customClient
createOrderForDomain verifications by also verifying that each client never
received the other provider’s domain names. Keep the existing positive
exactly-once assertions and add negative verifications for the cross-provider
orders.
- Around line 92-98: Remove the later System.setProperty calls for
carapace.acme.dnschallengereachabilitycheck.limit from
testWildcardCertificateStateManagement and testSanCertificateStateManagement;
retain the class-level setup executed before DynamicCertificatesManager loads so
both tests use the intended limit.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9cf060c0-03e9-4d32-a638-35af274aa951

📥 Commits

Reviewing files that changed from the base of the PR and between f9d80aa and 4970f11.

📒 Files selected for processing (22)
  • carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/CertificateData.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java
  • carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/ACMEClient.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/AcmeProviderConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java
  • carapace-server/src/main/resources/conf/server.dynamic.properties
  • carapace-server/src/test/java/org/carapaceproxy/configstore/ConfigurationStoreTest.java
  • carapace-server/src/test/java/org/carapaceproxy/core/RuntimeServerConfigurationTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/ACMEClientTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java
  • carapace-ui/src/main/webapp/src/components/certificates/Certificate.vue
  • carapace-ui/src/main/webapp/src/components/certificates/CertificateForm.vue
  • carapace-ui/src/main/webapp/src/components/certificates/Certificates.vue

CA rejections (stale orders, malformed responses) and unexpected
local failures bump the cert attempts and fall back to a fresh
order; network errors, rate limits and CA outages retry in the same
state. Order polls fetch once per cycle. Policy lives in
AcmeFailureClassifier with fast pure unit tests.
@NiccoMlt
NiccoMlt force-pushed the 536-acme-certificate-support-custom-acme-providers branch from eb29216 to d57bbc2 Compare August 4, 2026 22:03
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java`:
- Around line 174-183: Update the reload flow around
DynamicCertificatesManager’s ACME client construction and loadCertificates() so
clients and the certificate cache are built in local variables first. Publish
both together only after certificate loading completes successfully, preserving
the previous runtime state unchanged when reload fails and keeping
acmeClientFor() consistent with the active certificate cache.
- Around line 395-408: Update the exception handling around the dynamic
certificate operation in DynamicCertificatesManager so
store.saveCertificate(cert) failures are handled independently from ACME failure
classification. After ACME state or remote side effects change, retry saving the
exact post-operation certificate state; do not invoke cert.error() or otherwise
route persistence failures through certificate failure handling, and preserve
the renewal task from being cancelled.

In
`@carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java`:
- Around line 492-494: Update the restoration assertions in the certificate test
to also compare previous.getProvider() with restored.getProvider(), ensuring a
failed update does not persist the changed provider value.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dca8182-ead7-4eb2-bf26-2b53ad683089

📥 Commits

Reviewing files that changed from the base of the PR and between f9d80aa and d57bbc2.

📒 Files selected for processing (22)
  • carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/CertificateData.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java
  • carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/ACMEClient.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/AcmeFailureClassifier.java
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/AcmeProviderConfiguration.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java
  • carapace-server/src/main/resources/conf/server.dynamic.properties
  • carapace-server/src/test/java/org/carapaceproxy/configstore/ConfigurationStoreTest.java
  • carapace-server/src/test/java/org/carapaceproxy/core/RuntimeServerConfigurationTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/ACMEClientTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java
  • carapace-ui/src/main/webapp/src/components/certificates/Certificate.vue
  • carapace-ui/src/main/webapp/src/components/certificates/CertificateForm.vue
  • carapace-ui/src/main/webapp/src/components/certificates/Certificates.vue
🚧 Files skipped from review as they are similar to previous changes (17)
  • carapace-ui/src/main/webapp/src/components/certificates/Certificate.vue
  • carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/CertificateData.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/AcmeFailureClassifierTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/ACMEClientTest.java
  • carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java
  • carapace-server/src/main/resources/conf/server.dynamic.properties
  • carapace-ui/src/main/webapp/src/components/certificates/CertificateForm.vue
  • carapace-server/src/main/java/org/carapaceproxy/server/config/AcmeProviderConfiguration.java
  • carapace-ui/src/main/webapp/src/components/certificates/Certificates.vue
  • carapace-server/src/main/java/org/carapaceproxy/configstore/PropertiesConfigurationStore.java
  • carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java
  • carapace-server/src/test/java/org/carapaceproxy/configstore/ConfigurationStoreTest.java
  • carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java
  • carapace-server/src/main/java/org/carapaceproxy/api/CertificatesResource.java
  • carapace-server/src/main/java/org/carapaceproxy/configstore/HerdDBConfigurationStore.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java (2)

175-188: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Publish all reload-dependent state only after loading succeeds.

keyPairsSize changes at Line 174 and domainsCheckerIPAddresses changes at Line 184 before loadCertificates(...) runs at Line 185. If loading fails, Lines 187-188 do not run, but those fields retain the new values. The next lifecycle can process the old certificate and client state with new reload settings. Stage all reload-dependent values and publish them together after loading succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java`
around lines 175 - 188, Stage keyPairsSize and domainsCheckerIPAddresses in
local reload variables alongside the rebuilt ACME clients and certificates,
without mutating manager fields before loadCertificates succeeds. After all
reload-dependent values load successfully, publish keyPairsSize,
domainsCheckerIPAddresses, acmeClients, and certificates together in
reloadConfiguration; preserve the existing state unchanged when loading fails.

270-273: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reset in-flight ACME state when the provider changes.

loadOrCreateDynamicCertificateForDomain reloads the persisted order and challenge state, then overwrites the provider at Line 272. If a certificate changes provider while it is DNS_CHALLENGE_WAIT, VERIFYING, VERIFIED, or ORDERING, Lines 345-359, 551, and 572 use the new provider with the previous provider's order URL or challenge data. Reset and clean up the old ACME state before switching providers, or reject the change until no order or challenge is pending.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java`
around lines 270 - 273, Update loadOrCreateDynamicCertificateForDomain so
changing the provider does not retain in-flight ACME state from the previous
provider. Before cert.setProvider(provider), detect a provider change and either
clear the persisted order, challenge, and related ACME state with the required
cleanup, or reject the change while an operation is pending; preserve normal
renewal behavior when the provider is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java`:
- Around line 187-188: Update DynamicCertificatesManager’s reloadConfiguration,
certificatesLifecycle, and reloadCertificatesFromDBInternal flows so acmeClients
and certificates are published atomically as one immutable runtime-state
snapshot, or consistently protected by the same mutex. Ensure readers cannot
observe values from different reloads or a later database reload.

---

Outside diff comments:
In
`@carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java`:
- Around line 175-188: Stage keyPairsSize and domainsCheckerIPAddresses in local
reload variables alongside the rebuilt ACME clients and certificates, without
mutating manager fields before loadCertificates succeeds. After all
reload-dependent values load successfully, publish keyPairsSize,
domainsCheckerIPAddresses, acmeClients, and certificates together in
reloadConfiguration; preserve the existing state unchanged when loading fails.
- Around line 270-273: Update loadOrCreateDynamicCertificateForDomain so
changing the provider does not retain in-flight ACME state from the previous
provider. Before cert.setProvider(provider), detect a provider change and either
clear the persisted order, challenge, and related ACME state with the required
cleanup, or reject the change while an operation is pending; preserve normal
renewal behavior when the provider is unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54247e35-d020-4f29-aadf-b4f0fa761945

📥 Commits

Reviewing files that changed from the base of the PR and between d57bbc2 and 229d1cb.

📒 Files selected for processing (3)
  • carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesTest.java
  • carapace-server/src/test/java/org/carapaceproxy/server/certificates/DynamicCertificatesManagerTest.java

HerdDB duplicate keys are typed, so real database failures now
propagate instead of masking as duplicates; secrets are masked in
configuration save logs. A failed configuration apply restores the
previously stored certificate row, and the UI blocks submits until
the provider list is available.
All reload-dependent state (key size, checker IPs, clients,
certificate cache) publishes together only after loading succeeds.
Store failures in the renewal loop are logged and retried next
cycle instead of burning attempts as CA rejections.
@NiccoMlt
NiccoMlt force-pushed the 536-acme-certificate-support-custom-acme-providers branch from 229d1cb to bd5f456 Compare August 5, 2026 08:22
@NiccoMlt

NiccoMlt commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@NiccoMlt

NiccoMlt commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@dmercuriali
dmercuriali merged commit 06642c6 into diennea:master Aug 5, 2026
2 checks 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.

Acme certificate > Support custom ACME providers

2 participants