Skip to content

Repository files navigation

certo

中文文档

DNS TXT record management server for ACME DNS-01 challenges — speaks both the lego httpreq and native acme-dns protocols over the same records, via CNAME delegation.

Online Service:https://dnsall.com — free to use, no deployment required.

Features

  • Two challenge protocols, one record — the lego httpreq provider (/present, /cleanup) and the native acme-dns protocol (/register, /update) both drive the same subdomain/TXT/CNAME
  • acme-dns HTTP storage backend — implements lego's ACME_DNS_STORAGE_BASE_URL so an existing account needs no local JSON; stock file-storage + /register also works
  • CNAME delegation — delegate _acme-challenge records via CNAME, no need to give CA clients access to your primary DNS
  • Multi-user — each user gets isolated domains with unique nanoid subdomains
  • API keys with scopes — global (*), wildcard (*.example.com) or exact scopes; wildcard/global keys auto-create domains on demand, exact keys are update-only
  • Source-IP allow list — optional allowfrom CIDRs per acme-dns registration restrict who may /update
  • Admin APIX-Admin-Key endpoints to manage all users, domains and records
  • Web dashboard — embedded React SPA (single binary), manage domains, view CNAME/TXT records, copy client config
  • SQLite storage — local database files without CGO
  • Multi-arch Dockerlinux/amd64 + linux/arm64

How It Works

sequenceDiagram
participant User
participant Dashboard as certo Dashboard
participant DNS as User's DNS
participant Lego as lego / Traefik
participant Server as certo Server
participant CA as ACME CA
User->>Dashboard: Register & add domain
Dashboard-->>User: CNAME target (e.g. a1b2c3d4.s.dnsall.com)
User->>DNS: Set _acme-challenge.example.com<br/>CNAME a1b2c3d4.s.dnsall.com
Lego->>Server: POST /present {fqdn, value}<br/>Basic Auth (username:api_key)
Server-->>Lego: 200 OK
CA->>DNS: Query _acme-challenge.example.com TXT
DNS-->>CA: CNAME → a1b2c3d4.s.dnsall.com
CA->>Server: Query a1b2c3d4.s.dnsall.com TXT
Server-->>CA: TXT challenge-token
CA-->>Lego: Certificate issued
Lego->>Server: POST /cleanup {fqdn, value}
Loading

Quick Start

Docker Compose

services:
certo:
image: dotns/certo:latestrestart: unless-stoppedports:
- "53:53"
- "53:53/udp"
- "3000:3000"volumes:
- ./data:/app/data

Create data/config.toml:

[general]
listen = "0.0.0.0:53"protocol = "both"domain = "s.dnsall.com"nsname = "s.dnsall.com"nsadmin = "admin.dnsall.com"records = [
"s.dnsall.com. A 1.2.3.4",
"s.dnsall.com. NS s.dnsall.com.",
]
[database]
engine = "sqlite"connection = "data/db/certo.db"
[api]
api_domain = "api.dnsall.com"ip = "0.0.0.0"port = "3000"tls = "none"jwt_secret = "change-me-to-a-random-string"admin_key = "change-me-to-a-secret-key"
[logconfig]
loglevel = "info"logtype = "stdout"logformat = "json"
docker compose up -d

Binary

cd web && npm ci && npx vite build &&cd .. # build the embedded dashboard (web/dist)
CGO_ENABLED=0 go build -o certo .
./certo -c data/config.toml

The dashboard is embedded via //go:embed, so web/dist must exist before go build.

Usage with lego (httpreq)

LEGO_DISABLE_CNAME_SUPPORT=true \
HTTPREQ_ENDPOINT=https://api.dnsall.com \
HTTPREQ_USERNAME=myuser \
HTTPREQ_PASSWORD=<api_key> \
lego --dns httpreq \
--dns.propagation-disable-ans \
--domains example.com \
--domains "*.example.com" \
--email admin@example.com \
--accept-tos run

Traefik

# traefik.ymlcertificatesResolvers:
letsencrypt:
acme:
email: admin@example.comstorage: /data/ssl/acme.jsondnsChallenge:
provider: httpreqpropagation:
disableChecks: true
# docker-compose.ymlservices:
traefik:
environment:
LEGO_DISABLE_CNAME_SUPPORT: "true"HTTPREQ_ENDPOINT: "https://api.dnsall.com"HTTPREQ_USERNAME: "myuser"HTTPREQ_PASSWORD: "<api_key>"

Usage with lego (acme-dns)

certo also speaks the acme-dns protocol over the same records as httpreq. The acme-dns API is unchanged — /update stays on the native root path (ACME_DNS_API_BASE). certo additionally implements lego's HTTP storage backend (ACME_DNS_STORAGE_BASE_URL), which only stores/serves the per-domain account so there is nothing to pre-seed. Point lego at certo with your existing username + API key:

ACME_DNS_API_BASE=https://api.dnsall.com \
ACME_DNS_STORAGE_BASE_URL=https://myuser:<api_key>@api.dnsall.com/acmedns \
lego --dns acme-dns \
--domains example.com \
--domains "*.example.com" \
--email admin@example.com \
--accept-tos run

lego fetches the account (subdomain + credentials) from the storage URL, then sends every TXT update to the native POST /update. The storage URL carries your username:api_key (lego's HTTP storage authenticates only via the URL userinfo). The domain is auto-provisioned on first fetch (within the key's scope) — no manual domain setup. Set the same CNAME as httpreq once: _acme-challenge.example.com CNAME <cname_target> (shown in the dashboard). Once the account is saved, updates work with plain file storage and /update — no storage URL needed (fully compatible with stock acme-dns).

Stock acme-dns (local file storage) also works without any certo account: omit ACME_DNS_STORAGE_BASE_URL and use ACME_DNS_STORAGE_PATH=/path/acme.json. lego then calls POST /register, which anonymously allocates an acme-<nanoid> account bound to a random subdomain, saves it to the file, and prints the one-time CNAME — exactly the upstream acme-dns flow.

Using an existing certo account with acme-dns

An existing account is already a valid acme-dns account — the acme-dns username/password map to your certo username/API key, and the subdomain is the deterministic one certo already uses. Two ways:

  • HTTP storage (nothing to seed): just set ACME_DNS_STORAGE_BASE_URL=https://<username>:<api_key>@<host>/acmedns (as shown above). lego pulls the account and updates.

  • Local file storage: pre-seed the JSON with your account, so lego skips /register and only calls /update:

    {
    "example.com": {
    "username": "<your certo username>",
    "password": "<api_key>",
    "fulldomain": "<subdomain>.<base_domain>",
    "subdomain": "<subdomain>",
    "server_url": "https://<host>"
    }
    }

    Grab the exact object from the dashboard (cname_target = fulldomain) or with curl -u <username>:<api_key> https://<host>/acmedns/example.com — it returns the ready-to-paste account.

Domain creation follows the API-key scope. A key auto-creates a not-yet-registered domain on /present and on acme-dns storage fetch as long as the domain is within the key's scope — a global key (*) for any domain, a scoped key (*.example.com or an exact example.com) only for the domains it covers. Out-of-scope domains return 403. Issue a narrowly-scoped key to limit which domains an automation may create.

API

Public

MethodPathDescription
POST/api/registerRegister {username, password}{token}
POST/api/loginLogin {username, password}{token}
GET/api/infoServer info {provider, version, base_domain, api_domain, capabilities}

Account & Keys (JWT or global key)

MethodPathDescription
GET/api/profileGet username
DELETE/api/profileDelete account and all data
GET/api/keysList API keys
POST/api/keysCreate key {name, scope}
DELETE/api/keys/:idDelete key

Domains & Records (JWT or any key, scope checked)

MethodPathDescription
GET/api/domainsList user's domains
POST/api/domainsAdd domain {domain}
DELETE/api/domains/:domainRemove domain
GET/api/recordsList user's active TXT records

httpreq (Basic Auth: username + api_key)

MethodPathDescription
POST/presentStore TXT record {fqdn, value}
POST/cleanupRemove TXT record {fqdn, value}

acme-dns (lego acme-dns provider + HTTP storage)

Credentials are the certo username + API key — basic-auth in the storage URL, headers for update. Domains are auto-provisioned on first storage fetch.

MethodPathDescription
POST/registerNative acme-dns — anonymously allocate an acme-<nanoid> account + random subdomain → {username, password, fulldomain, subdomain, allowfrom}. Optional body {allowfrom:[CIDR…]} restricts which source IPs may /update.
POST/updateNative acme-dns TXT update {subdomain, txt} (headers X-Api-User/X-Api-Key; txt 43 chars; keeps 2 newest)
GET/acmedns/:domainHTTP storage Fetch (get-or-create) → {username, password, subdomain, fulldomain, server_url}
POST/acmedns/:domainHTTP storage Put (get-or-create); used by lego's register→save flow
GET/acmednsHTTP storage FetchAll — accounts for the authenticated user, keyed by domain

Admin (X-Admin-Key header)

MethodPathDescription
GET/admin/usersList all users
POST/admin/usersCreate user
DELETE/admin/users/:idDelete user and domains
GET/admin/domainsList all domains
POST/admin/domainsAdd domain for user
DELETE/admin/domains/:domainRemove domain
GET/admin/recordsList all TXT records

Configuration

Key settings — see docs/configuration.md for every key, its actual code default, and env overrides.

SectionKeyDescriptionDefault
generaldomainBase domain; records live at <subdomain>.<domain>required
generalnsnameName used in SOA/NS answersrequired
generallistenDNS listen address:53 when empty
generalprotocolboth, udp, tcp (+ 4/6 variants)required
databaseenginesqlite (only supported engine)sqlite
databaseconnectionLocal DB path or file: URLrequired
apiip / portHTTP listen address; PORT env overrides the portrequired
apiapi_domainAPI domain (display only)falls back to general.domain
apitlsnone, cert, letsencrypt, letsencryptstagingnone
apijwt_secretJWT signing keyrandom per start (sessions drop on restart)
apiadmin_keyAdmin API keyempty (admin API disabled)

Documentation

DocumentContents
docs/architecture.mdPackages, request flows, DNS resolution, database schema, identifier formats
docs/configuration.mdEvery config key with its real code default
docs/api.mdFull HTTP API reference, status codes and error codes
docs/protocols.mdhttpreq and acme-dns client setup, incl. using an existing account
docs/deployment.mdDocker/binary deployment, DNS delegation, TLS
docs/development.mdBuild, tests, e2e suite, conventions

Development

just build # vite build + go build -o dist/certo
just test# go test ./pkg/...
just lint # go vet ./... + tsc --noEmitcd tests/e2e && bun test# end-to-end suite (real binary + dig)

Full details, including the web/dist embed requirement, in docs/development.md.

Acknowledgements

DNS server core based on acme-dns.

License

Apache-2.0

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages