{"id":51686589,"url":"https://github.com/rescaled/knot-api","last_synced_at":"2026-07-15T21:37:22.576Z","repository":{"id":369761113,"uuid":"1291437330","full_name":"rescaled/knot-api","owner":"rescaled","description":"Lightweight Python REST API wrapper around Knot's Python bindings for building your own DNS management on top of it.","archived":false,"fork":false,"pushed_at":"2026-07-06T19:58:49.000Z","size":31,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-15T21:36:56.208Z","etag":null,"topics":["dns","dns-api","knot","knot-api"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rescaled.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-06T19:57:17.000Z","updated_at":"2026-07-06T20:06:57.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rescaled/knot-api","commit_stats":null,"previous_names":["rescaled/knot-api"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rescaled/knot-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescaled%2Fknot-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescaled%2Fknot-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescaled%2Fknot-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescaled%2Fknot-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rescaled","download_url":"https://codeload.github.com/rescaled/knot-api/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rescaled%2Fknot-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35522129,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-15T02:00:06.706Z","response_time":131,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dns","dns-api","knot","knot-api"],"created_at":"2026-07-15T21:37:21.354Z","updated_at":"2026-07-15T21:37:22.570Z","avatar_url":"https://github.com/rescaled.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# knot-api\n\nThis is a REST API wrapper for managing zones on a **local Knot DNS primary** that runs on\nthe binary LMDB confdb and publishes its zones to secondaries through a **generated catalog zone**.\n\n## How it works\n\n| Operation | What happens                                                                                                                                                                                        |\n|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| **Create** (`PUT`, zone unknown) | validate name → `kzonecheck` the body → write zonefile atomically → *txn:* `conf-set 'zone[X]'` + `conf-set 'zone[X].template'` → commit (zone loads from the file) → report the initial serial     |\n| **Update** (`PUT`, zone known) | validate → atomically replace the zonefile → blocking `zone-reload` - **no config transaction at all**; Knot diffs the file, bumps the serial, notifies secondaries                                 |\n| **Delete** (`DELETE`) | *txn:* `conf-unset 'zone[X]'` → commit → `zone-purge +orphan` (journal, timers, KASP, catalog state) → unlink the zonefile (an orphan purge cannot delete the file - Knot no longer knows its path) |\n\nAll file I/O and validation happen **outside** the transaction window, so a\ntransaction only ever contains two or three control messages. On any failure\nthe transaction is aborted (with a fresh-connection fallback), and a create\nrolls its zonefile back so the filesystem stays consistent with the confdb.\nIf someone else (e.g. an admin in `knotc`) has a transaction open, the API\nretries with backoff and eventually answers `503` + `Retry-After`.\n\nThe catalog zone updates automatically on the primary when member zones are\nadded or removed; secondaries purge de-cataloged zones themselves. The API\nnever has to touch the secondaries.\n\n## Requirements\n\n- Knot DNS **3.x** on the same host (control socket access) - tested with 3.5.x\n- Python **3.12+**\n- `libknot.so.16` and `kzonecheck` available\n- The PyPI `libknot` bindings are pinned `\u003e=3.5,\u003c3.6` with the installed Knot major.minor.\n\n### Required knotd template\n\nZones created by the API get `template: member` (configurable). The template\n**must** contain the settings from [`deploy/member-template.conf`](deploy/member-template.conf):\n\n```yaml\ntemplate:\n  - id: member\n    storage: /var/lib/knot/zones      # must equal KNOT_API_ZONES_DIR\n    file: \"%s.zone\"\n    catalog-role: member\n    catalog-zone: \u003cyour-catalog-zone\u003e.\n    zonefile-load: difference-no-serial\n    zonefile-sync: -1                 # load-bearing, see below\n    journal-content: all              # hard requirement of difference-no-serial\n    serial-policy: unixtime\n```\n\nExplanation:\n\n- `zonefile-load: difference-no-serial` - **Knot owns the SOA serial.**\n  Clients submit zonefiles with any serial; Knot computes an IXFR-friendly\n  diff on each reload and bumps the serial per `serial-policy`. The serial\n  in the on-disk file is intentionally stale; the live serial is in the API\n  response / `zone-status`.\n- `zonefile-sync: -1` - Knot must never flush zones back to disk, otherwise\n  its background flushes race the API's atomic file writes and can silently\n  clobber an update.\n\n## Configuration\n\nVia environment variables or an `.env` file - see [`.env.example`](.env.example).\nThe only required value is `KNOT_API_TOKEN`. Set `KNOT_API_CATALOG_ZONE` so\nthe catalog zone is refused for writes (`403`); a PUT would otherwise flip it\nto the member template.\n\n## Running\n\n```console\n$ python3 -m venv .venv \u0026\u0026 .venv/bin/pip install .\n$ KNOT_API_TOKEN=... .venv/bin/uvicorn --factory knot_api.app:create_app \\\n      --host 127.0.0.1 --port 8080 --workers 1\n```\n\n**`--workers 1` is required.** Zone and transaction locks are in-process;\nmultiple workers would race each other (knotd would still serialize\ntransactions, but ordering guarantees per zone would be gone). One worker\nwith FastAPI's threadpool handles concurrent requests fine: different zones\nproceed in parallel, same-zone requests serialize.\n\nRun as the `knot` user (or a member of the knot group): the process needs the\ncontrol socket (`/run/knot/knot.sock`) and write access to the zones\ndirectory. A hardened systemd unit is in\n[`deploy/knot-api.service`](deploy/knot-api.service); put the environment\nfile at `/etc/knot-api.env` with mode `0600`.\n\nBind to localhost (or put a TLS-terminating reverse proxy in front) - the\nbearer token is plaintext on the wire.\n\n## API\n\nAll endpoints except `GET /v1/healthz` require `Authorization: Bearer \u003ctoken\u003e`.\nZone names are normalized (case, trailing dot); IDNs must be punycode.\n\n```console\n# create (201) or update (200)\n$ curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' \\\n      -d '{\"zonefile\": \"example.com. 3600 SOA ns1.example.com. hostmaster.example.com. 1 86400 900 691200 3600\\nexample.com. 3600 NS ns1.example.com.\\nns1.example.com. 3600 A 192.0.2.1\\n\"}' \\\n      http://127.0.0.1:8080/v1/zones/example.com\n{\"name\":\"example.com\",\"serial\":\"1751500000\",\"knot\":{...},\"created\":true}\n\n$ curl -H \"Authorization: Bearer $TOKEN\" http://127.0.0.1:8080/v1/zones\n$ curl -H \"Authorization: Bearer $TOKEN\" http://127.0.0.1:8080/v1/zones/example.com\n$ curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://127.0.0.1:8080/v1/zones/example.com\n```\n\n| Status | Meaning |\n|---|---|\n| `401` | missing/invalid token |\n| `403` | protected zone (catalog zone, `KNOT_API_PROTECTED_ZONES`) |\n| `404` | zone not configured |\n| `413` | zonefile exceeds `KNOT_API_MAX_ZONEFILE_BYTES` |\n| `422` | invalid zone name or zonefile (`detail` carries the kzonecheck output) |\n| `500` | knotd rejected an operation (`detail` carries the daemon error) |\n| `503` | knotd unreachable, or a foreign config transaction stayed open (`Retry-After: 30`) |\n\nInteractive OpenAPI docs: `http://127.0.0.1:8080/docs`.\n\n## Operations notes\n\n- **Stale transaction**: if a config transaction is ever left open (admin\n  `knotc conf-begin` without commit, crashed tooling), all writes answer\n  `503`. Remediate with `knotc conf-abort`, or set\n  `KNOT_API_ABORT_STALE_TXN_ON_STARTUP=true` on single-operator hosts.\n- **Partial delete**: delete is `conf-unset` → purge → unlink. If the purge\n  fails after the unset, the API answers `500` with the manual remediation\n  (`knotc zone-purge -f \u003czone\u003e. +orphan`); a repeated DELETE returns `404`\n  because the zone is already deconfigured.\n- The response `serial` can be `null` right after a create of a very large\n  zone (the API polls ~5 s for the initial load); check `GET /v1/zones/\u003czone\u003e`.\n\n## Development\n\n```console\n$ .venv/bin/pip install -e . pytest httpx ruff mypy\n$ .venv/bin/pytest          # unit tests, no Knot needed (FakeKnot + fake kzonecheck)\n$ .venv/bin/ruff check src tests\n$ .venv/bin/mypy\n```\n\n### End-to-end harness\n\n`deploy/docker-compose.yml` runs knotd 3.5 (confdb bootstrapped from\n`deploy/knot.conf`, catalog zone `catalog.`) plus the API, sharing the\ncontrol socket and zones directory:\n\n```console\n$ docker compose -f deploy/docker-compose.yml up --build -d\n$ curl -s -H 'Authorization: Bearer e2e-secret' http://127.0.0.1:8080/v1/zones\n$ KNOT_API_E2E=1 .venv/bin/pytest -m integration\n$ docker compose -f deploy/docker-compose.yml down -v\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frescaled%2Fknot-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frescaled%2Fknot-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frescaled%2Fknot-api/lists"}