Tags: acme, certificates

ACME Service Discovery

Automated Certificate Management Environment (ACME) is a protocol for automated identifer validation certificate issuance. Over the past five years it gained widespread adoption thanks to Let's Encrypt, the first publicly trusted CA that implemented it. ACME is supported by a plethora of server programs and service providers, Let’s Encrypt has now issued over 1 billion certificates and together with the ACME protocol itself is largely responsible for pushing the adoption of TLS from around 50% of page loads five years ago to well over 80% today. This is an amazing result!

So it’s no surprise that the ACME ecosystem is growing. Some other publicly trusted CAs now support the ACME protocol. Enterprise CAs are learning how to speak ACME. This includes Dogtag, and by extension FreeIPA. The upcoming FreeIPA 4.9 release will support ACME (I blogged about this a few months ago).

Having proved itself good for DNS certificates, RFC 8738 introduced supported for IP addresses. Work to support email addresses (for S/MIME), .onion addresses (Tor services), and other identifer types is underway in the IETF acme Working Group. (ACME itself is defined in RFC 8555).

The outcome of all of this is that already today, and increasingly into the future, network environments will often have access to multiple ACME servers. These servers may differ in the kinds of certificates they issue and the validation methods (also called “challenge types”) they support. Also, it is desirable that a client (e.g. a printer or an IoT “thing”) would be able to opportunistically and automatically locate a suitable ACME server to acquire certificates without any operator (human or otherwise) intervention (and Let’s Encrypt or other public ACME servers may not be accessible in some environments).

So, what’s an ACME client to do?

Internet-Draft

I have published an Internet-Draft defining a service discovery protocol for ACME. Internet-Draft is IETF jargon for a work-in-progress document that might one day become an RFC. An outline of how ACME Service Discovery works follows.

ACME Service Discovery is a profile of DNS-based Service Discovery (DNS-SD) (RFC 6763). Given a parent domain, Service Instance Names are listed by the PTR records of _acme-server._tcp.$PARENT. For example, the corp.example. parent domain advertises two service instances called CorpCA and C4A:

$ORIGIN corp.example.

_acme-server._tcp PTR CorpCA._acme-server._tcp
_acme-server._tcp PTR C4A._acme-server._tcp

Each Service Instance Name owns an SRV and TXT record that together describe the location, priority and capabilities of the server, as well as the path to the ACME directory object. Continuing with the example, CorpCA has the higher priority and supports the ip and dns identifer types, whereas C4A has a lower priority and only supports dns identifiers:

$ORIGIN corp.example.

CorpCA._acme-server._tcp SRV 10 0 443 ca
CorpCA._acme-server._tcp TXT "path=/acme" "i=ip,dns"

C4A._acme-server._tcp    SRV 20 0 443 certs4all.example.
C4A._acme-server._tcp    TXT "path=/acme/v2" "i=dns"

ACME clients are assumed to know (or deduce) one or more candidate parent domains. Possible sources for the candidate parent domain(s) are the DNS search domains, host FQDN or Kerberos realm. The client performs ACME Service Discovery on each parent domain, selecting and probing eligible service instances, until they find one that works. The probe step involves constructing a URL from the SRV target and port and TXT path attribute, performing an HTTP GET request for that resource, and checking that the response is a valid ACME directory object. In the example above, the directory URL for CorpCA is https://ca.corp.example/acme.

And that’s the main idea! There’s a fair bit more detail in the Internet-Draft but I won’t belabour it all here.

Enabling ACME Service Discovery in FreeIPA

To enable ACME Service Discovery in a FreeIPA environment using the integrated DNS service, add the PTR, SRV and TXT records for each service instance. This requires a recently merged patch to allow PTR records to be created in arbitrary zones (PTR records were previously limited to .arpa reverse zones). The fix should be included in FreeIPA 4.9 and will also be backported to the 4.8.x branch.

The following DNS records advertise the FreeIPA CA itself:

% ipa dnsrecord-add ipa.local ipa._acme-server._tcp \
    --srv-priority 10 --srv-weight 0 \
    --srv-port 443 --srv-target ipa-ca \
    --txt-rec '"path=/acme/directory" "i=dns"'
  Record name: ipa._acme-server._tcp
  SRV record: 10 0 443 ipa-ca
  TXT record: "path=/acme/directory" "i=dns"

% ipa dnsrecord-add ipa.local _acme-server._tcp \
    --ptr-rec "ipa._acme-server._tcp.ipa.local."
  Record name: _acme-server._tcp
  PTR record: ipa._acme-server._tcp.ipa.local.

The procedure to advertise additional ACME servers is similar.

If the ACME Service Discovery proposal gets traction we would ideally create these records to advertise the FreeIPA CA automatically (when it is enabled).

Certbot plugin

I wrote a Certbot plugin to experiment with service discovery. It lives in a private branch at https://github.com/frasertweedale/certbot/tree/feature/discovery. I will probably submit a pull request soon, to invite feedback about the implementation and the service disovery proposal itself.

To install Certbot and the plugin under ~/.local/ (command output omitted):

# git clone https://github.com/certbot/certbot -b feature/discovery
# cd certbot/certbot
# pip install --user .
# cd ../certbot-discovery
# pip install --user .

Run certbot plugins to verify that the plugin is installed:

# certbot plugins
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* discovery
Description: ACME Service Discovery
Interfaces: IPlugin
Entry point: discovery = certbot_discovery:ACMEServiceDiscovery

* standalone
Description: Spin up a temporary webserver
Interfaces: IAuthenticator, IPlugin
Entry point: standalone = certbot._internal.plugins.standalone:Authenticator

* webroot
Description: Place files in webroot directory
Interfaces: IAuthenticator, IPlugin
Entry point: webroot = certbot._internal.plugins.webroot:Authenticator
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Now register an account with the ACME server. Note the --discovery option:

# certbot --discovery register \
  --email ftweedal@redhat.com \
  --agree-tos --no-eff-email
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Account registered.

If service discovery fails, it will fail silently and use Let’s Encrypt (Certbot’s default). --discovery=force suppresses this fallback behaviour; if service discovery fails Certbot will abort.

Next request the certificate:

# certbot --discovery certonly \
    --domain $(hostname) --standalone
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for f33-0.ipa.local
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/f33-0.ipa.local/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/f33-0.ipa.local/privkey.pem
   Your cert will expire on 2021-02-10. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

We can check that the certificate was issued by the FreeIPA CA, not Let’s Encrypt:

# openssl x509 -issuer -noout  \
    < /etc/letsencrypt/live/f33-0.ipa.local/fullchain.pem
issuer=O = IPA.LOCAL 202011061623, CN = Certificate Authority

You do have to supply the --discovery option to both the register and certonly commands (otherwise certonly will try to use Let’s Encrypt). Fortunately, for renewal (the renew command) Certbot does remember which server issued the certificate, and uses the same server for renewal.

What happens when service discovery fails? I’ll disable the ACME service on the FreeIPA server:

% sudo ipa-acme-manage disable
The ipa-acme-manage command was successful

Then, running certbot register again, this time with --discovery=force to prevent fallback to Let’s Encrypt:

# certbot --discovery=force register \
  --email ftweedal@redhat.com \
  --agree-tos --no-eff-email
usage:
  certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...

Certbot can obtain and install HTTPS/TLS/SSL certificates.  By default,
it will attempt to use a webserver both for obtaining and installing the
certificate.
certbot: error: service discovery failed (see /tmp/tmp6qq8pnks for info)

The log file contains a transcript of the service discovery plugin’s activity:

# cat /tmp/tmp6qq8pnks
[INFO] processing parent domain ipa.local.
[INFO] enumerating service instances for _acme-server._tcp.ipa.local.
[INFO]   found service instances: [<DNS name ipa._acme-server._tcp.ipa.local.>]
[INFO] resolving service instance ipa._acme-server._tcp.ipa.local.
[INFO]   (<DNS IN SRV rdata: 10 0 443 ipa-ca.ipa.local.>, (b'path=/acme/directory', b'i=dns'))
[INFO] eligible service instances:
[INFO]   (<DNS IN SRV rdata: 10 0 443 ipa-ca.ipa.local.>, (b'path=/acme/directory', b'i=dns'))
[INFO] GET https://ipa-ca.ipa.local/acme/directory
[WARNING] failed to reach server: <Response [503]>

We can see that the plugin found the service instance and requested the directory resource, but got a 503 response (as expected). So, when service discovery fails the plugin gives you some useful log output to debug the issue.

The log file is only persisted when service discovery fails, otherwise it is deleted. In the current implementation we cannot write to the “normal” Certbot log file because we don’t know where that is. The discovery plugin is actually doing all its work inside the argument parsing. It feels like a brutal hack but it’s the only way I found (in the limited time I had) to override the --server option whilst keeping the implementation as a plugin, fully separate from Certbot core. A nicer implementation is possible if service discovery were to be implemented in Certbot core (this would introduce a dependency on dnspython).

Next steps

I will present and demo this proposal during the acme Working Group meeting at IETF 109 (November 2020). From there I hope that it will be adopted, developed, and shepherded through to become an RFC. I will also seek feedback from Certbot developers about the proposal and my experimental implementation.

I also intend to submit another Internet-Draft proposing a mechanism for servers to advertise their capabilities in the ACME directory object. This could be useful to help clients choose from multiple servers (regardless of how they find out about the servers). And I think it’s good practice. When a protocol has many possible features that a server may or may not implement, servers should declare their capabilities for the benefit of clients.

Beyond that, I am starting to think about SRVName support in ACME. This would be useful in enterprise environments and on the open internet for protocols where SRV records are used to locate servers. Such protocols include Kerberos, LDAP, SIP and XMPP.

Creative Commons License
Except where otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License .