Commit:
ca917b62635321a15d79f190225b7cff9acc1bc9Source updated: ยท Edit this page
RillDNS management API¶
The first control-plane API manages existing primary zones. It is intentionally bound to 127.0.0.1:8053 and has no remote authentication yet. Access it locally or through an SSH tunnel.
dns-primary is the writable PoC primary. dns-secondary runs with -read-only while
phased primary-to-secondary replication is active, preventing independent API
writes from creating divergent zones.
Cloudflare records (read-only)¶
Configure an explicit zone-name allowlist and a root-owned API token file in
/etc/rilldns/rill-api.env:
RILLDNS_CLOUDFLARE_ZONES=example.com,example.net
RILLDNS_CLOUDFLARE_TOKEN_FILE=/etc/rilldns/cloudflare.token
The token currently needs Cloudflare Zone Read and DNS Read for only those
zones. RillDNS resolves and caches zone IDs; callers provide zone names only.
The authenticated web console can validate and update the allowlist. Committed
changes are revisioned and stored in /var/lib/rilldns/cloudflare-zones.json;
the token remains in its separate credential file.
curl -sS http://127.0.0.1:8053/v1/providers/cloudflare/zones
curl -i http://127.0.0.1:8053/v1/providers/cloudflare/zones/example.com/records
The record response includes a synthetic revision in its ETag header and
JSON body. Use it to preview a complete-RRset change without writing:
curl -sS -X POST \
-H 'If-Match: "revision-from-record-response"' \
-H 'Content-Type: application/json' \
http://127.0.0.1:8053/v1/providers/cloudflare/zones/example.com/plans \
-d '{"changes":[{"action":"upsert","name":"www","type":"A","ttl":300,"records":["192.0.2.10"]}]}'
The plan lists exact record-ID deletes and proposed creates. It supports A,
AAAA, CAA, CNAME, MX, NS, SRV, and TXT records, preserves uniform existing
Cloudflare proxy/comment/tag attributes unless overridden, and rejects stale
revisions, invalid TTLs, duplicate values, and CNAME conflicts. Apply the same
change body at /changes with confirm:true. The server reads and compares the
revision again, submits a minimal Cloudflare batch, verifies the API and
assigned authoritative nameservers, audits the outcome, and attempts a
compensating rollback if API-state verification fails. DNS propagation lag is
reported as dns_verified:false with a warning rather than rolling back an
otherwise API-verified change.
Refresh health¶
The loopback API exposes the last zone-transfer and blocklist refresh results:
HA role, write mode, VIP ownership, peer reachability and latency, replication
freshness, and zone snapshot state are available from GET /v1/status/ha.
When configured, the response also summarizes the peer's role, write mode, VIP
ownership, replication state, zone count, and last snapshot. This endpoint is
also exposed on the read-only metrics listener for peer checks. See
high availability.
Refresh health becomes false after a failed job, when hourly zone data is more than two hours old, when daily blocklist data is more than 26 hours old, or when the earliest transferred DNSSEC signature expires within 12 hours. The metrics endpoint uses Prometheus text format and includes last-success timestamps, blocklist domain count, and earliest RRSIG expiry. The same status response includes the most recent RillDNS primary-versus-secondary differential result; mismatches or a check older than two hours make aggregate refresh health unhealthy.
Query and cache history¶
GET /v1/metrics/history?range=6h returns fixed Prometheus-backed series for
query rate, cache-hit ratio, NXDOMAIN rate, and cache entries. Valid ranges are
1h, 6h, 24h, and 7d. The endpoint does not accept PromQL, metric names,
or arbitrary Prometheus URLs from callers.
GET /v1/query-analytics?range=24h&limit=10&recent=100 proxies the
loopback-only telemetry snapshot for the authenticated UI. It returns the
configured privacy mode, totals, blocked percentage, bounded domain/client/type
rankings, and truncation status. In detailed mode it also returns the newest
bounded recent queries. Valid ranges are 1h, 6h, 24h, and 7d; ranking
limits are 1โ100 and recent-query limits are 0โ1000. The endpoint does not
expose the analytics state file or accept a caller-selected upstream URL.
Blocklist configuration¶
GET /v1/blocklists/config returns the complete HTTPS source list, explicit
allow/deny domains, and an ETag revision. Replace the complete configuration
with PUT and the exact current revision:
curl -sS -X PUT -H "If-Match: \"$revision\"" \
-H 'Content-Type: application/json' \
http://127.0.0.1:8053/v1/blocklists/config \
-d '{
"sources": ["https://example.net/blocklist.txt"],
"allow": ["needed.example"],
"deny": ["ads.example"],
"dry_run": true
}'
Only HTTPS sources are accepted. Inputs are normalized, deduplicated, sorted,
and limited to 32 sources. dry_run: false atomically publishes the three
configuration files. The next scheduled refresh compiles them; operators can
start rilldns-refresh-blocklists.service for immediate application.
Read zones¶
The zone response includes an ETag. Mutations must send that revision through If-Match or expected_revision.
Create or import a zone¶
PUT /v1/zones/{zone} accepts complete RFC 1035 zone text and a primary or
secondary role. Use dry_run: true to validate without publishing.
curl -sS -X PUT -H 'Content-Type: application/json' \
http://127.0.0.1:8053/v1/zones/demo.example \
-d '{"role":"primary","zone_text":"$ORIGIN demo.example.\n@ 300 IN SOA ns.demo.example. hostmaster.demo.example. 2026081201 300 60 86400 60\n@ 300 IN NS ns.demo.example.\nns 300 IN A 192.0.2.53\n"}'
Delete requires the exact current revision:
curl -sS -X DELETE -H "If-Match: \"$revision\"" \
-H 'Content-Type: application/json' \
http://127.0.0.1:8053/v1/zones/demo.example \
-d "{\"expected_revision\":\"$revision\"}"
Create and delete wait for the cache-free authority to reflect the change, then send NOTIFY. Failed publication is rolled back. A NOTIFY failure is returned as a warning because hourly SOA polling remains a replication fallback.
Preview a record change¶
revision=$(curl -sS -D - -o /dev/null \
http://127.0.0.1:8053/v1/zones/example.test/rrsets \
| sed -n 's/^[Ee][Tt][Aa][Gg]: "\([^"]*\)".*/\1/p' \
| tr -d '\r')
curl -sS -X POST \
-H "If-Match: \"$revision\"" \
-H 'Content-Type: application/json' \
-H 'X-RillDNS-Actor: operator' \
http://127.0.0.1:8053/v1/zones/example.test/changes \
-d '{
"dry_run": true,
"changes": [{
"action": "upsert",
"name": "demo",
"type": "A",
"ttl": 300,
"records": ["192.0.2.25"]
}]
}'
Set dry_run to false to commit. A committed batch:
- Checks the expected revision.
- Applies all RRset changes in memory.
- Validates the complete zone.
- Advances the SOA serial once.
- Saves the prior revision under
/var/lib/rilldns/history. - Atomically publishes and synchronizes the zone file.
- Waits for CoreDNS to serve the new serial.
- Restores the prior zone if verification fails.
- Appends an audit event.
Change format¶
Upsert replaces the complete RRset:
{
"action": "upsert",
"name": "www",
"type": "A",
"ttl": 300,
"records": ["192.0.2.10", "192.0.2.11"]
}
Delete removes the complete RRset:
SOA records cannot be changed directly. CNAME coexistence, apex SOA/NS requirements, record syntax, duplicate records, owner boundaries, and Internet class are validated server-side.
Current limitations¶
- Loopback access only; remote OAuth and role-based authorization come later.
- No idempotency-key store yet.
- No rollback endpoint yet, although prior versions are retained.
- Blocklist configuration publication and blocklist compilation are separate; there is not yet an API endpoint to trigger the refresh job.
- Zones with the
secondaryrole reject RRset mutations; lifecycle deletion still requires an exact revision.