Let's Encrypt

DNS challenge

When a system is behind a firewall that does not allow HTTP traffic, the DNS challenge can be used as an alternative way of authenticating with the Let's Encrypt CA. When the DNS server for the domain allows dynamic updates, the DNS record for the challenge can be created automatically, by supplying the following script as a hook:

   1 #!/bin/bash
   2 
   3 # Certbot manual hook script for perfoming the DNS challenge.
   4 #
   5 # Copyright 2017 Sebastian Marsching
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining
   8 # a copy of this software and associated documentation files (the
   9 # "Software"), to deal in the Software without restriction, including
  10 # without limitation the rights to use, copy, modify, merge, publish,
  11 # distribute, sublicense, and/or sell copies of the Software, and to
  12 # permit persons to whom the Software is furnished to do so, subject to
  13 # the following conditions:
  14 #
  15 # The above copyright notice and this permission notice shall be included
  16 # in all copies or substantial portions of the Software.
  17 #
  18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  19 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  21 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  22 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  23 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  24 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  25 #
  26 # This script was inspired by the script from
  27 # https://ente.limmat.ch/ftp/pub/software/bash/letsencrypt/.
  28 #
  29 # In order to use this script, call Certbot in the following way:
  30 # certbot certonly --manual --manual-auth-hook /path/to/this/script \
  31 #   --manual-cleanup-hook /path/to/this/script --preferred-challenges dns \
  32 #   --manual-public-ip-logging-ok -d example.com
  33 #
  34 # In order for this script to work, dig and nsupdate have to be in the PATH.
  35 #
  36 # The authentication key for the DNS updates must be in
  37 # /etc/letsencrypt/ddns-keys and must conform to the format generated by
  38 # tsig-keygen. The filename must have the form ddns-key.example.com, where
  39 # example.com is the domain for which the certificate is requested or one of its
  40 # parent domains. If there is no domain-specific key file, this script falls
  41 # back to using the key in the file /etc/letsencrypt/ddns-keys/ddns-key (if it
  42 # exists).
  43 
  44 # Directory containing the DDNS authentication keys.
  45 KEYDIR="/etc/letsencrypt/ddns-keys"
  46 
  47 # DNS server to be updated. If empty, the DNS server that is responsible for the
  48 # zone is selected.
  49 SERVER=""
  50 
  51 # TTL of the TXT record created for the challenge.
  52 TTL=10
  53 
  54 # Maximum time to wait for all authoritative DNS servers to provide the updated
  55 # record.
  56 CHECK_NS_TIMEOUT=10
  57 
  58 # Usually, you should not have to make any changes below this line.
  59 
  60 # If there is an error, we want to fail immediately.
  61 set -e
  62 
  63 # Calls nsupdate and sends the update commands provided via stdin.
  64 call_nsupdate() {
  65   local keyfile
  66   local rc
  67   keyfile="$(find_ddns_keyfile)"
  68   {
  69     if [ -n "${SERVER}" ]; then
  70       echo "server ${SERVER}"
  71     fi
  72     cat
  73     echo "send"
  74   } | nsupdate -v -k "${keyfile}" >&2; rc="$?"; true
  75   if [ "${rc}" -ne 0 ]; then
  76     print_info "ERROR: DNS update failed."
  77     exit 2;
  78   fi
  79 }
  80 
  81 # Cleans the DNS records created earlier.
  82 clean_challenge() {
  83   # When cleaning the challenge, we only delete the record that we created
  84   # earlier.
  85   call_nsupdate <<EOF
  86 update delete ${DNS_RECORD}
  87 EOF
  88 }
  89 
  90 # Finds the DDNS keyfile that is supposed to be used for the domain.
  91 find_ddns_keyfile() {
  92   local current_domain
  93   local next_domain
  94   local keyfile
  95   current_domain="${CERTBOT_DOMAIN}"
  96   while [ -n "${current_domain}" ]; do
  97     keyfile="${KEYDIR}/ddns-key.${current_domain}"
  98     if [ -e "${keyfile}" ]; then
  99       echo "${keyfile}"
 100       return 0
 101     fi
 102     next_domain="${current_domain#*.}"
 103     if [ "${next_domain}" = "${current_domain}" ]; then
 104       # We have reached the point where the domain does not contain a dot, so
 105       # there are no more key files that we could try.
 106       next_domain=""
 107     fi
 108     current_domain="${next_domain}"
 109   done
 110   # If we have not found a key file, we try the generic one.
 111   keyfile="${KEYDIR}/ddns-key"
 112   if [ -e "${keyfile}" ]; then
 113     echo "${keyfile}"
 114     return 0
 115   fi
 116   print_info "ERROR: Could not find a key file for domain ${CERTBOT_DOMAIN}."
 117   exit 2
 118 }
 119 
 120 # Finds the DNS servers responsible for DNS name.
 121 find_dns_servers() {
 122   local current_domain
 123   local next_domain
 124   local found_servers
 125   current_domain="${DNS_NAME}"
 126   while [ -n "${current_domain}" ]; do
 127     found_servers="$(dig +noall +answer "${current_domain}" ns | rev | cut -f 1 | rev)"
 128     if [ -n "${found_servers}" ]; then
 129       echo "${found_servers}"
 130       return 0
 131     fi
 132     next_domain="${current_domain#*.}"
 133     if [ "${next_domain}" = "${current_domain}" ]; then
 134       # We have reached the point where the domain does not contain a dot, so
 135       # there are no more parent domains that we could try.
 136       next_domain=""
 137     fi
 138     current_domain="${next_domain}"
 139   done
 140   print_info "ERROR: Could not determine the name servers responsible for ${DNS_NAME}."
 141   exit 2
 142 }
 143 
 144 # Gets the challenge currently registered in DNS on the specified server.
 145 get_challenge_from_dns() {
 146   local dns_server
 147   dns_server="$1"
 148   dig +noall +answer "${DNS_NAME}" txt @"${dns_server}" | rev | cut -f 1 | rev
 149 }
 150 
 151 # Writes the current UNIX timestamp to stdout.
 152 now() {
 153   date "+%s"
 154 }
 155 
 156 # Prepares the challenge, registering the DNS record and waiting until it has
 157 # been distributed.
 158 prepare_challenge() {
 159   local dns_server
 160   local dns_servers
 161   local deadline
 162   # When preparing the challenge, we want to delete all old records that might
 163   # be lingering and to add a new record for the challenge.
 164   call_nsupdate <<EOF
 165 update delete ${DNS_NAME}. IN TXT
 166 update add ${DNS_RECORD}
 167 EOF
 168   # We wait some time for the DNS update to be distributed to all nameservers.
 169   dns_servers="$(find_dns_servers)"
 170   deadline=$(($(now) + ${CHECK_NS_TIMEOUT}))
 171   for dns_server in $dns_servers; do
 172     while [ "$(get_challenge_from_dns "${dns_server}")" != "\"${CERTBOT_VALIDATION}\"" ]; do
 173       if [ $(now) -gt ${deadline} ]; then
 174         print_info "ERROR: Timeout while waiting for DNS to be updated."
 175         exit 2
 176       fi
 177     done
 178   done
 179   # We have to generate some output so that CERTBOT_AUTH_OUTPUT is not empty
 180   # when this script is called again.
 181   echo "DNS update succeeded."
 182 }
 183 
 184 # Prints a message to stderr.
 185 print_info() {
 186   echo "$@" >&2
 187 }
 188 
 189 # Before starting the actual work, we verify the environment.
 190 if [ -z "${CERTBOT_DOMAIN}" ]; then
 191   print_info "ERROR: The CERTBOT_DOMAIN environment variable is empty."
 192   exit 1
 193 fi
 194 
 195 # We expect the CERTBOT_DOMAIN to have no trailing dot, but if it has one, we
 196 # remove it in order to avoid problems.
 197 CERTBOT_DOMAIN="${CERTBOT_DOMAIN%.}"
 198 
 199 if [ -z "${CERTBOT_VALIDATION}" ]; then
 200   print_info "ERROR: The CERTBOT_VALIDATION environment variable is empty."
 201   exit 1
 202 fi
 203 
 204 DNS_NAME="_acme-challenge.${CERTBOT_DOMAIN}"
 205 DNS_RECORD="${DNS_NAME}. ${TTL} IN TXT \"${CERTBOT_VALIDATION}\""
 206 
 207 # If CERTBOT_AUTH_OUTPUT is empty, this script has been called in order to
 208 # prepare the challenge, otherwise it has been called for cleanup.
 209 if [ -z "${CERTBOT_AUTH_OUTPUT}" ]; then
 210   prepare_challenge
 211 else
 212   clean_challenge
 213 fi


CategoryEnglish CategoryLinux CategoryNetwork

Network/Let's_Encrypt (last edited 2017-06-13 18:04:23 by SebastianMarsching)