Repository: kubernetes-sigs/external-dns

Issue:
Cloudflare Custom Hostnames can't be used without selecting a Certificate Authority
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!

-->

**What happened**: external-dns fails to add Cloudflare custom hostnames if the current service plan doesn't allow specifying a certificate authority. The operation fails with:
```
time="2025-05-20T17:06:07Z" level=error msg="failed to add custom hostname \"REDACTED\": Certificate Authority selection is only available on an Enterprise plan.  If you're interested in the Enterprise plan,  please fill out this form and someone from our sales team will contact you: https://www.cloudflare.com/plans/enterprise/contact/ (1459)" action=UPDATE record=REDACTED ttl=1 type=CNAME zone=REDACTED
```

**What you expected to happen**: I did not specify a certificate authority anywhere, so I shouldn't run into this error.  
Currently, the `cloudflare-custom-hostnames-certificate-authority` flag has a default value of `google` and doesn't allow an empty value. It should be possible to disable this feature, so that external-dns can be used with Cloudflare Custom Hostnames by everybody.

**How to reproduce it (as minimally and precisely as possible)**: Set up external-dns with the Cloudflare provider (on a non-Enterprise plan), add the `--cloudflare-custom-hostnames` flag, and set the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname` annotation to any arbitrary, valid hostname.

**Anything else we need to know?**:

**Environment**: Kubernetes 1.31, Ubuntu 24.04

- External-DNS version (use `external-dns --version`): v20250514-v0.17.0
- DNS provider: Cloudflare
- Others:


Discussion:
@henryjarend: We can confirm that this is an issue in v0.16.1 as well. I did test creating the custom hostname with a curl command from the cloudflare docs and it did create the custom hostname as expected. We're also not on an enterprise plan so we're stuck in the same situation as the original issue
```bash
curl https://api.cloudflare.com/client/v4/zones/$zoneId/custom_hostnames \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $BearerToken" \
    -d '{
      "hostname": "notaurl.example.com",
      "ssl": {
        "method": "http",
        "type": "dv",
        "cname": "notaurl.example.com",
        "settings": {
          "min_tls_version": "1.2" # not required but what we used
        }
      }
    }'
```

Solution patch:
diff --git a/docs/flags.md b/docs/flags.md
index 867031aa..5c3d7c36 100644
--- a/docs/flags.md
+++ b/docs/flags.md
@@ -92,7 +92,7 @@
 | `--[no-]cloudflare-proxied` | When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled) |
 | `--[no-]cloudflare-custom-hostnames` | When using the Cloudflare provider, specify if the Custom Hostnames feature will be used. Requires "Cloudflare for SaaS" enabled. (default: disabled) |
 | `--cloudflare-custom-hostnames-min-tls-version=1.0` | When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3) |
-| `--cloudflare-custom-hostnames-certificate-authority=google` | When using the Cloudflare provider with the Custom Hostnames, specify which Cerrtificate Authority will be used by default. (default: google, options: google, ssl_com, lets_encrypt) |
+| `--cloudflare-custom-hostnames-certificate-authority=none` | When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none) |
 | `--cloudflare-dns-records-per-page=100` | When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100) |
 | `--cloudflare-region-key=CLOUDFLARE-REGION-KEY` | When using the Cloudflare provider, specify the region (default: earth) |
 | `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') |
diff --git a/docs/monitoring/metrics.md b/docs/monitoring/metrics.md
index ae475291..8d73b202 100644
--- a/docs/monitoring/metrics.md
+++ b/docs/monitoring/metrics.md
@@ -80,6 +80,8 @@ curl https://localhost:7979/metrics
 | http_request_duration_seconds |
 | process_cpu_seconds_total |
 | process_max_fds |
+| process_network_receive_bytes_total |
+| process_network_transmit_bytes_total |
 | process_open_fds |
 | process_resident_memory_bytes |
 | process_start_time_seconds |
diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md
index abfb95ee..3b20e9f6 100644
--- a/docs/tutorials/cloudflare.md
+++ b/docs/tutorials/cloudflare.md
@@ -312,7 +312,7 @@ If not set the value will default to `global`.
 
 ## Setting cloudflare-custom-hostname
 
-Automatic configuration of Cloudflare custom hostnames (using A/CNAME DNS records as custom origin servers) is enabled by the --cloudflare-custom-hostnames flag and the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: <custom hostname>` annotation.
+Automatic configuration of Cloudflare custom hostnames (using A/CNAME DNS records as custom origin servers) is enabled by the `--cloudflare-custom-hostnames` flag and the `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: <custom hostname>` annotation.
 
 Multiple hostnames are supported via a comma-separated list: `external-dns.alpha.kubernetes.io/cloudflare-custom-hostname: <custom hostname 1>,<custom hostname 2>`.
 
@@ -320,6 +320,8 @@ See [Cloudflare for Platforms](https://developers.cloudflare.com/cloudflare-for-
 
 This feature is disabled by default and supports the `--cloudflare-custom-hostnames-min-tls-version` and `--cloudflare-custom-hostnames-certificate-authority` flags.
 
+`--cloudflare-custom-hostnames-certificate-authority` defaults to `none`, which explicitly means no Certificate Authority (CA) is set when using the Cloudflare API. Specifying a custom CA is only possible for enterprise accounts.
+
 The custom hostname DNS must resolve to the Cloudflare DNS record (`external-dns.alpha.kubernetes.io/hostname`) for automatic certificate validation via the HTTP method. It's important to note that the TXT method does not allow automatic validation and is not supported.
 
 Requires [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) product and "SSL and Certificates" API permission.
diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go
index df96f71b..e034c91d 100644
--- a/pkg/apis/externaldns/types.go
+++ b/pkg/apis/externaldns/types.go
@@ -254,7 +254,7 @@ var defaultConfig = &Config{
 	CFAPIEndpoint:               "",
 	CFPassword:                  "",
 	CFUsername:                  "",
-	CloudflareCustomHostnamesCertificateAuthority: "google",
+	CloudflareCustomHostnamesCertificateAuthority: "none",
 	CloudflareCustomHostnames:                     false,
 	CloudflareCustomHostnamesMinTLSVersion:        "1.0",
 	CloudflareDNSRecordsPerPage:                   100,
@@ -538,7 +538,7 @@ func App(cfg *Config) *kingpin.Application {
 	app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
 	app.Flag("cloudflare-custom-hostnames", "When using the Cloudflare provider, specify if the Custom Hostnames feature will be used. Requires \"Cloudflare for SaaS\" enabled. (default: disabled)").BoolVar(&cfg.CloudflareCustomHostnames)
 	app.Flag("cloudflare-custom-hostnames-min-tls-version", "When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3)").Default("1.0").EnumVar(&cfg.CloudflareCustomHostnamesMinTLSVersion, "1.0", "1.1", "1.2", "1.3")
-	app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Cerrtificate Authority will be used by default. (default: google, options: google, ssl_com, lets_encrypt)").Default("google").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt")
+	app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none")
 	app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage)
 	app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the region (default: earth)").StringVar(&cfg.CloudflareRegionKey)
 	app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment)
diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go
index 03898ec1..af827daa 100644
--- a/provider/cloudflare/cloudflare.go
+++ b/provider/cloudflare/cloudflare.go
@@ -810,15 +810,20 @@ func (p *CloudFlareProvider) listCustomHostnamesWithPagination(ctx context.Conte
 }
 
 func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) *cloudflare.CustomHostnameSSL {
-	return &cloudflare.CustomHostnameSSL{
-		Type:                 "dv",
-		Method:               "http",
-		CertificateAuthority: customHostnamesConfig.CertificateAuthority,
-		BundleMethod:         "ubiquitous",
+	ssl := &cloudflare.CustomHostnameSSL{
+		Type:         "dv",
+		Method:       "http",
+		BundleMethod: "ubiquitous",
 		Settings: cloudflare.CustomHostnameSSLSettings{
 			MinTLSVersion: customHostnamesConfig.MinTLSVersion,
 		},
 	}
+	// Set CertificateAuthority if provided
+	// We're not able to set it at all (even with a blank) if you're not on an enterprise plan
+	if customHostnamesConfig.CertificateAuthority != "none" {
+		ssl.CertificateAuthority = customHostnamesConfig.CertificateAuthority
+	}
+	return ssl
 }
 
 func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool {



Test patch:
diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go
index fe13da1a..ebe71a7b 100644
--- a/pkg/apis/externaldns/types_test.go
+++ b/pkg/apis/externaldns/types_test.go
@@ -76,7 +76,7 @@ var (
 		CloudflareProxied:                      false,
 		CloudflareCustomHostnames:              false,
 		CloudflareCustomHostnamesMinTLSVersion: "1.0",
-		CloudflareCustomHostnamesCertificateAuthority: "google",
+		CloudflareCustomHostnamesCertificateAuthority: "none",
 		CloudflareDNSRecordsPerPage:                   100,
 		CloudflareDNSRecordsComment:                   "",
 		CloudflareRegionKey:                           "",
diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go
index f8273a60..0a79647f 100644
--- a/provider/cloudflare/cloudflare_regional_test.go
+++ b/provider/cloudflare/cloudflare_regional_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/cloudflare/cloudflare-go"
 
 	"github.com/stretchr/testify/assert"
+
 	"sigs.k8s.io/external-dns/endpoint"
 )
 
@@ -124,7 +125,7 @@ func Test_regionalHostname(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			p := CloudFlareProvider{RegionKey: tt.args.defaultRegionKey}
 			got := p.regionalHostname(tt.args.endpoint)
-			assert.Equal(t, got, tt.want)
+			assert.Equal(t, tt.want, got)
 		})
 	}
 }