https://github.com/vooon/zoneomatic
DNS API server for self-hosted DynDNS
https://github.com/vooon/zoneomatic
acme-dns dns dnsapi
Last synced: 4 months ago
JSON representation
DNS API server for self-hosted DynDNS
- Host: GitHub
- URL: https://github.com/vooon/zoneomatic
- Owner: vooon
- License: apache-2.0
- Created: 2025-08-06T08:33:12.000Z (12 months ago)
- Default Branch: main
- Last Pushed: 2026-04-04T10:21:23.000Z (4 months ago)
- Last Synced: 2026-04-04T12:27:46.086Z (4 months ago)
- Topics: acme-dns, dns, dnsapi
- Language: Go
- Homepage:
- Size: 179 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
Zone-o-Matic
============
DNS API server for self-hosted DynDNS / ACME.
I use CoreDNS to serve my zones, unfortunately it does not support nsupdate protocol.
It does auto-reload modified zone files, so an external service can update them.
This project aims to provide DDNS API similar to *no-ip.com*,
so existing [ddns-scripts][ddns] can interact with it.
As a secondary feature it also provides API, which [acme-sh][acmesh] can use
to issue TLS certificates using `dns-01` challenge.
It also supports [LEGO HTTP-Request][legohttp] protocol for the same challenge.
You can use OpenWRT package from my feed: [vooon/my-openwrt-feed][owrtpkg].
Quick start
-----------
Start server:
```bash
zoneomatic --htpasswd ./htpasswd --zone ./example.com.zone --listen 0.0.0.0:9999
```
Update DDNS A record:
```bash
curl -u "user:password" \
"http://127.0.0.1:9999/nic/update?hostname=host.example.com&myip=203.0.113.10"
```
Update ACME TXT with `acme-dns` compatible endpoint:
```bash
curl -u "user:password" \
-H "Content-Type: application/json" \
-d '{"subdomain":"host.example.com","txt":"SomeRandomToken"}' \
"http://127.0.0.1:9999/acme/update"
```
Security notes
--------------
- Authentication uses htpasswd entries with bcrypt hashes.
- The server does not terminate TLS by itself; run it behind a reverse proxy with HTTPS.
- If you enable `--accept-proxy`, only expose the service behind a trusted proxy/LB.
Command line options
--------------------
```
Usage: zoneomatic --htpasswd=FILE --zone=FILE,... [flags]
DNS Zone file updater
Flags:
-h, --help Show context-sensitive help.
--listen="localhost:9999" Server listen address ($ZM_LISTEN)
--accept-proxy Accept PROXY protocol ($ZM_ACCEPT_PROXY)
--proxy-header-timeout=10s Timeout for PROXY headers ($ZM_PROXY_HEADER_TIMEOUT)
-p, --htpasswd=FILE Passwords file (bcrypt only) ($ZM_HTPASSWD)
-z, --zone=FILE,... Zone files to update ($ZM_ZONE)
--debug Enable debug logging ($ZM_DEBUG)
```
> [!NOTE]
> API description also available in OpenAPI 3 format on `/swagger`,
> e.g. http://localhost:9999/swagger
PowerDNS-Compatible API
-----------------------
Zone-o-matic exposes a PowerDNS-compatible API subset under `/api/v1`.
It is intended for clients that only need server discovery plus read/update access to existing zones,
such as Proxmox SDN.
Authentication:
- `X-API-Key` must contain base64-encoded `user:password`, using credentials from the htpasswd file.
- Regular HTTP Basic Auth with the same credentials is also accepted.
- The only server id is `localhost`.
Implemented operations:
- `GET /api/v1/servers`
- `GET /api/v1/servers/localhost`
- `GET /api/v1/servers/localhost/zones`
- `GET /api/v1/servers/localhost/zones/{zone_id}`
- `PATCH /api/v1/servers/localhost/zones/{zone_id}`
Notes:
- `PATCH` supports RRSet `REPLACE` and `DELETE` changes.
- Zone operations work on already configured zone files only; creating new zones through the API is not supported.
- Unsupported PowerDNS-compatible endpoints currently return `501 Not Implemented`.
- Other PowerDNS API areas such as config, metadata, export, search, and AXFR retrieval are not implemented.
`X-API-Key` example:
```bash
curl \
-H "X-API-Key: $(printf 'user:password' | base64 -w0)" \
"http://127.0.0.1:9999/api/v1/servers"
```
GET /myip
---------
Return client's IP Address in plain text.
Response status codes:
| Code | Meaning |
|------|---------|
| 200 | Success |
| 500 | Unexpected server error |
GET /nic/update
---------------
Update A/AAAA records.
Required HTTP Headers:
| Name | Req | Description |
|------|-----|-------------|
| Authorization | Yes | HTTP Basic Auth |
Query parameters:
| Name | Req | Description |
|------|-----|-------------|
| hostname | Yes | Record name to update |
| myip | No | IP address to set to A/AAAA |
| myipv6 | No | IPv6 address to set to AAAA |
| offline | No | Not supported |
See also: https://www.noip.com/integrate/request
> [!NOTE]
> If no `myip` nor `myipv6` provided, a client IP would be used.
Response status codes:
| Code | Meaning |
|------|---------|
| 200 | Updated |
| 400 | Bad request (e.g. missing `hostname`, invalid IP) |
| 401 | Unauthorized |
| 404 | Zone not found |
| 500 | Unexpected server error |
POST /acme/update
-----------------
Update ACME DNS TXT records.
Required HTTP Headers:
| Name | Req | Description |
|------|-----|-------------|
| X-Api-User | Yes* | Username from the htpasswd file |
| X-Api-Key | Yes* | Password from the htpasswd file |
| Authorization | Yes* | HTTP Basic Auth, alternative to pair above |
JSON Object fields:
| Name | Req | Description | Example |
|------|-----|-------------|---------|
| subdomain | Yes | Record name without `_acme-challenge.`, *not a UUID* | `foo.example.com` |
| txt | Yes | Validation token content for the TXT record | `SomeRandomToken` |
See also: https://github.com/joohoi/acme-dns
> [!NOTE]
> Original ACME-DNS uses `X-Api-User`/`X-Api-Key` style authentication and typically a
> per-record API key + CNAME alias flow.
> This implementation additionally accepts HTTP Basic Auth for simplicity.
> [!NOTE]
> For `acme.sh` option `ACMEDNS_BASE_URL` should be like that: `https://nsapi.example.com/acme`,
> `ACMEDNS_USERNAME` & `ACMEDNS_PASSWORD` - valid user in htpasswd file,
> `ACMEDNS_SUBDOMAIN` - base domain name for which you are requesting certificate.
Auth examples:
`Authorization: Basic ...` mode:
```bash
curl -u "user:password" \
-H "Content-Type: application/json" \
-d '{"subdomain":"foo.example.com","txt":"SomeRandomToken"}' \
"http://127.0.0.1:9999/acme/update"
```
`X-Api-User`/`X-Api-Key` mode:
```bash
curl \
-H "X-Api-User: user" \
-H "X-Api-Key: password" \
-H "Content-Type: application/json" \
-d '{"subdomain":"foo.example.com","txt":"SomeRandomToken"}' \
"http://127.0.0.1:9999/acme/update"
```
Response status codes:
| Code | Meaning |
|------|---------|
| 200 | Updated |
| 400 | Bad request |
| 401 | Unauthorized |
| 404 | Zone not found |
| 500 | Unexpected server error |
POST /present
-------------
Update ACME DNS TXT record, in LEGO HTTP-request format.
Required HTTP Headers:
| Name | Req | Description |
|------|-----|-------------|
| Authorization | Yes | HTTP Basic Auth |
JSON Object fields:
| Name | Req | Description | Example |
|------|-----|-------------|---------|
| fqdn | Yes | Record name without `_acme-challenge.` | `foo.example.com` |
| value | Yes | Validation token content for the TXT record | `SomeRandomToken` |
See also: https://go-acme.github.io/lego/dns/httpreq/
> [!NOTE]
> Only HTTPREQ_MODE=default is supported
Response status codes:
| Code | Meaning |
|------|---------|
| 200 | Updated |
| 400 | Bad request |
| 401 | Unauthorized |
| 404 | Zone not found |
| 500 | Unexpected server error |
POST /cleanup
-------------
Remove ACME DNS TXT record, in LEGO HTTP-request format.
Required HTTP Headers:
| Name | Req | Description |
|------|-----|-------------|
| Authorization | Yes | HTTP Basic Auth |
JSON Object fields:
| Name | Req | Description | Example |
|------|-----|-------------|---------|
| fqdn | Yes | Record name without `_acme-challenge.` | `foo.example.com` |
| value | No | Validation token content for the TXT record, Ignored | `SomeRandomToken` |
See also: https://go-acme.github.io/lego/dns/httpreq/
Response status codes:
| Code | Meaning |
|------|---------|
| 200 | Updated |
| 400 | Bad request |
| 401 | Unauthorized |
| 404 | Zone not found |
| 500 | Unexpected server error |
POST /zm/update
---------------
Custom Zone-o-matic call.
Allow to update any existing record(s).
Match records by FQDN and type, then each value will be translated to a record.
Required HTTP Headers:
| Name | Req | Description |
|------|-----|-------------|
| Authorization | Yes | HTTP Basic Auth |
JSON Object fields:
| Name | Req | Description | Example |
|------|-----|-------------|---------|
| fqdn | Yes | Record domain name. | `foo.example.com` |
| type | Yes | Record type, case-insensitive. | `NS` |
| values | Yes | List of records values | `["ns1", "ns2"]` |
> [!NOTE]
> `POST /zm/update` updates existing records only. If no matching record exists, it returns an error.
Response status codes:
| Code | Meaning |
|------|---------|
| 200 | Updated |
| 400 | Bad request |
| 401 | Unauthorized |
| 404 | Zone not found |
| 500 | Unexpected server error |
GET /health
-----------
Health check endpoint.
Response status codes:
| Code | Meaning |
|------|---------|
| 200 | Healthy |
dnsfmt behavior
---------------
- Multi-part `TXT` records are kept in parenthesized multiline form.
- `TLSA` records are kept on a single line.
[ddns]: https://openwrt.org/docs/guide-user/services/ddns/client
[acmesh]: https://openwrt.org/docs/guide-user/services/tls/acmesh
[legohttp]: https://go-acme.github.io/lego/dns/httpreq/
[owrtpkg]: https://github.com/vooon/my-openwrt-feed/tree/master/zoneomatic