Wiki source code of Certbot

Last modified by Sebastian Marsching on 2022/05/29 13:35

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