Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module for handling dns-01 challenges by Dynamic DNS updates #90

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

Yenya
Copy link

@Yenya Yenya commented Apr 2, 2024

Hello, Alexander,

I have written a module for storing dns-01 ACME challenges in the Dynamic DNS domain. Do you think it is suitable for inclusion in the Crypt::LE distribution?

Thanks,

-Yenya

NAME
    Crypt::LE::Challenge::DDNS - use dynamic DNS for ACME challenges

SYNOPSIS
     use Crypt::LE;
     use Crypt::LE::Challenge::DDNS;
     ...
     my $le = Crypt::LE->new();
     my $ddns_challenge = Crypt::LE::Challenge::DDNS->new(...);
     ..
     $le->accept_challenge($ddns_challenge, ...);
     $le->verify_challenge($ddns_challenge, ...);

     # Shell command line:
     $ le.pl ... --handle-as dns --handle-with Crypt::LE::Challenge::DDNS \
        --handle-params '{"server": "127.0.0.1", "keyfile": "/var/named/keys/_le.example.org.key", "zone": "_le.example.org"}'

DESCRIPTION
    This module uses Dynamic DNS (DDNS) updates for storing the ACME
    challenges for DNS-01 validation.

    Recommended mode of operation is to set up a Dynamic DNS subdomain
    solely for ACME challenges (for example, "_le.example.org"), and for
    domain names which would use Let's Encrypt certificates map their
    "_acme-challenge.$fqdn" into this domain.

    For example, to get a certificate for "myhost.example.org", create the
    following static DNS record in the "example.org" zone:

      _acme-challenge.myhost.example.org. IN CNAME myhost.example.org._le.example.org.

    This module will then ask the ACME server (Let's Encrypt CA) for a
    DNS-based challenge, and will store it using DDNS update to
    "myhost.example.org._le.example.org." TXT record. LE will then try to
    verify the challenge at "_acme-challenge.myhost.example.org" and will
    find it after being redirected by the above CNAME record.

    Note that ""example.org"" string is used both in the DDNS domain name,
    and inside that name. This is intentional - this way one common DDNS
    domain "_le.example.org" can serve for ACME challenges for multiple real
    DNS domains. If you want the renaming to be done in a different way,
    feel free to override the rr_from_fqdn() function in this module.

    The module accepts the following parameters (usable in "--handle-params"
    from the "le.pl" command line):

    server
        IP address of the DDNS server, where challenges will be stored.

    keyfile
        Authentication key for DDNS. This will be used for signing the DDNS
        update requests. Any key file format supported by
        "Net::DNS::RR::TSIG" will do.

    zone
        DDNS zone to which challenges will be written ("_le.example.org" in
        the above examples). If "zone" is a suffix of the host name for
        which the certificate is being created (e.g. "example.org"), then
        the challenge will be stored directly to

          _acme-challenge.$that_host_name

        instead of mapping to a different zone as described above.

DDNS ZONE SETUP
    A quick and dirty tutorial how to create a Dynamic DNS zone and key in
    BIND.

    Firstly, create directories and the key file:

      BIND_DIR=/var/named
      DDNS_DOMAIN=_le.example.org
      install -d -u named -g named -m 775 $BIND_DIR/dynamic
      install -d -u root -g named -m 755 $BIND_DIR/keys
      tsig-keygen $DDNS_DOMAIN > $BIND_DIR/keys/$DDNS_DOMAIN.key
      chown root:named $BIND_DIR/keys/$DDNS_DOMAIN.key
      chmod 640 $BIND_DIR/keys/$DDNS_DOMAIN.key

    Create a zone file:

      cat > $BIND_DIR/dynamic/$DDNS_DOMAIN <<'EOF'
      $TTL 300
         IN SOA  ns.example.org. root.example.org. (
            1   ; serial
            1H  ; refresh
            3H  ; retry
            2W  ; expire
            1   ; negative ttl
            )
         IN NS   ns.example.org.
      EOF

    Use the key and zone file in your named.conf:

      include "keys/_le.example.org.key";

      zone "_le.example.org" {
        type master;
        file "dynamic/_le.example.org";
        allow-query { any; };
        allow-update { !{ !127.0.0.1; any; }; key _le.example.org; };
        journal "dynamic/_le.example.org.jnl";
      }

    Reload named and verify that it works:

      rndc reload
      nsupdate -k $BIND_DIR/keys/$DDNS_DOMAIN.key
      > server 127.0.0.1
      > add test._le.example.org. 300 TXT "my test record"
      > send
      host -t any test._le.example.org. 127.0.0.1

SEE ALSO
    <https://letsencrypt.org/docs/challenge-types/>, Crypt::LE,
    Net::DNS::RR::TSIG, Crypt::LE::Challenge::Simple, nsupdate(1),
    tsig-keygen(1)

AUTHOR
    Jan "Yenya" Kasprzak "<kas you_know_what yenya.net>". Based on
    "Crypt::LE::Challenge::Simple" by Alexander Yezhov.
Yenya added 2 commits April 2, 2024 15:43
For wildcard certificates (*.$fqdn), ACME queries _acme-challenge.$fqdn,
without the "*." prefix, obviously. This has to be taken into account
when remapping the wildcard name to a different DDNS subdomain.

Moreover, when getting certificate for both $fqdn and *.$fqdn,
Crypt::LE calls ->handle_challenge_dns() twice for the same $fqdn,
and ACME expects _two_ TXT records. So we should not blindly delete
the TXT record at the beginning of ->handle_challenge_dns().

My approach is to count the number of times we've got called for
each $fqdn, store it in $self->{fqdn_seen}->{$fqdn},
and delete the TXT only in the first handle_challenge_dns() call,
and in the last handle_verification_dns() call.
@jb-wisemo
Copy link

Suggested documentation improvement: Change one of the two example domains to "example.com", this makes it easier to see which domain names are supposed to be the same, and gets rid of the awkward "used both in the DDNS domain name,
and inside that name" phrase in the explanation, allowing it to be replaced by "Of cause, if you only rent one domain name, you can use it for both the certificate domain and the DDNS domain" or a similar phrase.

@guest20
Copy link

guest20 commented May 31, 2024

@jb-wisemo that might be a good idea as a second ticket

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants