Customer & Operator Guide
A multi-tenant, policy-governed authoritative DNS control plane. Everything the console does is an API call; anything the console can do, you can script.
1 · Getting started
Console: /ops/ — sign in with your email address as the username. API: obtain a bearer token from POST /api/v1/auth/login; access tokens last 30 minutes, refresh at POST /api/v1/auth/refresh. Accounts with MFA receive a challenge token and complete it at /auth/mfa/verify.
Tenants
A deployment hosts many tenants (customers). Zones, records, users, roles, policies and audit history belong to exactly one tenant, and the API only ever returns your tenant's objects — isolation is enforced in the data layer, not hidden in the UI. A superuser administers any tenant through the API; the console dashboard stays scoped to the tenant it belongs to.
The three access levels
| Role | Can | Cannot |
|---|---|---|
| Viewer | Read zones, records, policies | Change anything — every write is 403 |
| Record Editor (operator) | Create/update records, create zones, load zone files | Delete records or zones |
| Zone Admin | Everything above + delete records/zones, manage permissions | — |
Roles are granted per zone, so one person can administer one zone and be read-only on another. Denials are explicit (403 with a reason) and audited.
Your first zone
Create example.com. (trailing dot) with a primary nameserver and hostmaster; SOA/NS are written for you and the zone is registered with the DNS server immediately. Add www → A → 203.0.113.10; it is live on port 53 when the call returns 201: dig @<host> www.example.com. Zone deletion requires the zone name as confirmation and the Zone Admin role.
2 · Zones & records
POST /api/v1/zones {"name":"example.com.","primary_ns":"ns1.example.com.","admin_email":"hostmaster.example.com."}
GET /api/v1/zones?limit=50&offset=0
DELETE /api/v1/zones/{id}?confirm=example.com. # Zone Admin
POST /api/v1/zones/{id}/records {"name":"@","record_type":"MX","content":"mail.example.com.","priority":10,"ttl":3600}
PUT/DELETE /api/v1/zones/{id}/records/{record_id}
| Field | Notes |
|---|---|
name | Relative to the zone: www, _dmarc, _sip._tcp; @ for the apex |
record_type | A, AAAA, CNAME, MX, NS, TXT, SRV, CAA, PTR … |
content | Targets fully qualified (mail.example.com.) |
ttl · priority · weight/port · flags/tag | seconds · MX/SRV · SRV · CAA |
example.com.example.com.), CNAME exclusivity is enforced, long TXT strings are split correctly, duplicates are rejected, and policies run on every write. A write is committed to the control plane and pushed to the DNS server as one unit — if the DNS server rejects it, the transaction rolls back. There is no "publish" step and no drift.3 · Loading zone files
Validate first: POST /zones/{id}/import/validate parses the file and evaluates every line against your policies without writing anything — {"summary":{"total":42,"valid":40,"invalid":1,"policy_denied":1}, "lines":[…]}.
curl -X POST https://<host>/api/v1/zones/$ZONE/import/bind -H "Authorization: Bearer $TOKEN" -F "file=@example.com.zone"
# -> {"imported": 40, "errors": [], "records": [...]}
$ORIGIN example.com.
$TTL 3600
@ IN A 203.0.113.10
@ IN MX 10 mail.example.com.
www IN CNAME example.com.
@ IN TXT "v=spf1 mx -all"
_dmarc IN TXT "v=DMARC1; p=reject"
Existing records are skipped (re-import is safe); malformed or policy-refused lines are reported with line numbers while the rest imports; the batch reaches the DNS server in chunked writes that roll back on partial failure; one audit entry summarises the import. SOA lines are ignored. CSV is accepted at /import/csv (name,type,content[,ttl,priority,weight,port]). Synchronous imports cap at 5,000 records; larger files go to /import/bind/async (202 + job_id, poll GET /jobs/{job_id}). Export any zone with GET /zones/{id}/export/bind or /export/csv. Importing needs Record Editor or above plus write permission on the zone.
4 · Users, roles & access
Four layers, all per tenant: users → roles (Zone Admin / Record Editor / Viewer) → groups that carry roles → zone permissions (READ/WRITE/ADMIN for a role, group or user on a specific zone). The console's Users tab creates users with a role attached, assigns or removes roles, and activates or deactivates accounts.
GET /api/v1/roles # find role ids
POST /api/v1/users {"email":"jane@example.com","username":"jane","password":"…","full_name":"Jane","role_ids":["<record-editor>"]}
POST /api/v1/users/{user_id}/roles/{role_id} # or attach later
POST /api/v1/users/{user_id}/groups/{group_id}
GET/POST /api/v1/zones/{zone_id}/permissions
Deactivate leavers (PUT /users/{id} → is_active:false) to keep their audit history attributable. Deactivating a whole tenant refuses all of its logins immediately. Emails are unique platform-wide so a login is never ambiguous. Every denied write is audited with the user, zone and reason.
5 · Policies
Guardrails evaluated on every record create and update. A policy is global (all zones) or scoped to one zone, has a priority, and holds rules; a matching DENY refuses the operation with a reason naming the policy and rule.
| rule_type | parameters | matches when… |
|---|---|---|
hostname_regex | pattern | the record name matches |
value_regex | pattern | the content matches |
ip_range | cidrs, record_types | an A/AAAA address is inside a CIDR |
allowed_types / record_type_filter | types | the type is in the list |
ttl_range | min_ttl, max_ttl | the TTL is inside the range |
max_records | max_per_name, max_per_type | counts are within limits |
conditional_record | if_type, then_allow, require_existing | a prerequisite record exists |
DENY fires when it matches — natural for blacklists (wildcards, private addresses). To enforce a range or whitelist ("TTL ≥ 300", "only these types"), write DENY with "negate": true so it fires on the non-compliant case; the reason is prefixed negated:. A plain ALLOW never blocks. Negation is only applied to genuine verdicts — a rule that does not apply to the record cannot fire by accident.TTL floor/cap: {"rule_type":"ttl_range","action":"deny","parameters":{"min_ttl":300,"max_ttl":86400,"negate":true}}
No wildcards: {"rule_type":"hostname_regex","action":"deny","parameters":{"pattern":"^\\*"}}
No private IPs: {"rule_type":"ip_range","action":"deny","parameters":{"cidrs":["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"],"record_types":["A","AAAA"]}}
Only prod types: {"rule_type":"allowed_types","action":"deny","parameters":{"types":["A","AAAA","CNAME","MX","NS","TXT","SRV","CAA","PTR"],"negate":true}}
MX requires SPF: {"rule_type":"conditional_record","action":"deny","parameters":{"if_type":"TXT","then_allow":["MX"],"require_existing":true,"negate":true}}
→ Policy denied: DENY by ttl_range in 'TTL Enforcement': negated: TTL 60 is below the minimum of 300
Zone-scoped example: a bank zone with no CNAMEs and a one-hour TTL floor — the same CNAME refused there is accepted in any other zone. The console's Policies tab lists every policy with its rules, activates or deactivates one without deleting it, and has a dry-run form that evaluates a hypothetical record (including its TTL) against your policies without writing anything — the same as POST /api/v1/policies/test.
6 · API quick start
TOKEN=$(curl -s -X POST https://<host>/api/v1/auth/login -H 'Content-Type: application/json' \
-d '{"username":"you@example.com","password":"…"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
curl -s https://<host>/api/v1/zones?limit=50 -H "Authorization: Bearer $TOKEN"
curl -s https://<host>/api/v1/records/search?q=mail -H "Authorization: Bearer $TOKEN"
curl -s https://<host>/api/v1/audit/recent -H "Authorization: Bearer $TOKEN"
Errors
JSON envelope with a stable code, message and request_id. Policy refusals are 403 naming the policy.
API keys
POST /api/v1/api-keys with explicit scopes (zones.view, zones.manage, zones.delete, audit.view, …) — least-privilege by default, shown once, sent as X-API-Key: <key>, optional expiry, revocable individually.
Idempotency
Send Idempotency-Key on POSTs; a repeated key returns the original response instead of a duplicate.
Rate limits
Per user and category; x-ratelimit-* headers on every response, 429 + Retry-After when exceeded.
Pagination
{"items","total","limit","offset"}, limit capped at 50.
Swagger
/docs — click Authorize, paste the token, try any endpoint.
7 · Security & operations
Transport & edge
HTTPS only with auto-renewed certificates and HSTS; CSP, frame-deny, nosniff, referrer and permissions policies; host firewall exposes only 22 (key-only, rate-limited), 80/443 and 53.
Identity
Modern password hashing, TOTP MFA with encrypted secrets, short-lived tokens with server-side revocation that fails closed in production, per-account and per-source lockouts on every auth endpoint.
Integrity
Policy engine on every write; tamper-evident, hash-chained audit log that records denials too; control plane and DNS server written atomically with rollback; outbox + reconciler converge any gap.
DNSSEC
Per zone: enable, DS records for your registrar, DS-check, key rotation and monitored rollovers. Private keys wrapped with a dedicated key at rest.
Backups
Encrypted (age) database backups with the key held off-box, plus scheduled restore tests that prove each backup is usable. Zone and tenant exports on demand.
Automation
Health probe with a real DNS query, backup + verified restore, outbox drain (30 s), drift reconcile (15 min), zone integrity checks, audit-chain anchoring, SIEM export, scheduled DNSSEC rollover, retention, certificate renewal — all on timers, observable at /health/ready and /metrics.
Webhooks
Signed deliveries for zone.*, record.*, policy.* and user.* events, with delivery history, redelivery, a test endpoint and zero-downtime secret rotation.
Recommended
MFA for every human; API keys per integration; least-privilege roles; TTL / wildcard / private-IP / allowed-types policies on from day one; audit feed to your SIEM.
KNS DNS Platform · this guide ships with the product at /ops/guide.html · full guides in docs/customer-guide/.