-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrenew-letsencrypt-tlsa.sh
executable file
·418 lines (370 loc) · 15 KB
/
renew-letsencrypt-tlsa.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
#!/bin/bash
#
# This script renews letsencrypt certificates with a given csr and private key file
# thereby maintaining the same SPKI hash for use with TLSA and DANE
# Author: Robert Strasser <[email protected]>
#
# usage: renew-letsencrypt-tlsa.sh -d www.domain.tld -m <dnsAlternativeNamesFile> -w [apache|nginx]
# optionally if using dovecot and postfix, specify the option -e true
############################################
## TODO:
## - CSR generation for additional Domains if newly added
############################################
# getting and setting script directory
SCRIPTDIR=$( dirname "$(readlink -f "$0")" )
# import openssl CSR configuration
source $SCRIPTDIR/openssl.conf
# import general settings
source $SCRIPTDIR/settings.conf
# checking if console log path exists, if not create it
if [ ! -d "$consoleLogPath" ]; then
mkdir -p $consoleLogPath
fi
# write output to logfile
exec > >(tee -i ${consoleLog})
exec 2>&1
# get IPv4 and IPv6 address of local webserver interface
if [ "$localIPv4override" == "yes" ] ; then
localIPv4=$localIPv4overrideAddress
else
localIPv4=$( ip address show dev $webserverInterface scope global | awk '/inet / {split($2,var,"/"); print var[1]}' )
fi
if [ $IPv6active == "yes" ] ; then
if [ "$localIPv6override" == "yes" ] ; then
localIPv6=$localIPv6overrideAddress
else
localIPv6=$( ip address show dev $webserverInterface scope global | awk '/inet6 / {split($2,var,"/"); print var[1]}' )
fi
fi
# get options from command line
while getopts ":d:e:f:m:w:" opt; do
case $opt in
d) domainCN="$OPTARG"
echo "Domain name is set to: $domainCN"
;;
e) email=true
echo "Note: email server option is set."
;;
f) forceRenewal=true
echo "Note: Renewal is forced."
;;
m) multipleDnsAlternativeNames=true
dnsAlternativeNamesFile="$OPTARG"
echo "Note: additional DNS subject alternative names option is set using input file $dnsAlternativeNamesFile"
# checking if dnsAlternativeNamesFile exists, if not exit
if [ ! -f $dnsAlternativeNamesFile ]; then
echo "Error, you selected the option -m, but $dnsAlternativeNamesFile is not found!"
echo "For additional DNS alternative names, you must provide the file $dnsAlternativeNamesFile"
echo "in the script root folder specifying each alternative subject name on a separate line"
exit 1;
else
# declaring dnsAlternativeNames as an indexed array
declare -a dnsAlternativeNames
# Reading in the lines of dnsAlternativeNamesFile
readarray -t dnsAlternativeNames < $dnsAlternativeNamesFile
# storing length of dnsAlternativeNames array in variable
dnsAlternativeNamesSum=${#dnsAlternativeNames[@]}
fi
;;
w) webserver="$OPTARG"
if [ "$webserver" == "nginx" ]; then
nginx=true
echo "Note: webserver is set to nginx."
elif [ "$webserver" == "apache" ]; then
apache=true
echo "Note: webserver is set to apache."
else
echo "error: unsupported webserver option"
echo "for nginx use: -w nginx"
echo "for apache use: -w apache"
exit 1;
fi
;;
\?) echo "Invalid option -$OPTARG" >&2
echo 'usage: renew-letsencrypt-tlsa.sh -d www.domain.tld -m <dnsAlternativeNamesFile> -w [apache|nginx]'
echo 'optionally: if dovecot and postfix use this certificate also, set "-e true" to restart them'
# exit if option is invalid
exit 1;
;;
esac
done
#########################################
### CHECKING IF domainCN variable is set
if [ -z "$domainCN" ]; then
echo "Error: domainname not specified, please use the following option"
echo "-d domainname"
exit 64;
fi
if [ -z "$webserver" ]; then
echo "Error: webserver is not specified, please use the following option"
echo "-w nginx or -w apache"
exit 64;
fi
if [ -z "$forceRenewal" ] ; then
forceRenewal=false
fi
### OTHER VARIABLES ###
certFilename="$domainCN.pem"
csrFilename="$domainCN.csr.pem"
csrConfigFileName="/etc/ssl/csr/$domainCN-csr.conf"
privateKeyFilename="$domainCN.privkey.pem"
certificateChainFilename="chain.pem"
fullChainFilename="fullchain.pem"
letsencryptLogDir="/var/log/letsencrypt"
NOW=$(date +%s)
genCertCommand="certbot certonly --agree-tos --standalone --non-interactive --email $email --csr $certPath/$domainCN/$csrFilename --cert-path $certPath/$domainCN/$certFilename --key-path $certPath/$domainCN/$privateKeyFilename --rsa-key-size 4096 --chain-path $certPath/$domainCN/$certificateChainFilename --fullchain-path $certPath/$domainCN/$fullChainFilename --logs-dir $letsencryptLogDir --quiet -d $domainCN"
###########################################
# set DNS server for name resolution in script
if [ "$localDNSServerOverride" == "yes" ] ; then
DNSServerAdress=$localDNSServerOverrideAdress
else
DNSServerAdress=$(cat /etc/resolv.conf | grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}" -m 1)
fi
### check if DNS resolution of domains points to this host/webserver
domainIPv4=$( dig @$DNSServerAdress +short $domainCN a | grep -v '\.$' )
if [ "$localIPv4" != "$domainIPv4" ] ; then
echo ""
echo "Error: $domainCN does not resolve to local IPv4 address $localIPv4"
echo "please adjust your DNS settings"
dnsError="yes"
fi
if [ $IPv6active == "yes" ] ; then
domainIPv6=$( dig @$DNSServerAdress +short $domainCN aaaa | grep -v '\.$' )
if [ "$localIPv6" != "$domainIPv6" ] ; then
echo ""
echo "Error: $domainCN does not resolve to local IPv6 address $localIPv6"
echo "please adjust your DNS settings"
dnsError="yes"
fi
fi
### ADJUSTING genCertCommand for additional DNS subject alternative names
if [ "$multipleDnsAlternativeNames" = true ] ; then
echo ""
echo "### Alternative Names Section ###"
echo "added the following $dnsAlternativeNamesSum DNS alternative subject names to certificate:"
let i=0
while (( ${#dnsAlternativeNames[@]} > i )); do
echo "${dnsAlternativeNames[i]}"
# check if DNS alternative name resolves properly to host/webserver IPs
domainIPv4=$( dig @$DNSServerAdress +short ${dnsAlternativeNames[i]} a | grep -v '\.$' )
if [ "$localIPv4" != "$domainIPv4" ] ; then
echo ""
echo "Error: ${dnsAlternativeNames[i]} does not resolve to local IPv4 address $localIPv4"
echo "please adjust your DNS settings"
dnsError="yes"
fi
if [ $IPv6active == "yes" ] ; then
domainIPv6=$( dig @$DNSServerAdress +short ${dnsAlternativeNames[i]} aaaa | grep -v '\.$' )
if [ "$localIPv6" != "$domainIPv6" ] ; then
echo ""
echo "Error: ${dnsAlternativeNames[i]} does not resolve to local IPv6 address $localIPv6"
echo "please adjust your DNS settings"
dnsError="yes"
fi
fi
# add DNS alternative name to command
genCertCommand="$genCertCommand -d ${dnsAlternativeNames[i++]}"
done
fi
##########################################
if [ "$dnsError" != "yes" ] ; then
### CHECKING DIRECTORIES and FILES ###
# checking if directory exists, if not create it
if [ ! -d "$certPath/$domainCN" ]; then
mkdir -p $certPath/$domainCN
fi
# checking if private key file exist, if not create it
if [ ! -f $certPath/$domainCN/$privateKeyFilename ]; then
echo ""
echo "### Private Key Section ###"
echo "Note: $certPath/$domainCN/$privateKeyFilename not found!"
echo "Private Key is required for this script to work"
echo "Generating private key $certPath/$domainCN/$privateKeyFilename with RSA 4096"
openssl genrsa -out $certPath/$domainCN/$privateKeyFilename 4096
fi
# generating csr file with specified settings from openssl.conf
echo -e "\n"
echo "### CSR Section ###"
echo "Generating CSR configuration file /etc/ssl/csr/$domainCN-csr.conf"
# checking if directory exists, if not create it
if [ ! -d "/etc/ssl/csr/" ]; then
echo "/etc/ssl/csr directory did not exist, creating it"
mkdir -p /etc/ssl/csr/
fi
# checking if csr file already exists, if yes then delete it
if [ -f $csrConfigFileName ]; then
rm -rf $csrConfigFileName
fi
# writing openssl CSR configuration
echo "[req]" >> $csrConfigFileName
echo "distinguished_name = req_distinguished_name" >> $csrConfigFileName
echo "req_extensions = v3_req" >> $csrConfigFileName
echo "prompt = no" >> $csrConfigFileName
echo "[req_distinguished_name]" >> $csrConfigFileName
echo "C = $COUNTRY" >> $csrConfigFileName
echo "ST = $STATE" >> $csrConfigFileName
echo "L = $CITY" >> $csrConfigFileName
echo "O = $ORGANIZATION" >> $csrConfigFileName
echo "OU = $DEPARTMENT" >> $csrConfigFileName
echo "CN = $domainCN" >> $csrConfigFileName
echo "[v3_req]" >> $csrConfigFileName
echo "keyUsage = keyEncipherment, dataEncipherment" >> $csrConfigFileName
echo "extendedKeyUsage = serverAuth" >> $csrConfigFileName
echo "subjectAltName = @alt_names" >> $csrConfigFileName
echo "[alt_names]" >> $csrConfigFileName
echo "DNS.1 = $domainCN" >> $csrConfigFileName
##########################################
# add additional alternative subject DNS names to CSR if set
if [ "$multipleDnsAlternativeNames" = true ] ; then
let j=0
# first dns entry is already present, start with DNS.2 and increment
dnsCount=2;
while (( ${#dnsAlternativeNames[@]} > j )); do
echo "DNS.$dnsCount = ${dnsAlternativeNames[j++]}" >> $csrConfigFileName
((dnsCount++))
done
fi
# finally create actual CSR file with openssl command
openssl req -new -out $certPath/$domainCN/$csrFilename -key $certPath/$domainCN/$privateKeyFilename -config /etc/ssl/csr/$domainCN-csr.conf
##########################################
# check if certificate already exists, evaluate its validity
if [ ! -f $certPath/$domainCN/$certFilename ]; then
# if certificate does not exist set validity to 0
CERT_VALIDITY=0
else
# if certificate exists read validity end date
OPENSSL_ENDDATE="$(openssl x509 -enddate -noout -in $certPath/$domainCN/$certFilename)"
# cutting first 9 letters of openssl output to get clean end date
CERT_ENDDATE=${OPENSSL_ENDDATE:9}
# convert to seconds EPOCH
CERT_ENDDATE=$(date -d "$CERT_ENDDATE" +%s)
# computing validity days
CERT_VALIDITY=$((($CERT_ENDDATE - $NOW) / (24*3600)))
if [ "$multipleDnsAlternativeNames" = true ] ; then
echo ""
echo "### Previous alternative name checks ###"
let i=0
while (( ${#dnsAlternativeNames[@]} > i )); do
# check if new DNS alternative names have changed or have been added, if so set CERT_VALIDITY to 0,
# so that certificate will be renewed with new/changed DNS alternative names
# store previous DNS alternative names
PREVIOUS_CERT_ALTERNATIVE_NAMES=$( openssl x509 -text -noout -in $certPath/$domainCN/$certFilename -certopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_issuer,no_pubkey,no_sigdump,no_aux | grep DNS: | sed 's/\<DNS\>://g' | sed 's/[[:blank:]]//g' )
# check if new DNS alternative names are already in the previous certificate, if not set CERT_VALIDITY to 0
if [[ "$PREVIOUS_CERT_ALTERNATIVE_NAMES" != *"${dnsAlternativeNames[i]}"* ]] ; then
echo Note: "${dnsAlternativeNames[i]} was NOT contained in previous certificate's DNS alternative names"
echo "Info: previous alternative DNS names were: $PREVIOUS_CERT_ALTERNATIVE_NAMES"
forceRenewal=true;
else
echo "${dnsAlternativeNames[i]} was already contained in previous certificate's DNS alternative names"
fi
# increment counter
((i=i+1))
done
fi
fi
###################################################
### RENEWING CERTIFICATE ###
# if valid less then 30 days renew
# if certificate does not exist create new certificate
if [ $CERT_VALIDITY -lt 30 ] || [ $forceRenewal == true ]
then
### STOPPING WEBSERVER(S) ###
if [ "$nginx" = true ] ; then
echo ""
echo "Stopping nginx"
systemctl stop nginx
fi
if [ "$apache" = true ] ; then
echo ""
echo "Stopping apache2"
systemctl stop apache2
fi
###########################################
# make a backup of previous certificate files
mkdir $certPath/$domainCN/$NOW
if [ -f $certPath/$domainCN/$certificateChainFilename ]; then
mv $certPath/$domainCN/$certificateChainFilename $certPath/$domainCN/$NOW/$certificateChainFilename
echo "old chainfile moved to $certPath/$domainCN/$NOW/$certificateChainFilename"
fi
if [ -f $certPath/$domainCN/$fullChainFilename ]; then
mv $certPath/$domainCN/$fullChainFilename $certPath/$domainCN/$NOW/$fullChainFilename
echo "old fullchain file moved to $certPath/$domainCN/$NOW/$fullChainFilename"
fi
if [ -f $certPath/$domainCN/$certFilename ]; then
mv $certPath/$domainCN/$certFilename $certPath/$domainCN/$NOW/$certFilename
echo "old certificate moved to $certPath/$domainCN/$NOW/$certFilename"
fi
############################################
### RENEW CERTIFICATE ###
# non-interactively renew certificates with specified private key and certificate request file
eval $genCertCommand
############################################
### RESTARTING WEBSERVER(S) ###
if [ "$nginx" = true ] ; then
echo "Starting nginx"
systemctl start nginx
fi
if [ "$apache" = true ] ; then
echo "Starting apache2"
systemctl start apache2
fi
############################################
### RELOAD CONFIGURATION to use new certificate ###
if [ "$nginx" = true ] ; then
echo "Reloading config for nginx..."
systemctl reload nginx
fi
if [ "$apache" = true ] ; then
echo "Reloading config for apache2..."
systemctl reload apache2
fi
if [ "$email" = true ] ; then
echo "Reloading config for postfix and dovecot..."
systemctl reload postfix
systemctl reload dovecot
fi
############################################
### PRINT SUMMARY ###
echo ""
echo "### Summary Section ###"
echo "--------"
echo "Domain name: $domainCN"
echo "Certficate: $certPath/$domainCN/$certFilename"
echo "Private Key: $certPath/$domainCN/$privateKeyFilename"
echo "Chain File: $certPath/$domainCN/$certificateChainFilename"
echo "Full Chain File: $certPath/$domainCN/$fullChainFilename"
echo "Renewed, validity was $CERT_VALIDITY days"
else
### DO NOT RENEW ###
### PRINT SUMMARY ###
echo ""
echo "### Summary Section ###"
echo "--------"
echo "Domain name: $domainCN"
echo "Certficate: $certPath/$domainCN/$certFilename"
echo "Private Key: $certPath/$domainCN/$privateKeyFilename"
echo "Chain File: $certPath/$domainCN/$certificateChainFilename"
echo "Full Chain File: $certPath/$domainCN/$fullChainFilename"
echo "Nothing done, validity ok, $CERT_VALIDITY days remaining"
############################################
fi
### OUTPUT TLSA Record SPKI ###
echo ''
echo 'The TLSA DNS Record for the Certificate is:'
echo '--------------------------------------------'
printf '_443._tcp.%s. IN TLSA 3 1 1 %s\n' \
$domainCN \
$(openssl req -in $certPath/$domainCN/$csrFilename -noout -pubkey |
openssl pkey -pubin -outform DER |
openssl dgst -sha256 -binary |
hexdump -ve '/1 "%02x"')
############################################
fi
# if a DNS error occurred set CERT_VALIDITY to 0, to trigger email alert
if [ "$dnsError" == "yes" ] ; then
CERT_VALIDITY=0
fi
# send an email if validity is less than 14 days or if a DNS resolution error occurred
if [ $CERT_VALIDITY -lt 30 ] ; then
mailx -a "From: "$host" Certificates <"$reportemail_from">" -s "Certificate Script | "$host $reportemail_to < $consoleLog
fi