Skip to content

External-DNS Integration

This guide covers integrating the Cloudflare Tunnel Gateway Controller with external-dns for automatic DNS record management.

Overview

The controller sets status.addresses on the Gateway with the tunnel CNAME (TUNNEL_ID.cfargotunnel.com). When external-dns is configured with Gateway API source, it automatically creates DNS records for your HTTPRoute hostnames.

Prerequisites

  • Cloudflare Tunnel Gateway Controller installed
  • external-dns installed and configured for Cloudflare
  • Cloudflare API token with DNS edit permissions

external-dns Configuration

Helm Values

# external-dns values.yaml
provider:
  name: cloudflare

env:
  - name: CF_API_TOKEN
    valueFrom:
      secretKeyRef:
        name: cloudflare-dns-token
        key: api-token

sources:
  - gateway-httproute
  - gateway-grpcroute

extraArgs:
  - --gateway-namespace=cloudflare-tunnel-system
  - --gateway-label-filter=app.kubernetes.io/name=cloudflare-tunnel-gateway-controller

API Token Permissions

The external-dns API token needs:

Scope Permission Access
Zone DNS Edit
Zone Zone Read

Separate Tokens

Use separate API tokens for the tunnel controller (Tunnel Edit) and external-dns (DNS Edit) following the principle of least privilege.

HTTPRoute Annotations

Add annotations to HTTPRoute for DNS configuration:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
  annotations:
    # Enable Cloudflare proxy (orange cloud)
    external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
    # Custom TTL (seconds)
    external-dns.alpha.kubernetes.io/ttl: "300"
spec:
  parentRefs:
    - name: cloudflare-tunnel
      namespace: cloudflare-tunnel-system
  hostnames:
    - app.example.com
  rules:
    - backendRefs:
        - name: my-service
          port: 80

Common Annotations

Annotation Description Example
external-dns.alpha.kubernetes.io/cloudflare-proxied Enable Cloudflare proxy "true"
external-dns.alpha.kubernetes.io/ttl DNS record TTL "300"
external-dns.alpha.kubernetes.io/target Override CNAME target custom.example.com
external-dns.alpha.kubernetes.io/hostname Override hostname override.example.com

How It Works

sequenceDiagram
    participant User
    participant K8s as Kubernetes
    participant Ctrl as Controller
    participant ExtDNS as external-dns
    participant CF as Cloudflare

    User->>K8s: Create Gateway
    K8s->>Ctrl: Watch event (GatewayReconciler)
    Ctrl->>K8s: Set Gateway status.addresses to tunnel CNAME
    User->>K8s: Create HTTPRoute
    K8s->>Ctrl: Watch event (RouteReconciler)
    Ctrl->>CF: Update tunnel config
    Ctrl->>K8s: Update HTTPRoute status
    K8s->>ExtDNS: Watch Gateway/HTTPRoute
    ExtDNS->>CF: Create DNS CNAME record
    Note over CF: app.example.com → TUNNEL_ID.cfargotunnel.com

The two controller writes come from separate reconcilers. GatewayReconciler sets status.addresses when the Gateway is first accepted, while the route reconcilers update the Cloudflare tunnel config and route status when an HTTPRoute or GRPCRoute changes. Creating an HTTPRoute re-enqueues the Gateway only to refresh AttachedRoutes and listener conditions, not to set status.addresses for the first time.

  1. User creates a Gateway; the controller sets Gateway status.addresses to the tunnel CNAME once the Gateway is accepted
  2. User creates an HTTPRoute with hostnames
  3. Controller updates the tunnel configuration in Cloudflare and the HTTPRoute status
  4. external-dns watches the Gateway and HTTPRoute
  5. external-dns creates a CNAME record pointing to the tunnel

Verifying DNS Records

Check that external-dns created the record:

# Check external-dns logs
kubectl logs --selector app.kubernetes.io/name=external-dns

# Query DNS
dig app.example.com CNAME

# Expected output
# app.example.com. 300 IN CNAME abc123.cfargotunnel.com.

Troubleshooting

DNS Record Not Created

  1. Check external-dns logs for errors:
kubectl logs --selector app.kubernetes.io/name=external-dns | grep -i error
  1. Verify Gateway has address set:
kubectl get gateway cloudflare-tunnel --namespace cloudflare-tunnel-system \
  --output jsonpath='{.status.addresses[*].value}'
  1. Verify HTTPRoute is accepted:
kubectl get httproute my-app --output jsonpath='{.status.parents[*].conditions}'

Proxy Status Not Applied

Ensure the annotation is on the HTTPRoute, not the Gateway:

# Correct - annotation on HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  annotations:
    external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"

Multiple Gateways

If you have multiple Gateways, use label filters:

extraArgs:
  - --gateway-label-filter=app.kubernetes.io/instance=my-tunnel

Production Recommendations

  1. Use Cloudflare Proxy - Always set cloudflare-proxied: "true" for DDoS protection and caching

  2. TTL Settings - When using Cloudflare proxy, TTL is managed by Cloudflare (set to "Auto")

  3. Separate Zones - Consider separate DNS zones for different environments (prod, staging)

  4. Policy Mode - Use --policy=sync in production for external-dns to clean up stale records

Example: Complete Setup

external-dns version

Use external-dns v0.21.0 or later. The controller stores Gateway, HTTPRoute, and GRPCRoute resources under the stable gateway.networking.k8s.io/v1 API. external-dns migrated its Gateway API sources to v1 in v0.21.0; older releases query v1alpha2 and find no routes. This matches the minimum version cited in the ListenerSet guide.

---
# external-dns deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: external-dns
spec:
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      containers:
        - name: external-dns
          image: registry.k8s.io/external-dns/external-dns:v0.21.0
          args:
            - --source=gateway-httproute
            - --source=gateway-grpcroute
            - --provider=cloudflare
            - --cloudflare-proxied
            - --policy=sync
            - --gateway-namespace=cloudflare-tunnel-system
          env:
            - name: CF_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: cloudflare-dns-token
                  key: api-token
---
# HTTPRoute with DNS annotations
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
  annotations:
    external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
spec:
  parentRefs:
    - name: cloudflare-tunnel
      namespace: cloudflare-tunnel-system
  hostnames:
    - app.example.com
    - www.example.com
  rules:
    - backendRefs:
        - name: my-service
          port: 80