Cloudflare DNS Skill
Complete Cloudflare DNS operations via REST API with focus on Azure integration.
Overview
This skill covers Cloudflare DNS management for Azure-hosted workloads, including:
- API token configuration and security
- DNS record management (A, AAAA, CNAME, TXT, MX)
- Proxy settings (orange/gray cloud)
- External-DNS integration for Kubernetes
- Troubleshooting and monitoring
Authentication
API Token (Recommended)
Create scoped API tokens instead of using Global API Key:
Required Permissions:
| Permission | Access | Purpose | |
|---|
| Zone > Zone | Read | List zones | |
| Zone > DNS | Edit | Manage DNS records | |
Create Token:
- Cloudflare Dashboard > My Profile > API Tokens
- Create Token > Custom token
- Add permissions above
- Zone Resources: Specific zones only
- (Optional) IP filtering for extra security
Environment Setup:
bash
# Export for API calls
export CF_API_TOKEN="your-api-token"
export CF_ZONE_ID="your-zone-id"
# Get zone ID
curl -s -X GET "https://api.cloudflare.com/client/v4/zones" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, id}'
Token Verification
bash
# Verify token is valid
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer $CF_API_TOKEN"
Quick Reference
List DNS Records
bash
# All records
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, type, content, proxied}'
# Filter by type
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?type=A" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[]'
# Search by name
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=app.example.com" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[]'
Create DNS Records
bash
# A Record (proxied)
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "A",
"name": "app",
"content": "20.185.100.50",
"ttl": 1,
"proxied": true
}'
# A Record (DNS-only)
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "A",
"name": "mail",
"content": "20.185.100.51",
"ttl": 3600,
"proxied": false
}'
# CNAME Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "CNAME",
"name": "www",
"content": "app.example.com",
"ttl": 1,
"proxied": true
}'
# TXT Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "TXT",
"name": "_dmarc",
"content": "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com",
"ttl": 3600
}'
# MX Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "MX",
"name": "@",
"content": "mail.example.com",
"priority": 10,
"ttl": 3600
}'
Update DNS Records
bash
# Get record ID first
RECORD_ID=$(curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=app.example.com&type=A" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq -r '.result[0].id')
# Update record
curl -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "A",
"name": "app",
"content": "20.185.100.60",
"ttl": 1,
"proxied": true
}'
# Patch (partial update)
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"proxied": false}'
Delete DNS Records
bash
# Get record ID
RECORD_ID=$(curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=old.example.com" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq -r '.result[0].id')
# Delete
curl -X DELETE "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN"
Proxy Settings (Orange/Gray Cloud)
When to Enable Proxy (Orange Cloud)
| Use Case | Proxy | Reason | |
|---|
| Web applications | Yes | CDN, DDoS protection | |
| REST APIs | Yes | Performance, security | |
| Static websites | Yes | Caching, optimization | |
| WebSockets | Yes | Supported with config | |
When to Disable Proxy (Gray Cloud)
| Use Case | Proxy | Reason | |
|---|
| Mail servers (MX) | No | SMTP not supported | |
| SSH access | No | Non-HTTP protocol | |
| FTP servers | No | Non-HTTP protocol | |
| Custom TCP/UDP | No | Only HTTP/HTTPS proxied | |
| VPN endpoints | No | Direct connection needed | |
Toggle Proxy via API
bash
# Enable proxy
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"proxied": true}'
# Disable proxy
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"proxied": false}'
External-DNS Integration
Kubernetes Secret
bash
kubectl create namespace external-dns
kubectl create secret generic cloudflare-api-token \
--namespace external-dns \
--from-literal=cloudflare_api_token="$CF_API_TOKEN"
Helm Values (kubernetes-sigs/external-dns)
yaml
fullnameOverride: external-dns
provider:
name: cloudflare
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: cloudflare_api_token
extraArgs:
cloudflare-proxied: true
cloudflare-dns-records-per-page: 5000
sources:
- service
- ingress
domainFilters:
- example.com
txtOwnerId: "aks-cluster-name" # MUST be unique per cluster
txtPrefix: "_externaldns."
policy: upsert-only # Production: NEVER use sync
interval: "5m"
logLevel: info
logFormat: json
resources:
requests:
memory: "64Mi"
cpu: "25m"
limits:
memory: "128Mi"
serviceMonitor:
enabled: true
interval: 30s
Ingress Annotations
yaml
metadata:
annotations:
# Hostname for External-DNS
external-dns.alpha.kubernetes.io/hostname: "app.example.com"
# Custom TTL
external-dns.alpha.kubernetes.io/ttl: "300"
# Override proxy setting
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
# Multiple hostnames
external-dns.alpha.kubernetes.io/hostname: "app.example.com,www.example.com"
Zone Management
List Zones
bash
curl -s "https://api.cloudflare.com/client/v4/zones" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, id, status, plan: .plan.name}'
Get Zone Details
bash
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result'
Zone Settings
bash
# Get all settings
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {id, value}'
# Get specific setting
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/ssl" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result'
# Update SSL mode
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/settings/ssl" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "full"}'
Export/Import DNS Records
Export (BIND Format)
bash
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/export" \
-H "Authorization: Bearer $CF_API_TOKEN" > dns-backup-$(date +%Y%m%d).txt
Import (BIND Format)
bash
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/import" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-F "file=@dns-backup.txt"
Troubleshooting
DNS Verification
bash
# Query Cloudflare DNS (1.1.1.1)
dig @1.1.1.1 app.example.com A
dig @1.1.1.1 app.example.com AAAA
# Check if proxied (returns Cloudflare IP)
dig +short app.example.com
# Proxied: 104.x.x.x or 172.64.x.x
# DNS-only: Your actual IP
# Check TXT records (External-DNS ownership)
dig @1.1.1.1 TXT _externaldns.app.example.com
# Full trace
dig +trace app.example.com
# Check nameservers
dig NS example.com +short
Common Errors
| Error | Cause | Solution | |
|---|
| 401 Unauthorized | Invalid token | Regenerate API token | |
| 403 Forbidden | Insufficient permissions | Add Zone:Read, DNS:Edit | |
| 429 Rate Limited | Too many requests | Increase interval, use pagination | |
| Record exists | Duplicate | Delete or update existing record | |
External-DNS Logs
bash
# Watch logs
kubectl logs -n external-dns deployment/external-dns -f
# Check for Cloudflare errors
kubectl logs -n external-dns deployment/external-dns | grep -i cloudflare
# Check sync status
kubectl logs -n external-dns deployment/external-dns | grep -i "All records are already up to date"
Security Best Practices
API Token Security
- Scope tokens - Use specific zones, not "All zones"
- IP filtering - Restrict to known IPs when possible
- Rotate regularly - Every 90 days for production
- Store securely - Kubernetes Secrets or Azure Key Vault
- Audit usage - Check Cloudflare audit logs
Token Rotation
bash
# 1. Create new token in Cloudflare dashboard
# 2. Update Kubernetes secret
kubectl create secret generic cloudflare-api-token \
--namespace external-dns \
--from-literal=cloudflare_api_token="NEW_TOKEN" \
--dry-run=client -o yaml | kubectl apply -f -
# 3. Restart External-DNS
kubectl rollout restart deployment external-dns -n external-dns
# 4. Verify
kubectl logs -n external-dns deployment/external-dns | head -20
# 5. Revoke old token in Cloudflare dashboard
Rate Limits
Cloudflare API Limits:
- 1,200 requests per 5 minutes (per account)
- 100 requests per 5 minutes (per zone, for some endpoints)
Mitigation:
yaml
# External-DNS optimizations
extraArgs:
cloudflare-dns-records-per-page: 5000 # Max pagination
zone-id-filter: "specific-zone-id" # Reduce API calls
interval: "10m" # Less frequent polling
Azure Integration
cert-manager with Cloudflare DNS-01
yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-cloudflare
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-cloudflare-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- example.com
AKS Ingress Configuration