Open-source project
acme-dns/acme-dns avatar
acme-dns/acme-dns

acme-dns: a DNS server whose API can only write TXT records

Limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely.

2,832 stars304 forksGoMIT

At a glance

What is it?
Most DNS APIs hand out credentials that can rewrite anything in a zone. acme-dns gives each certificate its own subdomain and an API that can only set one TXT value on it, which is a smaller blast radius than the usual automation approach.
Who is it for?
acme-dns solves a narrow problem and solves it properly: getting a DNS-01 challenge answered without handing a certificate bot credentials that can rewrite your zone. If that is your problem, a self-hosted instance in SQLite behind a CNAME is the whole design, and the three endpoints are small enough to read in one sitting.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 126 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why the credential cannot touch your zone

The README's Why section is the argument for the whole project and it is worth reading rather than paraphrasing. Many DNS servers offer no API at all, which blocks automation. Those that do tend to give the key far too much power, and leaving such keys lying around on random machines is often the price of meaningful process automation.

acme-dns takes the opposite route. Registration returns a unique subdomain and a credential pair, and the subdomain is meant to be the target of an `_acme-challenge` CNAME in a zone you control. The credential can only update the TXT value on that one subdomain. If the key leaks, the damage ceiling is one TXT record on a name that has no other purpose. The README puts the aim as two words, accessibility and security, and links to an EFF deeplinks piece on securing ACME DNS challenge validation for the longer argument.

The registration response shows the shape of it. A POST to `/register` returns a status of 201 and a JSON body containing the subdomain, a fulldomain under an auth zone, a username and a password:

json
{
    "allowfrom": [
        "192.168.100.1/24",
        "1.2.3.4/32",
        "2002:c0a8:2a00::0/40"
    ],
    "fulldomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a.auth.acme-dns.io",
    "password": "htB9mR9DYgcu9bX_afHF62erXaH2TS7bg9KW3F7Z",
    "subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a",
    "username": "c36f50e8-4632-44f0-83fe-e070fef28a10"
}

The `allowfrom` array is the other half of the security model. You can pass CIDR ranges at registration, IPv4 and IPv6 alike, and the server will refuse `/update` requests from outside them. That turns a leaked credential from a problem into a non-event as long as the update host is inside the network you named.

Three endpoints, and the update one is the whole product

The API surface is three routes: `/register`, `/update` and `/health`. There is no delete, no zone file editing, no record listing. That is a deliberate constraint rather than an unfinished implementation.

`/update` takes the subdomain and a TXT value in the body, and authenticates with two headers, `X-Api-User` for the UUIDv4 username from registration and `X-Api-Key` for the password:

json
{
    "subdomain": "8e5700ea-a4bf-41c7-8a77-e990661dcc6a",
    "txt": "___validation_token_received_from_the_ca___"
}

The response echoes the TXT value back with a 200, which is convenient for a client that wants to confirm what is currently published. `/health` returns 200 or is unreachable, so it works as both a readiness and a liveness probe with no payload to parse.

One feature in the list deserves a second look because it is the kind of detail that only appears after someone hit the problem. acme-dns performs a rolling update of two TXT records, so it can answer challenges for a certificate that covers both `yourdomain.tld` and `*.yourdomain.tld`. Both of those challenges point at the same `_acme-challenge` subdomain, and a naive single-value implementation would have one overwrite the other. Any ACME client issuing a certificate with a wildcard alongside an apex name will depend on this.

The rest of the feature list is short: it serves your custom A, AAAA and NS records, it can acquire a certificate for itself over HTTP automatically, and it stores state in either SQLite or PostgreSQL.

Building it and running it as a container

The README's installation path is a source build with Go 1.13 or newer named as the minimum:

bash
git clone https://github.com/joohoi/acme-dns
cd acme-dns
export GOPATH=/tmp/acme-dns
go build

The binary then goes on your path, and configuration is read from `/etc/acme-dns/config.cfg`, from `./config.cfg`, or from a location given with the `-c` flag. The repository ships a `config.cfg` at the root, an `acme-dns.service` unit, and a `Dockerfile` alongside a separate `Dockerfile.release`.

The Docker path is the quicker of the two, and `docker-compose.yml` already has the port set right:

yaml
version: '2'
services:
  acmedns:
    build:
      context: .
      dockerfile: Dockerfile
    image: joohoi/acme-dns:latest
    ports:
      - "443:443"
      - "53:53"
      - "53:53/udp"
      - "80:80"
    volumes:
      - ./config:/etc/acme-dns:ro
      - ./data:/var/lib/acme-dns

Port 53 on both TCP and UDP is the DNS service itself, 80 and 443 are there for the self-signed certificate acquisition and the ACME HTTP path. The two volumes are the interesting part: configuration is mounted read only, while `/var/lib/acme-dns` is writable state, which is where SQLite lives. The `Dockerfile` builds with `CGO_ENABLED=0` from a golang alpine stage and ships an alpine runtime with ca-certificates installed, and it declares the same volume paths.

If you use systemd instead, the README gives seven numbered steps from creating a system user to `systemctl enable acme-dns.service`. The reason for the unprivileged user is specific: adding the `CAP_NET_BIND_SERVICE` capability lets the process bind port 53 without running as root.

Go dependencies reveal what the project actually is

Reading `go.mod` is more informative than the feature list. The module is declared as `github.com/joohoi/acme-dns` and requires Go 1.23.0 with a toolchain line for go1.23.5, which is a long way past the 1.13 the README mentions. Direct dependencies include `github.com/miekg/dns` for the DNS server itself, `github.com/julienschmidt/httprouter` and `github.com/rs/cors` for the HTTP layer, `github.com/google/uuid` for the identifiers, and `go.uber.org/zap` for logging.

Two entries explain features that would otherwise look hand-rolled. `github.com/glebarez/go-sqlite` and `github.com/lib/pq` are the SQLite and PostgreSQL backends respectively, which is exactly the dual-database claim in the feature list. `github.com/caddyserver/certmagic` and `github.com/mholt/acmez/v3` are what make the automatic certificate acquisition work, since acmez is an ACME client library and certmagic handles issuance and renewal.

The file also confirms the testing story is real rather than decorative: `github.com/DATA-DOG/go-sqlmock` and `github.com/erikstmartin/go-testdb` for database tests, `github.com/gavv/httpexpect` for asserting HTTP behaviour, and `github.com/onsi/ginkgo` with gomega in the indirect list. The repository has a `test/` directory and a `run_tests.sh`, and the README carries build, coverage and Go Report Card badges.

There is a naming oddity worth noting because it will confuse anyone following links. The repository now lives under the `acme-dns` organisation, but `go.mod`, the clone URL in the README, the Travis badge and the compose image name all still say `joohoi`. The build works, since Go resolves the module path from `go.mod`, but the documentation has not been fully updated to match the move.

Client tooling, wildcard CNAMEs and the four-step workflow

Using acme-dns is described as a short sequence, and the order matters more than it looks. First, POST to a registration endpoint and receive a subdomain plus credentials. Second, create an `_acme-challenge.domainiwantcertfor.tld.` CNAME in your existing zone pointing at the fulldomain you were given, for example a UUID followed by an auth host. Third, use the credentials to POST challenge values to the server whenever the CA needs to validate. Fourth, put that on a crontab and stop thinking about it.

The second step is where the design meets reality. It requires write access to the real zone's DNS, which is the one piece of dangerous capability the design tries to avoid handing to a bot. It is a one-time action, done by a human, and after that the automation only ever touches the acme-dns side. That is the trade the project is offering, and it is a real reduction in standing privilege even though the one-time step still needs the keys.

There is an official client at `github.com/acme-dns/acme-dns-client`, described as supporting Certbot authentication hooks, and the README links an asciinema recording of it in use. Certbot is the integration most people care about, since its DNS plugins usually want provider API credentials, which is exactly the situation this project exists to avoid.

Against the usual alternative, the comparison is straightforward. A provider API token for Cloudflare, Route 53 or DigitalOcean can update any record in any zone on the account, and scoping that down often is not possible. acme-dns cannot do that at all. In exchange, you give up general DNS management on that server, accept running the authoritative endpoint yourself, and add a CNAME per certificate name, which means a delegation that has to be created once and kept.

Version 2.0 was a refactoring, which is the risk to plan for

The release history is three entries and they all landed within about an hour of each other on 2026-02-05: v2.0, then v2.0.1, then v2.0.2. The v2.0 notes list a refactoring under issue 325, a goreleaser configuration update with a new GitHub action to build a release on version tags, and a readme and changelog update. v2.0.1 added building and pushing a Docker image to Docker Hub when a release is triggered, and v2.0.2 fixed errors in the auto-released Docker images.

That sequence is worth reading carefully rather than as a formality. A major version whose headline change is a refactoring means behaviour was intended to be preserved but was not guaranteed to be, and the two point releases immediately afterwards confirm something did need fixing. The `.goreleaser.yml` file at the repository root is the visible consequence of the second change, and the presence of both `Dockerfile` and `Dockerfile.release` fits a project that now builds images as part of tagging.

The last recorded push to `master` was on 2026-06-08, and the project is not archived, so work is still landing on the branch between releases. With 191 open issues, that backlog is the other thing to weigh: a server that sits on the DNS path for certificate renewals is not a component you want to be reading issue threads about at the moment a renewal fails.

What the repository does not settle is the operational detail. There is a `Vagrantfile` for local development and a `config.cfg`, but the README does not document how to migrate from one database backend to the other, nor what happens to existing registrations when the auth zone changes, nor whether there is a documented backup procedure for the SQLite file. If you are running this for renewals that matter, those are the three questions to answer before you depend on it.

Editorial conclusion

acme-dns solves a narrow problem and solves it properly: getting a DNS-01 challenge answered without handing a certificate bot credentials that can rewrite your zone. If that is your problem, a self-hosted instance in SQLite behind a CNAME is the whole design, and the three endpoints are small enough to read in one sitting. If you need it, run your own, because the README is explicit that pointing a CNAME at somebody else's instance means authorising that instance to obtain a certificate for your name. The version history is the thing to note before adopting: v2.0 arrived in February 2026 as a refactoring, so read `CHANGELOG.md` and pin deliberately. Start with `docker-compose.yml`, which already exposes 53, 80 and 443, then decide whether the public instance is acceptable for your threat model.

Frequently asked questions

How to setup ACME-DNS?

Run an instance, register for a subdomain, and CNAME your own `_acme-challenge` record at it. The build path is `git clone` followed by `go build`, then move the binary into your path and edit `config.cfg`, which the server reads from `/etc/acme-dns/config.cfg`, `./config.cfg`, or a path given with `-c`. The repository also ships a `docker-compose.yml` that exposes ports 53, 80 and 443 and mounts config read only.

Is ACME-DNS safe?

The design limits what a leaked credential can do. Each registration gets its own subdomain and credentials that can only update the TXT value on that subdomain, and `/update` requests can additionally be restricted to CIDR ranges supplied at registration with `allowfrom`. The README also says you are encouraged to run your own instance, because a third-party one is authorised to answer challenges for your name.

Does acme-dns work with wildcard certificates?

Yes, and there is a specific mechanism for it. The feature list describes a rolling update of two TXT records so the server can answer challenges for certificates covering both `yourdomain.tld` and `*.yourdomain.tld`, since both challenges point at the same subdomain. An ACME client that issues both names in one certificate relies on that behaviour.

What database does acme-dns use?

Either SQLite or PostgreSQL, both named in the feature list. The dependencies in `go.mod` back that up: `github.com/glebarez/go-sqlite` and `github.com/lib/pq`. The Docker Compose file mounts `/var/lib/acme-dns` as the writable volume, which is where the SQLite file lives. The README does not document a procedure for moving an installation from one backend to the other.

Official sources

  1. acme-dns/acme-dns on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/acme-dns-acme-dns.svg)](https://hysenlabs.com/projects/acme-dns-acme-dns)