feat(ingress): opt-in DNS-01 ACME with pluggable providers (Cloudflare + exec) - #25
Open
zainokta wants to merge 1 commit into
Open
feat(ingress): opt-in DNS-01 ACME with pluggable providers (Cloudflare + exec)#25zainokta wants to merge 1 commit into
zainokta wants to merge 1 commit into
Conversation
…e + exec) Denia issued TLS certs only via HTTP-01, which fails for hostnames fronted by a proxy (Cloudflare orange-cloud, a WAF) because the :80 challenge never reaches the origin. Add an opt-in DNS-01 challenge, modeled on Traefik/lego: a provider-agnostic core + pluggable providers. - `Dns01Provider` trait (present/cleanup/timeout) + `build_provider` factory selected by `DENIA_ACME_DNS_PROVIDER`. - `cloudflare` provider: API token (Zone:Read + DNS:Edit), zone resolved by most-specific `GET /zones?name=` (no PSL dep), retryable/permanent error classification, token held in a redacted `Secret`. - `exec` provider: runs `<script> present|cleanup <fqdn> <value>` for any DNS host; Denia holds no credential. Hook stderr redacted by default (`DENIA_ACME_DNS_EXEC_DEBUG=1` to include). - Shared propagation: confirms the TXT over DoH (default Cloudflare+Google, per-request timeout) before `set_ready`; rejects SERVFAIL/REFUSED/5xx, treats NXDOMAIN as no-CNAME. CNAME delegation of `_acme-challenge` resolved to an effective FQDN; loops / hop-limit hard-error. - HTTP-01 remains the default and behavior-identical. `AcmeDriver::issue` keeps order setup + finalize identical; only per-authorization answering branches; TXT cleanup on every exit path. - Config: `acme_challenge`, `acme_dns_provider`, `cf_dns_api_token(_file)`, `acme_dns_exec`, `acme_dns_propagation_secs`, `acme_dns_resolvers`. `require_dns01_provider` fails fast at boot (cloudflare->token, exec->executable script path). - ADR-038, config template, and project guidance updated.
zainokta
force-pushed
the
feat/dns01-acme-cloudflare
branch
from
July 27, 2026 04:36
31342a7 to
9ffeddd
Compare
zainokta
force-pushed
the
feat/dns01-acme-cloudflare
branch
from
July 27, 2026 05:15
9ffeddd to
1a61605
Compare
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.
What
Adds an opt-in DNS-01 ACME challenge so Denia can issue TLS certs for hostnames fronted by a proxy/WAF (Cloudflare orange-cloud, SafeLine, …) where the in-process HTTP-01 challenge can't reach
:80. Modeled on Traefik/lego: a provider-agnostic core + pluggable providers. HTTP-01 stays the default and behavior-identical.Why
denia authagainst a Cloudflare-proxied console returned 403 and TLS couldn't be issued: HTTP-01 needs Let's Encrypt to reach the origin directly, which an orange-cloud proxy blocks. DNS-01 proves ownership via a_acme-challengeTXT record — no inbound reachability needed.How
Dns01Providertrait (present/cleanup/timeout) +build_providerfactory selected byDENIA_ACME_DNS_PROVIDER.cloudflareprovider — API token (Zone:Read+DNS:Edit), zone resolved most-specific-first viaGET /zones?name=(no public-suffix-list dep), retryable vs permanent error classification, token in a redactedSecret.execprovider — runs<script> present|cleanup <fqdn> <value>for any DNS host (Akamai, Route53, PowerDNS, BIND…); Denia holds no credential. Hook stderr redacted by default.set_ready; rejects SERVFAIL/REFUSED/5xx, treats NXDOMAIN as no-CNAME. CNAME delegation of_acme-challengeresolved to an effective FQDN (delegate into a zone you control); loops / hop-limit hard-error.require_dns01_providerfails fast at boot.Config
acme_challenge,acme_dns_provider,cf_dns_api_token(_file),acme_dns_exec,acme_dns_propagation_secs,acme_dns_resolvers. See ADR-038 andconfig.toml.in.Verification
cargo build— cleancargo test --lib— 543 passed, 2 ignored (network-gated DNS-01/HTTP-01 net tests underDENIA_RUN_ACME_NET_TESTS=1)cargo fmt --all,cargo clippy --all-targets --all-features— clean for new codeNote: scope is single-host certs; wildcard issuance is a documented follow-up.