Skip to content

Commit 07b5b1a

Browse files
authored
Merge pull request #64 from NitrogenUA/lets-encrypt-dns-01
Implementation of dns-01 challenge support in Confconsole Let's Encrypt plugin.
2 parents 293c2c2 + 50bb026 commit 07b5b1a

9 files changed

Lines changed: 315 additions & 35 deletions

docs/RelNotes-2.1.0.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
====================
2+
v2.1.0 Release Notes
3+
====================
4+
5+
* implemented support for dns-01 challenge in Let's Encrypt plugin.

plugins.d/Lets_Encrypt/dehydrated-wrapper

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#!/bin/bash -e
22

3-
# Copyright (c) 2016-2020 TurnKey GNU/Linux - https://www.turnkeylinux.org
3+
# Copyright (c) 2016-2022 TurnKey GNU/Linux - https://www.turnkeylinux.org
44
#
55
# dehyrdated-wrapper - A wrapper script for the Dehydrated
66
# Let's Encrypt client
7-
#
7+
#
88
# This file is part of Confconsole.
9-
#
9+
#
1010
# Confconsole is free software; you can redistribute it and/or modify it
1111
# under the terms of the GNU Affero General Public License as published by the
1212
# Free Software Foundation; either version 3 of the License, or (at your
@@ -39,8 +39,10 @@ LICENSE=$(curl $LE_TOS_URL 2>/dev/null | grep termsOfService \
3939

4040
SH_CONFIG=$SHARE/dehydrated-confconsole.config
4141
SH_HOOK=$SHARE/dehydrated-confconsole.hook.sh
42+
SH_HOOK_DNS=$SHARE/dehydrated-confconsole.hook-dns.sh
4243
SH_CRON=$SHARE/dehydrated-confconsole.cron
4344
SH_DOMAINS=$SHARE/dehydrated-confconsole.domains
45+
export LEXICON_CONFIG_DIR=$SHARE
4446

4547
export TKL_CERTFILE="/usr/local/share/ca-certificates/cert.crt"
4648
export TKL_KEYFILE="/etc/ssl/private/cert.key"
@@ -63,7 +65,7 @@ chown -R $HTTP_USR "$(dirname $HTTP_PID)" "$(dirname $LOG)"
6365
usage() {
6466
echo "$@"
6567
cat<<EOF
66-
Syntax: $APP [--force|-f] [-r|--register] [--log-info|-i] [--help|-h]
68+
Syntax: $APP [--force|-f] [--register|-r] [--challenge|-c <type>] [--provider|-p <name>] [--log-info|-i] [--help|-h]
6769
6870
TurnKey Linux wrapper script for dehydrated.
6971
@@ -82,23 +84,31 @@ Environment variables:
8284
8385
Options:
8486
85-
--force|-f - pass force switch to dehydrated
87+
--force|-f - Pass --force switch to dehydrated.
88+
89+
This will force dehydrated to update certs
90+
regardless of expiry. The included cron job does
91+
this by default (after checking the expiry of
92+
/etc/ssl/private/cert.pem).
93+
94+
--register|-r - Accept Terms of Service (ToS) and register a
95+
Let's Encrypt account. (Note if an LE account
96+
already registered, this option makes no difference
97+
so is safe to always use).
8698
87-
This will force dehydrated to update certs regardless of
88-
expiry. The included cron job does this by default (after
89-
checking the expiry of /etc/ssl/private/cert.pem)
99+
Let's Encrypt ToS can currently be found here:
100+
$LICENSE
90101
91-
--register|-r - Accept Terms of Service (ToS) and register a Let's Encrypt
92-
account. (Note if an LE account already registered, this
93-
option makes no difference so is safe to always use).
102+
--challenge|-c <type> - Instruct dehydrated to use specific challenge type.
94103
95-
Let's Encrypt ToS can currently be found here:
96-
$LICENSE
104+
--provider|-p <name> - Specify DNS provider name to use with dns-01
105+
challenge. Refer to lexicon documentation for the
106+
list of supported providers.
97107
98-
--log-info|-i - INFO will be logged (default logging is WARNING & FATAL
99-
only)
108+
--log-info|-i - INFO will be logged (default logging is
109+
WARNING & FATAL only).
100110
101-
--help|-h - print this information and exit
111+
--help|-h - Print this information and exit.
102112
103113
For more info on advanced usage, please see
104114
@@ -209,6 +219,14 @@ while [[ $# -gt 0 ]]; do
209219
case $arg in
210220
-f|--force) args="$args --force";;
211221
-r|--register) REGISTER=y;;
222+
-c|--challenge) if [[ ! -z $2 && ! $2 =~ ^- ]]; then
223+
CTYPE=${2,,}
224+
shift
225+
fi;;
226+
-p|--provider) if [[ ! -z $2 && ! $2 =~ ^- ]]; then
227+
export PROVIDER=${2,,}
228+
shift
229+
fi;;
212230
-i|--log-info) LOG_INFO=y;;
213231
-h|--help) usage;;
214232
*) usage "FATAL: unsupported or unknown argument: $1";;
@@ -235,7 +253,13 @@ copy_if_not_found "$DOMAINS_TXT" "$SH_DOMAINS"
235253
[ -z "$HOOK" ] && fatal "hook script not defined in $CONFIG"
236254
[ "$HOOK" != "$CC_HOOK" ] && warning "$CONFIG is not using $CC_HOOK"
237255

238-
copy_if_not_found "$HOOK" "$SH_HOOK"
256+
case $CTYPE in
257+
http-01) cp "$SH_HOOK" "$HOOK"
258+
sed -i 's/^CHALLENGETYPE.*/CHALLENGETYPE=\"http-01\"/' "$CONFIG";;
259+
dns-01) cp "$SH_HOOK_DNS" "$HOOK"
260+
sed -i 's/^CHALLENGETYPE.*/CHALLENGETYPE=\"dns-01\"/' "$CONFIG";;
261+
*) copy_if_not_found "$HOOK" "$SH_HOOK";;
262+
esac
239263

240264
chmod +x $HOOK
241265

@@ -285,7 +309,7 @@ else
285309
fi
286310

287311
[ "$AUTHBIND_USR" = "$HTTP_USR" ] || chown $HTTP_USR $AUTHBIND80
288-
systemctl start add-water
312+
[ "$CTYPE" != "dns-01" ] && systemctl start add-water
289313
info "running dehydrated"
290314
if [ "$DEBUG" = "y" ] || [ "$LOG_INFO" = "y" ]; then
291315
dehydrated --cron $args --config $CONFIG 2>&1 | tee -a $DEBUG_LOG

plugins.d/Lets_Encrypt/dns_01.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/python3
2+
import sys
3+
import subprocess
4+
import re
5+
6+
from subprocess import PIPE
7+
from os import path
8+
from shutil import which
9+
10+
LEXICON_CONF = '/usr/share/confconsole/letsencrypt/lexicon.yml'
11+
12+
LEXICON_CONF_NOTE = '''# Configure according to lexicon documentation https://dns-lexicon.readthedocs.io/
13+
# Note that documentation began around v.3.3.28 of lexicon. Therefore not all of
14+
# the features might be available to you!
15+
'''
16+
17+
LEXICON_CONF_MAX_LINES = 7
18+
19+
def load_config():
20+
''' Loads lexicon config if present '''
21+
config = []
22+
if not path.isfile(LEXICON_CONF):
23+
while len(config) < LEXICON_CONF_MAX_LINES:
24+
config.append('')
25+
return config
26+
else:
27+
with open(LEXICON_CONF, 'r') as fob:
28+
for line in fob:
29+
line = line.rstrip()
30+
if line and not line.startswith('#'):
31+
config.append(line)
32+
33+
while len(config) > LEXICON_CONF_MAX_LINES:
34+
config.pop()
35+
while len(config) < LEXICON_CONF_MAX_LINES:
36+
config.append('')
37+
return config
38+
39+
def save_config(config):
40+
''' Saves lexicon configuration '''
41+
with open(LEXICON_CONF, 'w') as fob:
42+
fob.write(LEXICON_CONF_NOTE)
43+
for line in config:
44+
line = line.rstrip()
45+
if line:
46+
fob.write(line + '\n')
47+
48+
def get_providers():
49+
lexicon_bin = which('lexicon')
50+
if not lexicon_bin:
51+
ret = console.yesno(
52+
'lexicon tool is required to use dns-01 challenge, '
53+
'however it is not found on your system.\n\n'
54+
'Do you wish to install it now?',
55+
autosize=True
56+
)
57+
if ret != 'ok':
58+
return None, 'Please install lexicon to use dns-01 challenge.'
59+
60+
apt = subprocess.run(['apt-get', '-y', 'install', 'lexicon'],
61+
encoding=sys.stdin.encoding,
62+
stderr=PIPE)
63+
if apt.returncode != 0:
64+
return None, apt.stderr.strip()
65+
66+
lexicon_bin = which('lexicon')
67+
if not lexicon_bin:
68+
return None, 'lexicon is not found on your system, is it installed?'
69+
70+
proc = subprocess.run([lexicon_bin, '--help'],
71+
encoding=sys.stdin.encoding,
72+
capture_output=True)
73+
if proc.returncode != 0:
74+
return None, proc.stderr.strip()
75+
76+
match = re.search(r"(?<={).*(?=})", proc.stdout.strip())
77+
if not match:
78+
return None, 'Could not obtain DNS providers list from lexicon!'
79+
80+
providers = []
81+
for provider in match.group().split(','):
82+
if len(provider) > 0:
83+
providers.append((provider, '%s provider' % provider))
84+
85+
if providers:
86+
return providers, None
87+
return None, 'DNS providers list is empty!'

plugins.d/Lets_Encrypt/get_certificate.py

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import requests
44
import sys
55
import subprocess
6+
67
from subprocess import PIPE
78
from os import path, remove
8-
from shutil import copyfile
9+
from shutil import copyfile, which
10+
11+
import dns_01
912

1013
LE_INFO_URL = 'https://acme-v02.api.letsencrypt.org/directory'
1114

@@ -72,6 +75,10 @@ def invalid_domains(domains):
7275
if len(domain) > 254:
7376
return ('Error in {}: Domain names must not exceed 254'
7477
' characters'.format(domain))
78+
if domain.count('.') < 1:
79+
return ('Error in {}: Domain may not have less'
80+
' than 2 segments'
81+
''.format(domain))
7582
for part in domain.split('.'):
7683
if not 0 < len(part) < 64:
7784
return ('Error in {}: Domain segments may not be larger'
@@ -100,16 +107,6 @@ def run():
100107
console.msgbox('Error', msg, autosize=True)
101108
return
102109

103-
ret = console.yesno(
104-
'DNS must be configured before obtaining certificates. '
105-
'Incorrectly configured dns and excessive attempts could '
106-
'lead to being temporarily blocked from requesting '
107-
'certificates.\n\nDo you wish to continue?',
108-
autosize=True
109-
)
110-
if ret != 'ok':
111-
return
112-
113110
ret = console.yesno(
114111
"Before getting a Let's Encrypt certificate, you must agree"
115112
' to the current Terms of Service.\n\n'
@@ -132,6 +129,74 @@ def run():
132129
)
133130
return
134131

132+
ret, challenge = console.menu('Challenge type',
133+
'Select challenge type to use', [
134+
('http-01', 'Requires public web access to this system'),
135+
('dns-01', 'Requires your DNS provider to provide an API')
136+
])
137+
if ret != 'ok':
138+
return
139+
140+
if challenge == 'http-01':
141+
ret = console.yesno(
142+
'DNS must be configured before obtaining certificates. '
143+
'Incorrectly configured DNS and excessive attempts could '
144+
'lead to being temporarily blocked from requesting '
145+
'certificates.\n\nDo you wish to continue?',
146+
autosize=True
147+
)
148+
if ret != 'ok':
149+
return
150+
151+
if challenge == 'dns-01':
152+
config = dns_01.load_config()
153+
fields = [
154+
('', 1, 0, config[0], 1, 10, field_width, 255),
155+
('', 2, 0, config[1], 2, 10, field_width, 255),
156+
('', 3, 0, config[2], 3, 10, field_width, 255),
157+
('', 4, 0, config[3], 4, 10, field_width, 255),
158+
('', 5, 0, config[4], 5, 10, field_width, 255),
159+
('', 6, 0, config[5], 6, 10, field_width, 255),
160+
('', 7, 0, config[6], 7, 10, field_width, 255),
161+
]
162+
ret, values = console.form('Lexicon configuration',
163+
'Review and adjust current lexicon '
164+
'configuration as necessary.\n\n'
165+
'You can follow configuration reference at:\n'
166+
'https://dns-lexicon.readthedocs.io/',
167+
fields, autosize=True)
168+
if ret != 'ok':
169+
return
170+
171+
if config != values:
172+
dns_01.save_config(values)
173+
174+
providers, err = dns_01.get_providers()
175+
if err:
176+
console.msgbox('Error', err, autosize=True)
177+
return
178+
179+
ret, provider = console.menu('DNS providers list',
180+
'Select DNS provider you\'d like to use',
181+
providers)
182+
if ret != 'ok':
183+
return
184+
elif provider == 'auto' and not which('nslookup'):
185+
ret = console.yesno(
186+
'nslookup tool is required to use dns-01 challenge with auto provider.\n\n'
187+
'Do you wish to install it now?',
188+
autosize=True
189+
)
190+
if ret != 'ok':
191+
return
192+
193+
apt = subprocess.run(['apt-get', '-y', 'install', 'dnsutils'],
194+
encoding=sys.stdin.encoding,
195+
stderr=PIPE)
196+
if apt.returncode != 0:
197+
console.msgbox('Error', apt.stderr.strip(), autosize=True)
198+
return
199+
135200
domains = load_domains()
136201
m = invalid_domains(domains)
137202

@@ -179,10 +244,13 @@ def run():
179244

180245
# User has accepted ToS as part of this process, so pass '--register'
181246
# switch to Dehydrated wrapper
182-
proc = subprocess.run(
183-
['bash', path.join(
184-
path.dirname(PLUGIN_PATH), 'dehydrated-wrapper'),
185-
'--register'],
247+
dehydrated_bin = ['bash', path.join(
248+
path.dirname(PLUGIN_PATH), 'dehydrated-wrapper'),
249+
'--register', '--challenge', challenge]
250+
if challenge == 'dns-01':
251+
dehydrated_bin.append('--provider')
252+
dehydrated_bin.append(provider)
253+
proc = subprocess.run(dehydrated_bin,
186254
encoding=sys.stdin.encoding,
187255
stderr=PIPE)
188256
if proc.returncode == 0:

share/letsencrypt/dehydrated-confconsole.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ BASEDIR=/var/lib/dehydrated
1919
WELLKNOWN="${BASEDIR}/acme-challenges"
2020
DOMAINS_TXT="/etc/dehydrated/confconsole.domains.txt"
2121
HOOK="/etc/dehydrated/confconsole.hook.sh"
22+
CHALLENGETYPE="http-01"

0 commit comments

Comments
 (0)