
Cloudflare Dns
- 169 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Manage Cloudflare DNS records, zones, proxies, and SSL settings when pointing domains, staging environments, or failover endpoints for production services.
About
The cloudflare-dns skill supports configuring and updating Cloudflare zones and records, including proxied hosts, TLS modes, and environment-specific routing so teams can operate domain changes without breaking production traffic.
- Zone and record management
- Proxy and SSL settings
- Staging cutovers
- API-driven DNS updates
- Safer domain migrations
Cloudflare Dns by the numbers
- 169 all-time installs (skills.sh)
- +5 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #481 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill cloudflare-dnsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 169 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Manage Cloudflare DNS records, zones, proxies, and SSL settings when pointing domains, staging environments, or failover endpoints for production services.
Files
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:
1. Cloudflare Dashboard > My Profile > API Tokens 2. Create Token > Custom token 3. Add permissions above 4. Zone Resources: Specific zones only 5. (Optional) IP filtering for extra security
Environment Setup:
# 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
# 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
# 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
# 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
# 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
# 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
# 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
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)
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: 30sIngress Annotations
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
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
curl -s "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result'Zone Settings
# 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)
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).txtImport (BIND Format)
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
# 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 +shortCommon 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
# 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
1. Scope tokens - Use specific zones, not "All zones" 2. IP filtering - Restrict to known IPs when possible 3. Rotate regularly - Every 90 days for production 4. Store securely - Kubernetes Secrets or Azure Key Vault 5. Audit usage - Check Cloudflare audit logs
Token Rotation
# 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 dashboardRate Limits
Cloudflare API Limits:
- 1,200 requests per 5 minutes (per account)
- 100 requests per 5 minutes (per zone, for some endpoints)
Mitigation:
# 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 pollingAzure Integration
cert-manager with Cloudflare DNS-01
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.comAKS Ingress Configuration
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-cloudflare
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80References
references/api-reference.md- Complete Cloudflare DNS API documentationreferences/azure-integration.md- Azure-specific patterns and configurationsscripts/cloudflare-dns.sh- Helper script for common operations- Cloudflare API Documentation
- External-DNS Cloudflare Tutorial
---
Gotchas
- Rate limits are per-API-token, not per-zone: A noisy External-DNS loop on token X exhausts the 1,200/5min budget for every zone that token touches. Split high-churn zones into a separate token to isolate blast radius.
- Proxied A records return Cloudflare IPs, not yours:
dig +short app.example.comshowing104.x.x.xis correct, not broken. Origin reachability must be tested via Host header or directly against the Azure origin IP. - TTL is ignored when proxied: Setting TTL on an orange-cloud record looks accepted but Cloudflare overrides it with "Auto" (=1). Disable proxy first if you genuinely need a specific TTL (e.g., DNS-01 cert flows).
- External-DNS `txtOwnerId` collisions corrupt records across clusters: Two clusters sharing the same
txtOwnerIdwill fight over ownership TXT records and silently overwrite each other's A records. Always use a unique cluster identifier. - `policy: sync` deletes records External-DNS didn't create: If a manual A record matches a managed hostname pattern, sync mode will delete it during reconciliation. Production must use
upsert-only. - Token verification endpoint returns 200 even with zero permissions:
/user/tokens/verifyconfirms the token exists, not that it has Zone:Read or DNS:Edit. Test by listing actual zones to confirm permissions are scoped correctly.
Cloudflare DNS API Reference
Complete API reference for Cloudflare DNS operations.
Base URL
https://api.cloudflare.com/client/v4Authentication
All requests require authentication:
-H "Authorization: Bearer $CF_API_TOKEN"
-H "Content-Type: application/json"DNS Records Endpoints
List DNS Records
GET /zones/{zone_id}/dns_recordsQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
type | string | Filter by record type (A, AAAA, CNAME, etc.) |
name | string | Filter by record name |
content | string | Filter by record content |
proxied | boolean | Filter by proxy status |
page | integer | Page number (default: 1) |
per_page | integer | Records per page (default: 100, max: 5000) |
order | string | Sort field (type, name, content, ttl, proxied) |
direction | string | Sort direction (asc, desc) |
match | string | Match type: any or all (default: all) |
Example:
# List all A records
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=A&per_page=5000" \
-H "Authorization: Bearer $CF_API_TOKEN"
# Search by name
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=app.example.com" \
-H "Authorization: Bearer $CF_API_TOKEN"Response:
{
"success": true,
"errors": [],
"messages": [],
"result": [
{
"id": "372e67954025e0ba6aaa6d586b9e0b59",
"type": "A",
"name": "app.example.com",
"content": "20.185.100.50",
"proxied": true,
"ttl": 1,
"locked": false,
"zone_id": "023e105f4ecef8ad9ca31a8372d0c353",
"zone_name": "example.com",
"created_on": "2024-01-01T00:00:00.000000Z",
"modified_on": "2024-01-01T00:00:00.000000Z"
}
],
"result_info": {
"page": 1,
"per_page": 100,
"total_pages": 1,
"count": 1,
"total_count": 1
}
}Get DNS Record
GET /zones/{zone_id}/dns_records/{record_id}Create DNS Record
POST /zones/{zone_id}/dns_recordsRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Record type (A, AAAA, CNAME, TXT, MX, etc.) |
name | string | Yes | DNS record name (use @ for root) |
content | string | Yes | Record content (IP, hostname, text) |
ttl | integer | No | TTL in seconds (1 = auto, min 60 for proxied) |
proxied | boolean | No | Whether to proxy through Cloudflare |
priority | integer | No | Priority for MX/SRV records |
comment | string | No | Record comment (visible in dashboard) |
tags | array | No | Record tags (Enterprise only) |
Examples:
# A Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$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,
"comment": "Managed by External-DNS"
}'
# AAAA Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "AAAA",
"name": "app",
"content": "2001:db8::1",
"ttl": 1,
"proxied": true
}'
# CNAME Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$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/$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/$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
}'
# SRV Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "SRV",
"name": "_sip._tcp",
"data": {
"priority": 10,
"weight": 5,
"port": 5060,
"target": "sip.example.com"
},
"ttl": 3600
}'
# CAA Record
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "CAA",
"name": "@",
"data": {
"flags": 0,
"tag": "issue",
"value": "letsencrypt.org"
},
"ttl": 3600
}'Update DNS Record
PUT /zones/{zone_id}/dns_records/{record_id}Full replacement - all fields required:
curl -X PUT "https://api.cloudflare.com/client/v4/zones/$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 DNS Record
PATCH /zones/{zone_id}/dns_records/{record_id}Partial update - only changed fields:
# Change content only
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "20.185.100.60"}'
# Toggle proxy
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"proxied": false}'Delete DNS Record
DELETE /zones/{zone_id}/dns_records/{record_id}curl -X DELETE "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_API_TOKEN"Export DNS Records
GET /zones/{zone_id}/dns_records/exportReturns BIND format zone file:
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/export" \
-H "Authorization: Bearer $CF_API_TOKEN" > zone-export.txtImport DNS Records
POST /zones/{zone_id}/dns_records/importcurl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/import" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-F "file=@zone-import.txt"Zones Endpoints
List Zones
GET /zonesQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | Zone name |
status | string | Zone status (active, pending, etc.) |
account.id | string | Account ID |
page | integer | Page number |
per_page | integer | Results per page |
curl -s "https://api.cloudflare.com/client/v4/zones" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {name, id, status}'Get Zone
GET /zones/{zone_id}Zone Settings
GET /zones/{zone_id}/settings
GET /zones/{zone_id}/settings/{setting_id}
PATCH /zones/{zone_id}/settings/{setting_id}Common Settings:
| Setting ID | Values | Description |
|---|---|---|
ssl | off, flexible, full, strict | SSL/TLS mode |
always_use_https | on, off | HTTPS redirect |
min_tls_version | 1.0, 1.1, 1.2, 1.3 | Minimum TLS |
tls_1_3 | on, off, zrt | TLS 1.3 support |
http2 | on, off | HTTP/2 |
http3 | on, off | HTTP/3 |
websockets | on, off | WebSocket support |
brotli | on, off | Brotli compression |
# Get SSL setting
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ssl" \
-H "Authorization: Bearer $CF_API_TOKEN"
# Set SSL to Full (Strict)
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ssl" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "strict"}'API Tokens Endpoints
Verify Token
GET /user/tokens/verifycurl -s "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer $CF_API_TOKEN"Response:
{
"success": true,
"errors": [],
"messages": [],
"result": {
"id": "token-id",
"status": "active",
"not_before": "2024-01-01T00:00:00Z",
"expires_on": "2024-12-31T23:59:59Z"
}
}Rate Limits
| Endpoint | Limit |
|---|---|
| Global | 1,200 requests / 5 min |
| DNS Records (per zone) | 100 requests / 5 min |
Headers in Response:
X-RateLimit-Limit: 1200
X-RateLimit-Remaining: 1195
X-RateLimit-Reset: 1609459200Error Codes
| Code | Description |
|---|---|
| 1000 | Invalid API key/token |
| 1001 | Invalid zone identifier |
| 1002 | Invalid domain |
| 1003 | Invalid parameter |
| 1004 | Record already exists |
| 1005 | Record not found |
| 1006 | Content required |
| 1007 | Invalid record type |
| 1008 | Invalid TTL |
| 1009 | Invalid priority |
| 9103 | DNS record locked |
| 10000 | Authentication error |
| 81044 | Record already exists |
| 81057 | Record does not exist |
Response Format
All responses follow this structure:
{
"success": true|false,
"errors": [
{
"code": 1001,
"message": "Invalid zone identifier"
}
],
"messages": [
{
"code": 10000,
"message": "Operation completed"
}
],
"result": { ... },
"result_info": {
"page": 1,
"per_page": 100,
"total_pages": 1,
"count": 1,
"total_count": 1
}
}Pagination
For endpoints returning lists:
# Page 1
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?page=1&per_page=100"
# Page 2
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?page=2&per_page=100"Check result_info.total_pages to determine if more pages exist.
Cloudflare DNS - Azure Integration Reference
Patterns and configurations for using Cloudflare DNS with Azure workloads.
Architecture Patterns
Pattern 1: AKS + Ingress-NGINX + Cloudflare
Internet
│
▼
Cloudflare (Proxy/CDN)
│
▼
Azure Load Balancer (Public IP)
│
▼
Ingress-NGINX Controller
│
▼
Kubernetes Services
│
▼
PodsDNS Configuration:
- Type: A Record
- Name: app.example.com
- Content: Azure Load Balancer IP
- Proxy: Enabled (orange cloud)
Pattern 2: Azure App Service + Cloudflare
Internet
│
▼
Cloudflare (Proxy/CDN)
│
▼
Azure App Service
(myapp.azurewebsites.net)DNS Configuration:
- Type: CNAME
- Name: app
- Content: myapp.azurewebsites.net
- Proxy: Enabled (orange cloud)
Pattern 3: Azure Static Web Apps + Cloudflare
Internet
│
▼
Cloudflare (DNS Only)
│
▼
Azure Static Web Apps CDNDNS Configuration:
- Type: CNAME
- Name: www
- Content: nice-beach-123.azurestaticapps.net
- Proxy: Disabled (gray cloud) - Azure handles CDN
Pattern 4: Azure Front Door (Not Recommended)
Avoid combining Cloudflare proxy with Azure Front Door - use one or the other.
If required:
- Type: CNAME
- Proxy: Disabled (gray cloud)
- Let Azure Front Door handle CDN/WAF
External-DNS Configuration
Complete Helm Values for AKS
# values.yaml - External-DNS with Cloudflare for AKS
fullnameOverride: external-dns
provider:
name: cloudflare
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: cloudflare_api_token
# Cloudflare optimizations
extraArgs:
cloudflare-proxied: "true"
cloudflare-dns-records-per-page: "5000"
# Sources
sources:
- service
- ingress
# Domain restrictions
domainFilters:
- example.com
# Ownership tracking
registry: txt
txtOwnerId: "aks-prod-eastus" # UNIQUE per cluster
txtPrefix: "_externaldns."
# Policy
policy: upsert-only # NEVER sync in production
# Sync interval
interval: "5m"
# Logging
logLevel: info
logFormat: json
# Resources
resources:
requests:
memory: "64Mi"
cpu: "25m"
limits:
memory: "128Mi"
# Pod security
securityContext:
runAsNonRoot: true
runAsUser: 65534
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
# Monitoring
serviceMonitor:
enabled: true
interval: 30s
namespace: monitoring
# High availability (optional)
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- external-dns
topologyKey: kubernetes.io/hostnameKubernetes Secret Setup
# Create namespace
kubectl create namespace external-dns
# Create secret from Azure Key Vault
CF_TOKEN=$(az keyvault secret show \
--vault-name "your-keyvault" \
--name "cloudflare-api-token" \
--query value -o tsv)
kubectl create secret generic cloudflare-api-token \
--namespace external-dns \
--from-literal=cloudflare_api_token="$CF_TOKEN"cert-manager Integration
DNS-01 Challenge with Cloudflare
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.comcert-manager Secret
# cert-manager uses different key name
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token="$CF_TOKEN"Complete Ingress with TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
# cert-manager
cert-manager.io/cluster-issuer: letsencrypt-cloudflare
# External-DNS
external-dns.alpha.kubernetes.io/hostname: app.example.com
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
external-dns.alpha.kubernetes.io/ttl: "300"
# NGINX
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80Origin Protection
Ingress-NGINX: Allow Only Cloudflare IPs
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
# Cloudflare IP ranges
# Source: https://www.cloudflare.com/ips/
whitelist-source-range: |
173.245.48.0/20,
103.21.244.0/22,
103.22.200.0/22,
103.31.4.0/22,
141.101.64.0/18,
108.162.192.0/18,
190.93.240.0/20,
188.114.96.0/20,
197.234.240.0/22,
198.41.128.0/17,
162.158.0.0/15,
104.16.0.0/13,
104.24.0.0/14,
172.64.0.0/13,
131.0.72.0/22,
2400:cb00::/32,
2606:4700::/32,
2803:f800::/32,
2405:b500::/32,
2405:8100::/32,
2a06:98c0::/29,
2c0f:f248::/32Azure NSG Rules
# Get Cloudflare IPs
curl -s https://www.cloudflare.com/ips-v4 > cf-ipv4.txt
curl -s https://www.cloudflare.com/ips-v6 > cf-ipv6.txt
# Create NSG rule (example)
az network nsg rule create \
--resource-group MC_rg-aks_aks-cluster_eastus \
--nsg-name aks-agentpool-nsg \
--name AllowCloudflareHTTPS \
--priority 100 \
--source-address-prefixes $(cat cf-ipv4.txt | tr '\n' ' ') \
--destination-port-ranges 443 \
--access Allow \
--protocol TcpSSL/TLS Configuration
Recommended Zone Settings
| Setting | Value | Reason |
|---|---|---|
| SSL Mode | Full (Strict) | Validates origin certificate |
| Always Use HTTPS | On | Force HTTPS |
| Min TLS Version | 1.2 | Security baseline |
| TLS 1.3 | On | Performance & security |
| HSTS | On | Strict transport security |
Origin Certificates
Option 1: Let's Encrypt via cert-manager
- Works with Full (Strict) mode
- Auto-renewal
- Recommended for Kubernetes
Option 2: Cloudflare Origin CA
- 15-year validity
- Only trusted by Cloudflare
- Good for App Service
Multi-Cluster Configuration
Unique txtOwnerId per Cluster
| Cluster | Region | txtOwnerId |
|---|---|---|
| aks-dev | East US | aks-dev-eastus |
| aks-stg | West Europe | aks-stg-westeurope |
| aks-prd | East US | aks-prd-eastus |
ArgoCD ApplicationSet
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: external-dns
namespace: argocd
spec:
generators:
- list:
elements:
- cluster: dev
txtOwnerId: aks-dev-eastus
- cluster: stg
txtOwnerId: aks-stg-westeurope
- cluster: prd
txtOwnerId: aks-prd-eastus
template:
metadata:
name: 'external-dns-{{cluster}}'
spec:
project: infrastructure
sources:
- chart: external-dns
repoURL: https://kubernetes-sigs.github.io/external-dns/
targetRevision: "1.18.0"
helm:
releaseName: external-dns
valueFiles:
- $values/argo-cd-helm-values/kube-addons/external-dns/{{cluster}}/values.yaml
- repoURL: https://your-repo.git
targetRevision: main
ref: values
destination:
server: '{{url}}'
namespace: external-dnsMonitoring & Alerting
Prometheus Metrics
# Sync errors
rate(external_dns_controller_sync_errors_total[5m])
# Records managed
external_dns_registry_endpoints_total
# Last sync time (staleness)
time() - external_dns_controller_last_sync_timestamp_seconds > 600Alert Rules
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: external-dns-cloudflare
namespace: monitoring
spec:
groups:
- name: external-dns
rules:
- alert: ExternalDNSSyncFailed
expr: increase(external_dns_controller_sync_errors_total[5m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: External-DNS sync errors detected
description: External-DNS is experiencing sync errors with Cloudflare
- alert: ExternalDNSStale
expr: time() - external_dns_controller_last_sync_timestamp_seconds > 900
for: 5m
labels:
severity: critical
annotations:
summary: External-DNS has not synced recently
description: External-DNS last sync was over 15 minutes agoTroubleshooting
Check External-DNS Status
# Pods running
kubectl get pods -n external-dns
# Logs
kubectl logs -n external-dns deployment/external-dns -f
# Check for Cloudflare errors
kubectl logs -n external-dns deployment/external-dns | grep -E "(error|cloudflare|401|403|429)"
# Check configuration
kubectl get deployment external-dns -n external-dns -o yaml | grep -A 30 argsVerify DNS Records
# Query Cloudflare DNS
dig @1.1.1.1 app.example.com A
# Check if proxied (Cloudflare IP = proxied)
dig +short app.example.com
# 104.x.x.x = proxied
# Your actual IP = DNS-only
# Check TXT ownership
dig @1.1.1.1 TXT _externaldns.app.example.comCommon Issues
| Issue | Symptom | Solution |
|---|---|---|
| No records created | Ingress exists but no DNS | Check domainFilters, verify annotations |
| Auth failed | 401/403 in logs | Verify CF_API_TOKEN secret |
| Rate limited | 429 errors | Increase interval, use zone-id-filter |
| TXT conflicts | Ownership errors | Ensure unique txtOwnerId per cluster |
| Wrong IP | DNS returns incorrect IP | Check ingress controller external IP |
#!/usr/bin/env bash
#
# Cloudflare DNS Management Script
# Helper script for common DNS operations
#
# Usage:
# ./cloudflare-dns.sh <command> [args]
#
# Commands:
# list-zones List all zones
# list-records [zone_id] List DNS records for a zone
# get-record <zone_id> <name> Get specific record by name
# create-a <zone_id> <name> <ip> [proxied] Create A record
# create-cname <zone_id> <name> <target> [proxied] Create CNAME
# delete-record <zone_id> <record_id> Delete record
# export <zone_id> Export zone to BIND format
# verify-token Verify API token validity
#
# Environment Variables:
# CF_API_TOKEN - Cloudflare API Token (required)
# CF_ZONE_ID - Default zone ID (optional)
#
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Cloudflare API base URL
CF_API="https://api.cloudflare.com/client/v4"
# Check required environment
check_token() {
if [[ -z "${CF_API_TOKEN:-}" ]]; then
echo -e "${RED}Error: CF_API_TOKEN environment variable not set${NC}"
echo "Export your Cloudflare API token:"
echo " export CF_API_TOKEN='your-api-token'"
exit 1
fi
}
# API request helper
cf_api() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
local args=(-s -X "$method")
args+=(-H "Authorization: Bearer $CF_API_TOKEN")
args+=(-H "Content-Type: application/json")
if [[ -n "$data" ]]; then
args+=(-d "$data")
fi
curl "${args[@]}" "${CF_API}${endpoint}"
}
# Print formatted output
print_success() { echo -e "${GREEN}✓${NC} $1"; }
print_error() { echo -e "${RED}✗${NC} $1"; }
print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
print_warn() { echo -e "${YELLOW}⚠${NC} $1"; }
# Verify API token
cmd_verify_token() {
check_token
echo "Verifying API token..."
result=$(cf_api GET "/user/tokens/verify")
if echo "$result" | jq -e '.success == true' > /dev/null 2>&1; then
print_success "Token is valid"
echo "$result" | jq -r '.result | " Status: \(.status)\n Expires: \(.expires_on // "Never")"'
else
print_error "Token verification failed"
echo "$result" | jq -r '.errors[] | " Error: \(.message)"'
exit 1
fi
}
# List all zones
cmd_list_zones() {
check_token
echo "Listing zones..."
cf_api GET "/zones?per_page=50" | jq -r '.result[] | "\(.name)\t\(.id)\t\(.status)"' | \
column -t -s $'\t' -N "ZONE,ID,STATUS"
}
# List DNS records
cmd_list_records() {
check_token
local zone_id="${1:-${CF_ZONE_ID:-}}"
if [[ -z "$zone_id" ]]; then
echo -e "${RED}Error: Zone ID required${NC}"
echo "Usage: $0 list-records <zone_id>"
echo " Or set CF_ZONE_ID environment variable"
exit 1
fi
echo "Listing DNS records for zone: $zone_id"
cf_api GET "/zones/$zone_id/dns_records?per_page=5000" | \
jq -r '.result[] | "\(.type)\t\(.name)\t\(.content)\t\(.proxied)\t\(.ttl)"' | \
column -t -s $'\t' -N "TYPE,NAME,CONTENT,PROXIED,TTL"
}
# Get specific record
cmd_get_record() {
check_token
local zone_id="${1:-}"
local name="${2:-}"
if [[ -z "$zone_id" || -z "$name" ]]; then
echo -e "${RED}Error: Zone ID and record name required${NC}"
echo "Usage: $0 get-record <zone_id> <name>"
exit 1
fi
cf_api GET "/zones/$zone_id/dns_records?name=$name" | jq '.result[]'
}
# Create A record
cmd_create_a() {
check_token
local zone_id="${1:-}"
local name="${2:-}"
local ip="${3:-}"
local proxied="${4:-true}"
if [[ -z "$zone_id" || -z "$name" || -z "$ip" ]]; then
echo -e "${RED}Error: Zone ID, name, and IP required${NC}"
echo "Usage: $0 create-a <zone_id> <name> <ip> [proxied]"
exit 1
fi
echo "Creating A record: $name -> $ip (proxied: $proxied)"
result=$(cf_api POST "/zones/$zone_id/dns_records" "{
\"type\": \"A\",
\"name\": \"$name\",
\"content\": \"$ip\",
\"ttl\": 1,
\"proxied\": $proxied
}")
if echo "$result" | jq -e '.success == true' > /dev/null 2>&1; then
print_success "A record created"
echo "$result" | jq -r '.result | " ID: \(.id)\n Name: \(.name)\n Content: \(.content)"'
else
print_error "Failed to create record"
echo "$result" | jq -r '.errors[] | " Error: \(.message)"'
exit 1
fi
}
# Create CNAME record
cmd_create_cname() {
check_token
local zone_id="${1:-}"
local name="${2:-}"
local target="${3:-}"
local proxied="${4:-true}"
if [[ -z "$zone_id" || -z "$name" || -z "$target" ]]; then
echo -e "${RED}Error: Zone ID, name, and target required${NC}"
echo "Usage: $0 create-cname <zone_id> <name> <target> [proxied]"
exit 1
fi
echo "Creating CNAME record: $name -> $target (proxied: $proxied)"
result=$(cf_api POST "/zones/$zone_id/dns_records" "{
\"type\": \"CNAME\",
\"name\": \"$name\",
\"content\": \"$target\",
\"ttl\": 1,
\"proxied\": $proxied
}")
if echo "$result" | jq -e '.success == true' > /dev/null 2>&1; then
print_success "CNAME record created"
echo "$result" | jq -r '.result | " ID: \(.id)\n Name: \(.name)\n Content: \(.content)"'
else
print_error "Failed to create record"
echo "$result" | jq -r '.errors[] | " Error: \(.message)"'
exit 1
fi
}
# Delete record
cmd_delete_record() {
check_token
local zone_id="${1:-}"
local record_id="${2:-}"
if [[ -z "$zone_id" || -z "$record_id" ]]; then
echo -e "${RED}Error: Zone ID and record ID required${NC}"
echo "Usage: $0 delete-record <zone_id> <record_id>"
exit 1
fi
echo "Deleting record: $record_id"
result=$(cf_api DELETE "/zones/$zone_id/dns_records/$record_id")
if echo "$result" | jq -e '.success == true' > /dev/null 2>&1; then
print_success "Record deleted"
else
print_error "Failed to delete record"
echo "$result" | jq -r '.errors[] | " Error: \(.message)"'
exit 1
fi
}
# Export zone
cmd_export() {
check_token
local zone_id="${1:-${CF_ZONE_ID:-}}"
if [[ -z "$zone_id" ]]; then
echo -e "${RED}Error: Zone ID required${NC}"
echo "Usage: $0 export <zone_id>"
exit 1
fi
local filename="zone-export-$(date +%Y%m%d-%H%M%S).txt"
echo "Exporting zone $zone_id to $filename..."
cf_api GET "/zones/$zone_id/dns_records/export" > "$filename"
if [[ -s "$filename" ]]; then
print_success "Zone exported to $filename"
echo " Records: $(grep -c '^[^;]' "$filename" || echo 0)"
else
print_error "Export failed or zone is empty"
exit 1
fi
}
# Check External-DNS in Kubernetes
cmd_check_external_dns() {
echo "Checking External-DNS status..."
# Check pods
echo -e "\n${BLUE}Pods:${NC}"
kubectl get pods -n external-dns -o wide 2>/dev/null || print_warn "Cannot access external-dns namespace"
# Check recent logs
echo -e "\n${BLUE}Recent logs:${NC}"
kubectl logs -n external-dns deployment/external-dns --tail=20 2>/dev/null || print_warn "Cannot access logs"
# Check for errors
echo -e "\n${BLUE}Errors (last 100 lines):${NC}"
kubectl logs -n external-dns deployment/external-dns --tail=100 2>/dev/null | grep -i error || print_success "No errors found"
}
# DNS verification
cmd_verify_dns() {
local hostname="${1:-}"
if [[ -z "$hostname" ]]; then
echo -e "${RED}Error: Hostname required${NC}"
echo "Usage: $0 verify-dns <hostname>"
exit 1
fi
echo "Verifying DNS for: $hostname"
echo -e "\n${BLUE}A Record (via Cloudflare 1.1.1.1):${NC}"
dig @1.1.1.1 "$hostname" A +short
echo -e "\n${BLUE}AAAA Record:${NC}"
dig @1.1.1.1 "$hostname" AAAA +short
echo -e "\n${BLUE}TXT Ownership Record:${NC}"
dig @1.1.1.1 "_externaldns.$hostname" TXT +short
echo -e "\n${BLUE}Proxy Status:${NC}"
ip=$(dig +short "$hostname" | head -1)
if [[ "$ip" =~ ^104\.|^172\.64\.|^141\.101\. ]]; then
print_success "Proxied through Cloudflare (IP: $ip)"
else
print_info "DNS-only / Direct (IP: $ip)"
fi
}
# Show help
cmd_help() {
cat << 'EOF'
Cloudflare DNS Management Script
Usage:
./cloudflare-dns.sh <command> [arguments]
Commands:
verify-token Verify API token validity
list-zones List all zones
list-records [zone_id] List DNS records
get-record <zone_id> <name> Get specific record
create-a <zone_id> <name> <ip> [proxied] Create A record
create-cname <zone_id> <name> <target> [proxied] Create CNAME
delete-record <zone_id> <record_id> Delete record
export <zone_id> Export zone to BIND format
check-external-dns Check External-DNS in Kubernetes
verify-dns <hostname> Verify DNS resolution
Environment Variables:
CF_API_TOKEN Cloudflare API Token (required)
CF_ZONE_ID Default zone ID (optional)
Examples:
# Set up environment
export CF_API_TOKEN='your-token-here'
export CF_ZONE_ID='your-zone-id'
# Verify token
./cloudflare-dns.sh verify-token
# List zones
./cloudflare-dns.sh list-zones
# List records
./cloudflare-dns.sh list-records
# Create proxied A record
./cloudflare-dns.sh create-a $CF_ZONE_ID app 20.185.100.50 true
# Create DNS-only A record (for mail)
./cloudflare-dns.sh create-a $CF_ZONE_ID mail 20.185.100.51 false
# Verify DNS
./cloudflare-dns.sh verify-dns app.example.com
EOF
}
# Main
main() {
local command="${1:-help}"
shift || true
case "$command" in
verify-token) cmd_verify_token "$@" ;;
list-zones) cmd_list_zones "$@" ;;
list-records) cmd_list_records "$@" ;;
get-record) cmd_get_record "$@" ;;
create-a) cmd_create_a "$@" ;;
create-cname) cmd_create_cname "$@" ;;
delete-record) cmd_delete_record "$@" ;;
export) cmd_export "$@" ;;
check-external-dns) cmd_check_external_dns "$@" ;;
verify-dns) cmd_verify_dns "$@" ;;
help|--help|-h) cmd_help ;;
*)
echo -e "${RED}Unknown command: $command${NC}"
echo "Run '$0 help' for usage"
exit 1
;;
esac
}
main "$@"