- Perl 59.7%
- Shell 40.3%
acdns
acdns - minimalistic DNS for serving ACME DNS-01 requests
TL;DR
Typical setup and required autorizations/skills:
- You host AcDns at the acdns nameserver with a acdns name (e.g.
acdns.foobar.example.com). - The acdns name can be resolved into a acdns public address whose
UDP port 53 can be queried from the Internet (in the picture above,
it's the address where the
dnsbox insideAcDnsis contacted by the remoteACME Server). You are able to set this resolution in the DNS. - You allocate a challenges zone in a domain under your control
(e.g.
acme.foobar.example.com). You are able to set a delegation for that zone to a nameserver of your choice in the DNS. - You need to get certificates for the target domain name (e.g.
target.example.com). You are able to set CNAME records for their respective target challenge name (e.g._acme-challenge.target.example.com) in the DNS.
What you should read next:
-
To install the program suite head to the installation instructions. The installation requires that you know the acdns name (e.g.
acdns.foobar.example.com) and the challenges zone (e.g.acme.foobar.example.com). -
To use the installed program for handling challenges for a target domain name head to the integration instructions.
-
The rest of this document provides a fair overview of the parts, why and how they interact together. It's not that long.
Motivation
The goal of acdns is to support requests of certificates through the DNS-01 variant of the ACME protocol. Strange as it might seem, acdns does not implement the ACME protocol - there's a multitude of them out there, covering several needs in terms of where to run (Linux, Windows), how to install certificates, and so on.
In the DNS-01 variant, the CA proposes one challenge per domain name to be validated (including one for each wildcard, if present), to be served by specific DNS Resource Records. Long story short, this means having some control over the DNS configuration.
To automate the process, an ACME client should be able to manipulate (directly or indirectly) the DNS configuration to inject the requested challenges as TXT Resource Records, and then be able to remove those challenges when they're not needed any more. This might be a problem, because it could mean giving a wide authorization just for manipulating a few specific TXT RRs.
Luckily, the DNS-01 variant supports redirection of DNS requests as
CNAME records. This makes it possible to move the interesting TXT
RRsin a different zone, whose only goal is to support these ACME
workflows and which can be served by an ad-hoc instance of a DNS server.
This is where acdns comes into play.
Design Principles and Architecture
The main design principles below acdns are:
- Separation of duties between different components, each concentrating on a specific task.
- Possibility to limit permissions for each component as much as possible, in terms of need of access to resources.
- Provide ways to set granular permissions about what client is allowed to request certificates for what domain. This aims at restricting the blast in case one of the client hosts is violated.
acdns has the following architecture:
- DNS Server serving DNS RRs required for serving DNS-01 challenge verifications.
- Web Server providing ACME Clients an automation entry point to set/remove challenges as TXT Resource Records served by the DNS Server.
- Resource Records directory providing a way for the Web Server to manipulate the TXT RRs served by the DNS Server
- Authentication/Authorization JSON file allowing to trim how different ACME Clients are allowed to manipulate TXT records for different domains.
Separating the dns and web parts makes it possible to assign different permissions. It might also be possible to provide additional remote manipulation capabilities, like e.g. using SSH instead of HTTP/TLS.
DNS Server
The DNS Server listens on a UDP port for DNS requests. Everything it is allowed to answer about is contained in the directory tree for Resource Records, which it accesses for each query; this is a feature, because the goal of acdns is not to act as a general purpose DNS server, but to provide as much dynamicity as possible to support the ACME challenge mechanism.
In general, the DNS Server will be the most exposed externally; for this reason, the design is such that this component needs extremely restricted permissions:
- The user executing the program needs no administrative permissions.
- The program can bind to high ports. To make it respond to UDP/53, though, some redirection mechanism must be put in place.
- The whole filesystem can be mounted read-only, including temporary directories and the Resource Records directory.
Web Server
The Web Server exposes a REST API for manipulating TXT records as files in the Resource Records directory observed by the DNS server. As such, it needs to have read-write access to that directory.
The Web Server supports a simple authentication/authorization mechanism
that is based on an Authentication/Authorization JSON file, to which it
needs read-only access. As it is currently, it allows to authenticate
traffic from a client hosted in a specific IP address (used as a poor
man's version of a username) when it provides the right Bearer token
for authorization. When it does, this can give it access to manipulation
of TXT records for some domains (not necessarily all of them).
In addition to the main components above, the acdns program also
provides sub-commands for acting on the Resource Records directory and
the Authentication/Authorization file, although it's also possible
to act on them directly when following the instructions in their
respective sections below.
Resource Records directory
This directory is organized in two different layers:
- At the topmost layer, each directory represents domain name. By design it does NOT represent a zone (more on this later).
- Immediately below each domain name directory, there are resource
records sub-directories for different Resource Record types as
uppercase, e.g.
TXTorNS. - Inside each resource records sub-directory, each file ending in
.rr.jsonis a resource record file, saved as a JSON file holding the specific items for the specific resource record type.
For example, a Resource Records directory might be organized as follows:
dns
+- acme.example.com
| +- NS
| +- ns-definition.rr.json
|
+- test01.example.com.acme.example.com
| +- TXT
| +- record01.rr.json
| +- some-other-record.rr.json
|
+- foobar.galook.it.acme.example.com
+- TXT
+- foobar.rr.json
JSON files for resource records definition MUST contain a single JSON
object, with keys compatible with what is needed by the respective
resource record type in Net::DNS. In addition to those fields,
anything starting with a minus character - can be added and will be
later ignored when reading the data for DNS purposes; this is how
expiration of Resource Records is supported, by setting -expire to the
expiration time expressed as a UNIX epoch.
For acdns purposes, only a few record types should be strictly needed:
NS:nsdnamekey with the value representing the nameserver hostnameTXT:txtdatakey with the value of the TXT RR contents
Examples:
$ cat acme.example.com/NS/ns-definition.rr.json
{"nsdname": "acdns.example.com"}
$ cat foobar.galook.it.acme.example.com/TXT/record01.rr.json
{"txtdata": "foo the bar, galook!"}
As already observed, first-layer directories represent domain names
and not zones. This means that setup for sibling domains
foo.example.com and bar.example.com must each have their respective
directory in the first layer, and it's not possible to create an
example.com directory to hold them. This is a simplification that
hinders scaling to a lot of domains, but it should work fine up to a
few thousand domains (after all, scaling is not one of the design
goals).
Authorization (and Authenticatio) JSON file
Authorization data is stored in a JSON file; as anticipated, it only needs to be accessed by the Web Server in read-only mode. The file also technically contains some way to do authentication as well.
The JSON file contains one single object with the following structure:
- The top object's keys are IP addresses to which the authorizations are granted. Each value is an object with further data as explained below.
- the middle level are objects where keys are hashed authorization tokens and values are arrays.
- the lowest level are arrays of domain names.
Example:
{
"10.20.30.40" : {
"$argon2id$v=19$m=32768,t=4,p=187ドルHBV5zY+kqqJOyw/XL0Xg$JB3B2DeTIqo3cQQcsjg6tg" : [
"foo.bar.baz"
]
},
"127.0.0.1" : {
"$argon2id$v=19$m=32768,t=4,p=1$t6HHQy7cTF1kginS71NAgQ$RvrobKy3X3n55qDAfSZ/4g" : [
"another.example.com",
"whatever.example.com"
]
}
}
In both examples the (cleartext) authorization token is foobar,
although the hashing mechanism generated two different strings at the
second level. This is considered a feature and reusing these hashes
makes the solution marginally weaker, especially across different
servers. In general, different tokens SHOULD be generated for
installation in different hosts/accounts.
COPYRIGHT & LICENSE
The contents of this repository are licensed according to the Apache
License 2.0 (see file LICENSE in the project's root directory):
Copyright 2025 by Flavio Poletti (flavio@polettix.it).
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.