diff --git a/AcmeCaPlugin/AcmeCaPlugin.cs b/AcmeCaPlugin/AcmeCaPlugin.cs index bd842fe..fd7a2d8 100644 --- a/AcmeCaPlugin/AcmeCaPlugin.cs +++ b/AcmeCaPlugin/AcmeCaPlugin.cs @@ -685,21 +685,38 @@ private async Task ProcessAuthorizations(AcmeClient acmeClient, OrderDetails ord async () => await cnameResolver.ResolveChallengeTargetAsync(validation.DnsRecordName), detail: $"from {validation.DnsRecordName}"); + // Decide which name to resolve the DNS provider plugin against. The framework + // matches validators against the (wildcard) domain patterns configured per + // provider, keyed on the certificate/zone name (e.g. *.keyfactor.ssl4saas.com) + // -- NOT the _acme-challenge record name. So: + // * No delegation -> resolve on the cert domain (www.example.com), exactly + // as before delegation support existed. + // * CNAME delegated -> resolve on the terminal target (host.validation-zone), + // so the provider that owns the delegation zone is chosen. + // Note the resolver returns the original name unchanged when no CNAME exists, + // and that original name still carries the _acme-challenge prefix, which would + // not match the configured wildcard -- hence the explicit branch here. + bool isDelegated = !string.Equals( + recordName?.TrimEnd('.'), + validation.DnsRecordName?.TrimEnd('.'), + StringComparison.OrdinalIgnoreCase); + var validatorLookupName = isDelegated ? recordName : domain; + var domainValidator = flow.Step($"ResolveValidator:{domain}", () => { - var v = _validatorFactory.ResolveDomainValidator(recordName, DNS_CHALLENGE_TYPE); + var v = _validatorFactory.ResolveDomainValidator(validatorLookupName, DNS_CHALLENGE_TYPE); if (v == null) { throw new InvalidOperationException( - $"Failed to resolve domain validator for '{recordName}' (challenge for '{domain}'). " + + $"Failed to resolve domain validator for '{validatorLookupName}' (challenge for '{domain}'). " + "Ensure the appropriate DNS provider plugin is deployed and configured for the zone that hosts the (possibly CNAME-delegated) challenge record."); } return v; }); - _logger.LogInformation("Using domain validator: {ValidatorType} for record: {RecordName} (domain: {Domain})", - domainValidator.GetType().Name, recordName, domain); + _logger.LogInformation("Using domain validator: {ValidatorType} resolved on {LookupName} (record: {RecordName}, domain: {Domain}, delegated: {Delegated})", + domainValidator.GetType().Name, validatorLookupName, recordName, domain, isDelegated); await flow.StepAsync($"StageValidation:{domain}", async () => diff --git a/AcmeCaPlugin/AcmeCaPlugin.csproj b/AcmeCaPlugin/AcmeCaPlugin.csproj index 942c7ec..961da45 100644 --- a/AcmeCaPlugin/AcmeCaPlugin.csproj +++ b/AcmeCaPlugin/AcmeCaPlugin.csproj @@ -22,32 +22,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/CHANGELOG.md b/CHANGELOG.md index f89b88f..3346fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# v1.4.0 +# v2.0.0 * Dns Plugin Support * Cname Proxy Support diff --git a/README.md b/README.md index e6523cb..f9282ad 100644 --- a/README.md +++ b/README.md @@ -52,17 +52,14 @@ This plugin has been tested and confirmed to work with the following ACME provid It is designed to be provider-agnostic and should work with any standards-compliant ACME server. -### 🌐 Supported DNS Providers -DNS-01 challenge automation is supported through the following providers: -- **Google Cloud DNS** -- **AWS Route 53** -- **Azure DNS** -- **Cloudflare** -- **NS1** -- **Infoblox** -- **RFC 2136 Dynamic DNS** (BIND with TSIG authentication) - -Additional DNS providers can be added by extending the included `IDnsProvider` interface. +### 🌐 DNS Providers (Pluggable) +DNS-01 challenge automation is handled by **separate, pluggable DNS provider plugins** that are deployed alongside the AnyCA Gateway — they are no longer built into this plugin. The Gateway resolves the appropriate DNS provider plugin per domain at enrollment time. + +For the current list of available DNS provider plugins, see the Keyfactor GitHub organization: + +👉 **[Keyfactor DNS provider plugins (`-dnsplugin`)](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin)** + +Each plugin repository documents its own supported authentication methods and configuration keys. New DNS providers can be added by publishing a new plugin that implements the Gateway's `IDomainValidator` interface — no change to this ACME plugin is required. --- @@ -73,9 +70,10 @@ Additional DNS providers can be added by extending the included `IDnsProvider` i 2. Plugin initializes ACME client and creates a new order. 3. For each domain: a. Retrieve DNS-01 challenge. - b. Use the configured DNS provider to publish challenge record. - c. Wait for DNS propagation and validate record. - d. Notify ACME provider to trigger validation. + b. Resolve any CNAME delegation: follow the CNAME chain from `_acme-challenge.` to its terminal target (see CNAME Delegation below). + c. Select the DNS provider plugin for the zone that owns the (resolved) record name and publish the challenge TXT record there. + d. Wait for DNS propagation and validate record. + e. Notify ACME provider to trigger validation. 4. Once all challenges are valid, finalize the order using CSR. 5. Download the signed certificate from ACME provider. 6. Return PEM certificate to the Gateway. @@ -87,7 +85,7 @@ The plugin uses a modular design that separates ACME communication logic and DNS ## Compatibility -The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2 and later. +The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 26.2 and later. ## Support The Acme AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. @@ -101,17 +99,15 @@ The Acme AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor custo This plugin automates DNS-01 challenges using pluggable DNS provider implementations. These providers create and remove TXT records to prove domain control to ACME servers.
-✅ Supported DNS Providers - -| Provider | Auth Methods Supported | Config Keys Required | -|--------------|-----------------------------------------------|--------------------------------------------------------| -| Google DNS | Service Account Key (file or JSON), or ADC | `Google_ServiceAccountKeyPath`, `Google_ServiceAccountKeyJson`, `Google_ProjectId` | -| AWS Route 53 | Access Key/Secret or IAM Role | `AwsRoute53_AccessKey`, `AwsRoute53_SecretKey` | -| Azure DNS | Client Secret or Managed Identity | `Azure_TenantId`, `Azure_ClientId`, `Azure_ClientSecret`, `Azure_SubscriptionId` | -| Cloudflare | API Token | `Cloudflare_ApiToken` | -| NS1 | API Key | `Ns1_ApiKey` | -| Infoblox | Username/Password (Basic Auth) | `Infoblox_Host`, `Infoblox_Username`, `Infoblox_Password` | -| RFC 2136 | TSIG Key (BIND) | `Rfc2136_Server`, `Rfc2136_Zone`, `Rfc2136_TsigKeyName`, `Rfc2136_TsigKey` | +🔌 Available DNS Provider Plugins + +DNS providers are distributed as **standalone plugins**, each in its own repository, and are deployed alongside the AnyCA Gateway rather than bundled into this ACME plugin. This lets you add or upgrade a DNS provider without rebuilding the ACME plugin. + +For the current, authoritative list of available DNS provider plugins, query the Keyfactor GitHub organization: + +👉 **[github.com/orgs/Keyfactor/repositories?q=-dnsplugin](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin)** + +Each plugin's own repository is the source of truth for its supported authentication methods, required configuration keys, and setup instructions. Configure the DNS provider(s) through the AnyCA Gateway's **Domain Validation** configuration; the Gateway resolves the correct plugin per domain at enrollment time.
@@ -125,133 +121,86 @@ This logic is handled by the `DnsVerificationHelper` class and ensures a high-co
-🔑 Credential Flow - -Each provider supports multiple credential strategies: - -- **Google DNS**: - - ✅ **Service Account Key File** (via `Google_ServiceAccountKeyPath`) - - ✅ **Service Account Key JSON** (via `Google_ServiceAccountKeyJson` - paste JSON directly) - - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth) +🔗 CNAME Delegation (Proxy) Lookup -- **AWS Route 53**: - - ✅ **Access/Secret Keys** (`AwsRoute53_AccessKey`, `AwsRoute53_SecretKey`) - - ✅ **IAM Role via EC2 Instance Metadata** (no explicit credentials) +Many organizations do not want ACME automation to hold write access to their production DNS zone. The industry-standard pattern is to **delegate just the ACME challenge name** to a separate, isolated validation zone using a `CNAME` record. The plugin supports this transparently. -- **Azure DNS**: - - ✅ **Client Secret** (explicit `TenantId`, `ClientId`, `ClientSecret`) - - ✅ **Managed Identity** or environment-based credentials via `DefaultAzureCredential` +#### Why delegate? -- **Cloudflare**: - - ✅ **Bearer API Token** for zone-level DNS control +A `CNAME` at `_acme-challenge.` points challenge validation at another zone. ACME automation then only needs write access to that isolated zone — never the production zone. A `CNAME` also cannot coexist with any other record type at the same name (RFC 1034), so the TXT record **must** be created at the CNAME's target, not at the original challenge name. -- **NS1**: - - ✅ **API Key** passed in header `X-NSONE-Key` +#### How the plugin resolves it -- **Infoblox**: - - ✅ **Username/Password** (Basic Auth via WAPI REST API) - - Optional: `Infoblox_WapiVersion` (defaults to `2.12`) - - Optional: `Infoblox_IgnoreSslErrors` for self-signed certificates +Before publishing the challenge record, the plugin runs the `CnameResolver`, which: -- **RFC 2136 (BIND)**: - - ✅ **TSIG Key** for secure dynamic DNS updates - - Supports algorithms: `hmac-md5`, `hmac-sha1`, `hmac-sha256`, `hmac-sha384`, `hmac-sha512` - - Default algorithm: `hmac-sha256` (recommended) - - Optional: `Rfc2136_Port` (defaults to `53`) +1. Issues a DNS `CNAME` query for `_acme-challenge.`. +2. **Follows the chain to its terminus.** Delegation can be nested multiple levels deep (`A → B → C → â€Ļ`); the resolver re-queries at each hop and stops only when it reaches a name that has no further `CNAME`. That terminal name is where the TXT record is created. +3. Returns the original name unchanged when **no** `CNAME` exists — so non-delegated domains behave exactly as before (fully backwards compatible). -
- -
-đŸĸ On-Premise DNS (RFC 2136) +Safety guards: the resolver detects loops (a name that reappears in the chain) and enforces a maximum depth of **10 hops**, logging a warning and stopping at the last good name rather than looping forever. -The RFC 2136 provider enables ACME DNS-01 challenges with on-premise DNS servers that support dynamic updates, including: +#### Provider selection follows the delegation -- **BIND** (Berkeley Internet Name Domain) -- **PowerDNS** (with dynamic update support) -- Any DNS server supporting RFC 2136 with TSIG authentication +The DNS provider plugin is resolved against the **name where the record actually lands**: -#### Configuration Requirements +- **No delegation** → the provider is selected for the certificate domain (e.g. `www.example.com`). +- **Delegated** → the provider is selected for the **resolved terminal target** (e.g. `abc123.acme-validation.net`). -| Field | Description | Required | -|-------|-------------|----------| -| `Rfc2136_Server` | DNS server hostname or IP address | ✅ Yes | -| `Rfc2136_Zone` | DNS zone to update (e.g., `example.com`) | ✅ Yes | -| `Rfc2136_TsigKeyName` | TSIG key name (e.g., `acme-update-key`) | ✅ Yes | -| `Rfc2136_TsigKey` | Base64-encoded TSIG secret key | ✅ Yes | -| `Rfc2136_TsigAlgorithm` | TSIG algorithm (default: `hmac-sha256`) | Optional | -| `Rfc2136_Port` | DNS server port (default: `53`) | Optional | -| `DnsVerificationServer` | DNS server IP for verification (for private zones) | Optional | +This means a challenge delegated into a zone hosted by a *different* DNS provider is routed to the plugin that owns that zone. Propagation checks and cleanup also operate on the resolved name. -#### Generating TSIG Keys +> â„šī¸ Provider selection uses the AnyCA Gateway's Domain Validation configuration, which matches a configured (optionally wildcard) domain pattern one label at a time. Ensure the **delegation target's zone** is covered by a Domain Validation entry — e.g. a target of `abc123.acme-validation.net` needs a validator whose domain matches `*.acme-validation.net`. -**For BIND:** -bash +#### Example (multi-level delegation across providers) -**Generate a TSIG key using tsig-keygen (BIND 9.10+)** -tsig-keygen -a hmac-sha256 acme-update-key +```text +Cert domain: www.example.com (production zone, e.g. GoDaddy) +Challenge name: _acme-challenge.www.example.com + +DNS records (static, created once): + _acme-challenge.www.example.com CNAME hop1.example.com (GoDaddy) + hop1.example.com CNAME hop2.example.com (GoDaddy) + hop2.example.com CNAME val.acme-zone.net (points into the validation zone) + +Resolution + placement: + _acme-challenge.www.example.com → hop1 → hop2 → val.acme-zone.net (terminal) + Provider plugin selected for: acme-zone.net (the zone that owns val.acme-zone.net) + TXT record created at: val.acme-zone.net + ACME CA queries _acme-challenge.www.example.com, follows the CNAMEs, finds the TXT ✅ +``` -**Output example:** -key "acme-update-key" { - algorithm hmac-sha256; - secret "base64encodedkey=="; -}; +> â„šī¸ **Private/internal delegation zones:** set `DnsVerificationServer` to your authoritative DNS server IP. The `CnameResolver` honors it for the CNAME lookups; otherwise public resolvers (Google, Cloudflare, Quad9) are used. -#### BIND Configuration Example +
-Add to `named.conf`: +
+🔑 Provider Credentials & Configuration -key "acme-update-key" { - algorithm hmac-sha256; - secret "YourBase64EncodedKeyHere=="; -}; +Authentication methods and required configuration keys are specific to each DNS provider and are **documented in that provider's own plugin repository** — see the [`-dnsplugin` repositories](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin). Credentials and settings are supplied through the AnyCA Gateway's **Domain Validation** configuration for the chosen plugin, not in this ACME plugin's configuration. -zone "example.com" { - type master; - file "/var/named/example.com.zone"; - allow-update { key "acme-update-key"; }; -}; +
+
+đŸĸ On-Premise / Private DNS -> âš ī¸ **Security Note:** TSIG keys should be treated as secrets. Store them securely and use strong keys generated with cryptographically secure random number generators. +On-premise and private DNS support (e.g. RFC 2136 dynamic updates against BIND/PowerDNS with TSIG) is provided by the corresponding DNS provider plugin — see its repository under the [`-dnsplugin` list](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin) for TSIG key generation, server/zone settings, and setup examples. -> âš ī¸ **Private DNS Zones:** For private/local DNS zones (e.g., `.local`), set `DnsVerificationServer` to your authoritative DNS server IP so the plugin can verify TXT record propagation. +> âš ī¸ **Private DNS Zones:** For private/local DNS zones (e.g., `.local`) that are not reachable via public resolvers, set `DnsVerificationServer` to your authoritative DNS server IP. This ACME plugin uses it both to verify TXT record propagation and to resolve CNAME delegation chains.
🧩 Adding New DNS Providers -To add support for new DNS services: +DNS providers are independent plugins, so adding a new one requires **no change to this ACME plugin**. Publish a plugin that implements the AnyCA Gateway's `IDomainValidator` interface (create/cleanup the validation record for a domain), deploy it alongside the Gateway, and configure it under **Domain Validation**. The Gateway will resolve it per domain at enrollment time. -1. Implement the `IDnsProvider` interface: - ```csharp - public interface IDnsProvider - { - Task CreateRecordAsync(string recordName, string txtValue); - Task DeleteRecordAsync(string recordName); - } - ``` - -2. Register the new provider in the `DnsProviderFactory`: - ```csharp - case "yourprovider": - return new YourCustomDnsProvider(config.YourProviderConfigValues...); - ``` - -3. Use zone detection logic similar to `GoogleDnsProvider`, `AzureDnsProvider`, or `Ns1DnsProvider`. - -Each provider is instantiated dynamically based on the `DnsProvider` field in the `AcmeClientConfig`. - -> 🔁 This modular DNS system ensures challenge automation works across cloud providers and is easily extensible. +Use any existing plugin in the [`-dnsplugin` list](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin) as a reference implementation.
-🔒 CA-Level DNS Provider Binding - -Each ACME/DNS combination is supported **at the CA level**, meaning that only **one DNS provider** is configured per CA entry in Keyfactor. This ensures a clear and isolated challenge path for each ACME CA connector instance. +🔒 Per-Domain DNS Provider Resolution -If you need to support multiple DNS zones/providers (e.g., both AWS and Cloudflare), configure **separate CA entries**, each with its own DNS provider configuration. +You can configure **multiple DNS provider plugins** and the Gateway selects the appropriate one for each domain based on your **Domain Validation** configuration (matching a configured, optionally wildcard, domain pattern). This also means a single certificate with SANs across different zones/providers can be validated using different plugins, and CNAME-delegated challenges are routed to the plugin that owns the delegation target's zone (see **CNAME Delegation** above).
@@ -523,13 +472,9 @@ If `AccountStoragePath` is not set and `%APPDATA%` is unavailable, the plugin de
-🌐 Google Cloud DNS in Containers - -For Google Cloud DNS in container environments, you have three authentication options: +🌐 DNS Provider Authentication in Containers -1. **Workload Identity (GKE)**: No explicit credentials needed; uses pod identity. -2. **JSON key in config**: Paste the service account JSON directly into `Google_ServiceAccountKeyJson`. -3. **Mounted JSON file**: Mount the service account key file and set `Google_ServiceAccountKeyPath`. +DNS provider credentials in containerized environments are handled by each **DNS provider plugin**, not by this ACME plugin. Options such as cloud-native identity (GKE Workload Identity, EKS IRSA, AKS Pod Identity), mounted key files, or config-supplied secrets depend on the provider — see the relevant plugin under the [`-dnsplugin` list](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin) for its supported container authentication methods.
@@ -679,29 +624,8 @@ spec: * **EabKid** - External Account Binding Key ID (optional) * **EabHmacKey** - External Account Binding HMAC key (optional) * **SignerEncryptionPhrase** - Used to encrypt singer information when account is saved to disk (optional) - * **DnsProvider** - DNS Provider to use for ACME DNS-01 challenges (options: Google, Cloudflare, AwsRoute53, Azure, Ns1, Rfc2136, Infoblox) - * **Google_ServiceAccountKeyPath** - Google Cloud DNS: Path to service account JSON key file only if using Google DNS (Optional) - * **Google_ServiceAccountKeyJson** - Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments) - * **Google_ProjectId** - Google Cloud DNS: Project ID only if using Google DNS (Optional) * **AccountStoragePath** - Path for ACME account storage. Defaults to %APPDATA%\AcmeAccounts on Windows or ./AcmeAccounts in containers. - * **Cloudflare_ApiToken** - Cloudflare DNS: API Token only if using Cloudflare DNS (Optional) - * **Azure_ClientId** - Azure DNS: ClientId only if using Azure DNS and Not Managed Itentity in Azure (Optional) - * **Azure_ClientSecret** - Azure DNS: ClientSecret only if using Azure DNS and Not Managed Itentity in Azure (Optional) - * **Azure_SubscriptionId** - Azure DNS: SubscriptionId only if using Azure DNS and Not Managed Itentity in Azure (Optional) - * **Azure_TenantId** - Azure DNS: TenantId only if using Azure DNS and Not Managed Itentity in Azure (Optional) - * **AwsRoute53_AccessKey** - Aws DNS: Access Key only if not using AWS DNS and default AWS Chain Creds on AWS (Optional) - * **AwsRoute53_SecretKey** - Aws DNS: Secret Key only if using AWS DNS and not using default AWS Chain Creds on AWS (Optional) - * **Ns1_ApiKey** - Ns1 DNS: Api Key only if Using Ns1 DNS (Optional) - * **Rfc2136_Server** - RFC 2136 DNS: Server hostname or IP address (Optional) - * **Rfc2136_Port** - RFC 2136 DNS: Server port (default 53) (Optional) - * **Rfc2136_Zone** - RFC 2136 DNS: Zone name (e.g., example.com) (Optional) - * **Rfc2136_TsigKeyName** - RFC 2136 DNS: TSIG key name for authentication (Optional) - * **Rfc2136_TsigKey** - RFC 2136 DNS: TSIG key (base64 encoded) for authentication (Optional) - * **Rfc2136_TsigAlgorithm** - RFC 2136 DNS: TSIG algorithm (default hmac-sha256) (Optional) - * **DnsVerificationServer** - DNS server to use for verifying TXT record propagation. For private/local DNS zones, set this to your authoritative DNS server IP (e.g., 10.3.10.37). Leave empty to use public DNS servers (Google, Cloudflare, etc.). - * **Infoblox_Host** - Infoblox DNS: API URL (e.g., https://infoblox.example.com/wapi/v2.12) only if using Infoblox DNS (Optional) - * **Infoblox_Username** - Infoblox DNS: Username for authentication only if using Infoblox DNS (Optional) - * **Infoblox_Password** - Infoblox DNS: Password for authentication only if using Infoblox DNS (Optional) + * **DnsVerificationServer** - DNS server used to verify TXT record propagation and to resolve CNAME delegation chains. For private/local DNS zones, set this to your authoritative DNS server IP (e.g., 10.3.10.37). Leave empty to use public DNS servers (Google, Cloudflare, etc.). 2. Define [Certificate Profiles](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCP-Gateway.htm) and [Certificate Templates](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm) for the Certificate Authority as required. One Certificate Profile must be defined per Certificate Template. It's recommended that each Certificate Profile be named after the Product ID. The Acme plugin supports the following product IDs: @@ -711,7 +635,7 @@ spec: ## Compatibility -The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2 and later. +The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 26.2 and later. ## License diff --git a/docsource/configuration.md b/docsource/configuration.md index 186f285..9a92d95 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -18,17 +18,14 @@ This plugin has been tested and confirmed to work with the following ACME provid It is designed to be provider-agnostic and should work with any standards-compliant ACME server. -### 🌐 Supported DNS Providers -DNS-01 challenge automation is supported through the following providers: -- **Google Cloud DNS** -- **AWS Route 53** -- **Azure DNS** -- **Cloudflare** -- **NS1** -- **Infoblox** -- **RFC 2136 Dynamic DNS** (BIND with TSIG authentication) - -Additional DNS providers can be added by extending the included `IDnsProvider` interface. +### 🌐 DNS Providers (Pluggable) +DNS-01 challenge automation is handled by **separate, pluggable DNS provider plugins** that are deployed alongside the AnyCA Gateway — they are no longer built into this plugin. The Gateway resolves the appropriate DNS provider plugin per domain at enrollment time. + +For the current list of available DNS provider plugins, see the Keyfactor GitHub organization: + +👉 **[Keyfactor DNS provider plugins (`-dnsplugin`)](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin)** + +Each plugin repository documents its own supported authentication methods and configuration keys. New DNS providers can be added by publishing a new plugin that implements the Gateway's `IDomainValidator` interface — no change to this ACME plugin is required. --- @@ -39,9 +36,10 @@ Additional DNS providers can be added by extending the included `IDnsProvider` i 2. Plugin initializes ACME client and creates a new order. 3. For each domain: a. Retrieve DNS-01 challenge. - b. Use the configured DNS provider to publish challenge record. - c. Wait for DNS propagation and validate record. - d. Notify ACME provider to trigger validation. + b. Resolve any CNAME delegation: follow the CNAME chain from `_acme-challenge.` to its terminal target (see CNAME Delegation below). + c. Select the DNS provider plugin for the zone that owns the (resolved) record name and publish the challenge TXT record there. + d. Wait for DNS propagation and validate record. + e. Notify ACME provider to trigger validation. 4. Once all challenges are valid, finalize the order using CSR. 5. Download the signed certificate from ACME provider. 6. Return PEM certificate to the Gateway. @@ -53,7 +51,7 @@ The plugin uses a modular design that separates ACME communication logic and DNS ## Compatibility -The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2 and later. +The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 26.2 and later. ## Requirements @@ -63,17 +61,15 @@ The Acme AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gatewa This plugin automates DNS-01 challenges using pluggable DNS provider implementations. These providers create and remove TXT records to prove domain control to ACME servers.
-✅ Supported DNS Providers - -| Provider | Auth Methods Supported | Config Keys Required | -|--------------|-----------------------------------------------|--------------------------------------------------------| -| Google DNS | Service Account Key (file or JSON), or ADC | `Google_ServiceAccountKeyPath`, `Google_ServiceAccountKeyJson`, `Google_ProjectId` | -| AWS Route 53 | Access Key/Secret or IAM Role | `AwsRoute53_AccessKey`, `AwsRoute53_SecretKey` | -| Azure DNS | Client Secret or Managed Identity | `Azure_TenantId`, `Azure_ClientId`, `Azure_ClientSecret`, `Azure_SubscriptionId` | -| Cloudflare | API Token | `Cloudflare_ApiToken` | -| NS1 | API Key | `Ns1_ApiKey` | -| Infoblox | Username/Password (Basic Auth) | `Infoblox_Host`, `Infoblox_Username`, `Infoblox_Password` | -| RFC 2136 | TSIG Key (BIND) | `Rfc2136_Server`, `Rfc2136_Zone`, `Rfc2136_TsigKeyName`, `Rfc2136_TsigKey` | +🔌 Available DNS Provider Plugins + +DNS providers are distributed as **standalone plugins**, each in its own repository, and are deployed alongside the AnyCA Gateway rather than bundled into this ACME plugin. This lets you add or upgrade a DNS provider without rebuilding the ACME plugin. + +For the current, authoritative list of available DNS provider plugins, query the Keyfactor GitHub organization: + +👉 **[github.com/orgs/Keyfactor/repositories?q=-dnsplugin](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin)** + +Each plugin's own repository is the source of truth for its supported authentication methods, required configuration keys, and setup instructions. Configure the DNS provider(s) through the AnyCA Gateway's **Domain Validation** configuration; the Gateway resolves the correct plugin per domain at enrollment time.
@@ -87,134 +83,86 @@ This logic is handled by the `DnsVerificationHelper` class and ensures a high-co
-🔑 Credential Flow +🔗 CNAME Delegation (Proxy) Lookup -Each provider supports multiple credential strategies: +Many organizations do not want ACME automation to hold write access to their production DNS zone. The industry-standard pattern is to **delegate just the ACME challenge name** to a separate, isolated validation zone using a `CNAME` record. The plugin supports this transparently. -- **Google DNS**: - - ✅ **Service Account Key File** (via `Google_ServiceAccountKeyPath`) - - ✅ **Service Account Key JSON** (via `Google_ServiceAccountKeyJson` - paste JSON directly) - - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth) +#### Why delegate? -- **AWS Route 53**: - - ✅ **Access/Secret Keys** (`AwsRoute53_AccessKey`, `AwsRoute53_SecretKey`) - - ✅ **IAM Role via EC2 Instance Metadata** (no explicit credentials) +A `CNAME` at `_acme-challenge.` points challenge validation at another zone. ACME automation then only needs write access to that isolated zone — never the production zone. A `CNAME` also cannot coexist with any other record type at the same name (RFC 1034), so the TXT record **must** be created at the CNAME's target, not at the original challenge name. -- **Azure DNS**: - - ✅ **Client Secret** (explicit `TenantId`, `ClientId`, `ClientSecret`) - - ✅ **Managed Identity** or environment-based credentials via `DefaultAzureCredential` +#### How the plugin resolves it -- **Cloudflare**: - - ✅ **Bearer API Token** for zone-level DNS control +Before publishing the challenge record, the plugin runs the `CnameResolver`, which: -- **NS1**: - - ✅ **API Key** passed in header `X-NSONE-Key` - -- **Infoblox**: - - ✅ **Username/Password** (Basic Auth via WAPI REST API) - - Optional: `Infoblox_WapiVersion` (defaults to `2.12`) - - Optional: `Infoblox_IgnoreSslErrors` for self-signed certificates - -- **RFC 2136 (BIND)**: - - ✅ **TSIG Key** for secure dynamic DNS updates - - Supports algorithms: `hmac-md5`, `hmac-sha1`, `hmac-sha256`, `hmac-sha384`, `hmac-sha512` - - Default algorithm: `hmac-sha256` (recommended) - - Optional: `Rfc2136_Port` (defaults to `53`) - -
- -
-đŸĸ On-Premise DNS (RFC 2136) +1. Issues a DNS `CNAME` query for `_acme-challenge.`. +2. **Follows the chain to its terminus.** Delegation can be nested multiple levels deep (`A → B → C → â€Ļ`); the resolver re-queries at each hop and stops only when it reaches a name that has no further `CNAME`. That terminal name is where the TXT record is created. +3. Returns the original name unchanged when **no** `CNAME` exists — so non-delegated domains behave exactly as before (fully backwards compatible). -The RFC 2136 provider enables ACME DNS-01 challenges with on-premise DNS servers that support dynamic updates, including: +Safety guards: the resolver detects loops (a name that reappears in the chain) and enforces a maximum depth of **10 hops**, logging a warning and stopping at the last good name rather than looping forever. -- **BIND** (Berkeley Internet Name Domain) -- **PowerDNS** (with dynamic update support) -- Any DNS server supporting RFC 2136 with TSIG authentication +#### Provider selection follows the delegation -#### Configuration Requirements +The DNS provider plugin is resolved against the **name where the record actually lands**: -| Field | Description | Required | -|-------|-------------|----------| -| `Rfc2136_Server` | DNS server hostname or IP address | ✅ Yes | -| `Rfc2136_Zone` | DNS zone to update (e.g., `example.com`) | ✅ Yes | -| `Rfc2136_TsigKeyName` | TSIG key name (e.g., `acme-update-key`) | ✅ Yes | -| `Rfc2136_TsigKey` | Base64-encoded TSIG secret key | ✅ Yes | -| `Rfc2136_TsigAlgorithm` | TSIG algorithm (default: `hmac-sha256`) | Optional | -| `Rfc2136_Port` | DNS server port (default: `53`) | Optional | -| `DnsVerificationServer` | DNS server IP for verification (for private zones) | Optional | +- **No delegation** → the provider is selected for the certificate domain (e.g. `www.example.com`). +- **Delegated** → the provider is selected for the **resolved terminal target** (e.g. `abc123.acme-validation.net`). -#### Generating TSIG Keys +This means a challenge delegated into a zone hosted by a *different* DNS provider is routed to the plugin that owns that zone. Propagation checks and cleanup also operate on the resolved name. -**For BIND:** -bash +> â„šī¸ Provider selection uses the AnyCA Gateway's Domain Validation configuration, which matches a configured (optionally wildcard) domain pattern one label at a time. Ensure the **delegation target's zone** is covered by a Domain Validation entry — e.g. a target of `abc123.acme-validation.net` needs a validator whose domain matches `*.acme-validation.net`. -**Generate a TSIG key using tsig-keygen (BIND 9.10+)** -tsig-keygen -a hmac-sha256 acme-update-key +#### Example (multi-level delegation across providers) -**Output example:** -key "acme-update-key" { - algorithm hmac-sha256; - secret "base64encodedkey=="; -}; +```text +Cert domain: www.example.com (production zone, e.g. GoDaddy) +Challenge name: _acme-challenge.www.example.com + +DNS records (static, created once): + _acme-challenge.www.example.com CNAME hop1.example.com (GoDaddy) + hop1.example.com CNAME hop2.example.com (GoDaddy) + hop2.example.com CNAME val.acme-zone.net (points into the validation zone) + +Resolution + placement: + _acme-challenge.www.example.com → hop1 → hop2 → val.acme-zone.net (terminal) + Provider plugin selected for: acme-zone.net (the zone that owns val.acme-zone.net) + TXT record created at: val.acme-zone.net + ACME CA queries _acme-challenge.www.example.com, follows the CNAMEs, finds the TXT ✅ +``` +> â„šī¸ **Private/internal delegation zones:** set `DnsVerificationServer` to your authoritative DNS server IP. The `CnameResolver` honors it for the CNAME lookups; otherwise public resolvers (Google, Cloudflare, Quad9) are used. -#### BIND Configuration Example +
-Add to `named.conf`: +
+🔑 Provider Credentials & Configuration -key "acme-update-key" { - algorithm hmac-sha256; - secret "YourBase64EncodedKeyHere=="; -}; +Authentication methods and required configuration keys are specific to each DNS provider and are **documented in that provider's own plugin repository** — see the [`-dnsplugin` repositories](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin). Credentials and settings are supplied through the AnyCA Gateway's **Domain Validation** configuration for the chosen plugin, not in this ACME plugin's configuration. -zone "example.com" { - type master; - file "/var/named/example.com.zone"; - allow-update { key "acme-update-key"; }; -}; +
+
+đŸĸ On-Premise / Private DNS -> âš ī¸ **Security Note:** TSIG keys should be treated as secrets. Store them securely and use strong keys generated with cryptographically secure random number generators. +On-premise and private DNS support (e.g. RFC 2136 dynamic updates against BIND/PowerDNS with TSIG) is provided by the corresponding DNS provider plugin — see its repository under the [`-dnsplugin` list](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin) for TSIG key generation, server/zone settings, and setup examples. -> âš ī¸ **Private DNS Zones:** For private/local DNS zones (e.g., `.local`), set `DnsVerificationServer` to your authoritative DNS server IP so the plugin can verify TXT record propagation. +> âš ī¸ **Private DNS Zones:** For private/local DNS zones (e.g., `.local`) that are not reachable via public resolvers, set `DnsVerificationServer` to your authoritative DNS server IP. This ACME plugin uses it both to verify TXT record propagation and to resolve CNAME delegation chains.
🧩 Adding New DNS Providers -To add support for new DNS services: +DNS providers are independent plugins, so adding a new one requires **no change to this ACME plugin**. Publish a plugin that implements the AnyCA Gateway's `IDomainValidator` interface (create/cleanup the validation record for a domain), deploy it alongside the Gateway, and configure it under **Domain Validation**. The Gateway will resolve it per domain at enrollment time. -1. Implement the `IDnsProvider` interface: - ```csharp - public interface IDnsProvider - { - Task CreateRecordAsync(string recordName, string txtValue); - Task DeleteRecordAsync(string recordName); - } - ``` - -2. Register the new provider in the `DnsProviderFactory`: - ```csharp - case "yourprovider": - return new YourCustomDnsProvider(config.YourProviderConfigValues...); - ``` - -3. Use zone detection logic similar to `GoogleDnsProvider`, `AzureDnsProvider`, or `Ns1DnsProvider`. - -Each provider is instantiated dynamically based on the `DnsProvider` field in the `AcmeClientConfig`. - -> 🔁 This modular DNS system ensures challenge automation works across cloud providers and is easily extensible. +Use any existing plugin in the [`-dnsplugin` list](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin) as a reference implementation.
-🔒 CA-Level DNS Provider Binding - -Each ACME/DNS combination is supported **at the CA level**, meaning that only **one DNS provider** is configured per CA entry in Keyfactor. This ensures a clear and isolated challenge path for each ACME CA connector instance. +🔒 Per-Domain DNS Provider Resolution -If you need to support multiple DNS zones/providers (e.g., both AWS and Cloudflare), configure **separate CA entries**, each with its own DNS provider configuration. +You can configure **multiple DNS provider plugins** and the Gateway selects the appropriate one for each domain based on your **Domain Validation** configuration (matching a configured, optionally wildcard, domain pattern). This also means a single certificate with SANs across different zones/providers can be validated using different plugins, and CNAME-delegated challenges are routed to the plugin that owns the delegation target's zone (see **CNAME Delegation** above).
@@ -486,13 +434,9 @@ If `AccountStoragePath` is not set and `%APPDATA%` is unavailable, the plugin de
-🌐 Google Cloud DNS in Containers - -For Google Cloud DNS in container environments, you have three authentication options: +🌐 DNS Provider Authentication in Containers -1. **Workload Identity (GKE)**: No explicit credentials needed; uses pod identity. -2. **JSON key in config**: Paste the service account JSON directly into `Google_ServiceAccountKeyJson`. -3. **Mounted JSON file**: Mount the service account key file and set `Google_ServiceAccountKeyPath`. +DNS provider credentials in containerized environments are handled by each **DNS provider plugin**, not by this ACME plugin. Options such as cloud-native identity (GKE Workload Identity, EKS IRSA, AKS Pod Identity), mounted key files, or config-supplied secrets depend on the provider — see the relevant plugin under the [`-dnsplugin` list](https://github.com/orgs/Keyfactor/repositories?q=-dnsplugin) for its supported container authentication methods.
diff --git a/integration-manifest.json b/integration-manifest.json index 08ba468..62482ca 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -9,7 +9,7 @@ "support_level": "kf-supported", "link_github": true, "update_catalog": true, - "gateway_framework": "24.2", + "gateway_framework": "26.2", "about": { "carest": { "ca_plugin_config": [ @@ -37,97 +37,13 @@ "name": "SignerEncryptionPhrase", "description": "Used to encrypt singer information when account is saved to disk (optional)" }, - { - "name": "DnsProvider", - "description": "DNS Provider to use for ACME DNS-01 challenges (options: Google, Cloudflare, AwsRoute53, Azure, Ns1, Rfc2136, Infoblox)" - }, - { - "name": "Google_ServiceAccountKeyPath", - "description": "Google Cloud DNS: Path to service account JSON key file only if using Google DNS (Optional)" - }, - { - "name": "Google_ServiceAccountKeyJson", - "description": "Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments)" - }, - { - "name": "Google_ProjectId", - "description": "Google Cloud DNS: Project ID only if using Google DNS (Optional)" - }, { "name": "AccountStoragePath", "description": "Path for ACME account storage. Defaults to %APPDATA%\\AcmeAccounts on Windows or ./AcmeAccounts in containers." }, - { - "name": "Cloudflare_ApiToken", - "description": "Cloudflare DNS: API Token only if using Cloudflare DNS (Optional)" - }, - { - "name": "Azure_ClientId", - "description": "Azure DNS: ClientId only if using Azure DNS and Not Managed Itentity in Azure (Optional)" - }, - { - "name": "Azure_ClientSecret", - "description": "Azure DNS: ClientSecret only if using Azure DNS and Not Managed Itentity in Azure (Optional)" - }, - { - "name": "Azure_SubscriptionId", - "description": "Azure DNS: SubscriptionId only if using Azure DNS and Not Managed Itentity in Azure (Optional)" - }, - { - "name": "Azure_TenantId", - "description": "Azure DNS: TenantId only if using Azure DNS and Not Managed Itentity in Azure (Optional)" - }, - { - "name": "AwsRoute53_AccessKey", - "description": "Aws DNS: Access Key only if not using AWS DNS and default AWS Chain Creds on AWS (Optional)" - }, - { - "name": "AwsRoute53_SecretKey", - "description": "Aws DNS: Secret Key only if using AWS DNS and not using default AWS Chain Creds on AWS (Optional)" - }, - { - "name": "Ns1_ApiKey", - "description": "Ns1 DNS: Api Key only if Using Ns1 DNS (Optional)" - }, - { - "name": "Rfc2136_Server", - "description": "RFC 2136 DNS: Server hostname or IP address (Optional)" - }, - { - "name": "Rfc2136_Port", - "description": "RFC 2136 DNS: Server port (default 53) (Optional)" - }, - { - "name": "Rfc2136_Zone", - "description": "RFC 2136 DNS: Zone name (e.g., example.com) (Optional)" - }, - { - "name": "Rfc2136_TsigKeyName", - "description": "RFC 2136 DNS: TSIG key name for authentication (Optional)" - }, - { - "name": "Rfc2136_TsigKey", - "description": "RFC 2136 DNS: TSIG key (base64 encoded) for authentication (Optional)" - }, - { - "name": "Rfc2136_TsigAlgorithm", - "description": "RFC 2136 DNS: TSIG algorithm (default hmac-sha256) (Optional)" - }, { "name": "DnsVerificationServer", - "description": "DNS server to use for verifying TXT record propagation. For private/local DNS zones, set this to your authoritative DNS server IP (e.g., 10.3.10.37). Leave empty to use public DNS servers (Google, Cloudflare, etc.)." - }, - { - "name": "Infoblox_Host", - "description": "Infoblox DNS: API URL (e.g., https://infoblox.example.com/wapi/v2.12) only if using Infoblox DNS (Optional)" - }, - { - "name": "Infoblox_Username", - "description": "Infoblox DNS: Username for authentication only if using Infoblox DNS (Optional)" - }, - { - "name": "Infoblox_Password", - "description": "Infoblox DNS: Password for authentication only if using Infoblox DNS (Optional)" + "description": "DNS server used to verify TXT record propagation and to resolve CNAME delegation chains. For private/local DNS zones, set this to your authoritative DNS server IP (e.g., 10.3.10.37). Leave empty to use public DNS servers (Google, Cloudflare, etc.)." } ], "enrollment_config": [],