Skip to content

Commit

Permalink
Merge pull request #64 from NitrogenUA/lets-encrypt-dns-01
Browse files Browse the repository at this point in the history
Implementation of dns-01 challenge support in Confconsole Let's Encrypt plugin.
  • Loading branch information
JedMeister authored Apr 5, 2023
2 parents 293c2c2 + 50bb026 commit 07b5b1a
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 35 deletions.
5 changes: 5 additions & 0 deletions docs/RelNotes-2.1.0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
====================
v2.1.0 Release Notes
====================

* implemented support for dns-01 challenge in Let's Encrypt plugin.
60 changes: 42 additions & 18 deletions plugins.d/Lets_Encrypt/dehydrated-wrapper
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/bin/bash -e

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

SH_CONFIG=$SHARE/dehydrated-confconsole.config
SH_HOOK=$SHARE/dehydrated-confconsole.hook.sh
SH_HOOK_DNS=$SHARE/dehydrated-confconsole.hook-dns.sh
SH_CRON=$SHARE/dehydrated-confconsole.cron
SH_DOMAINS=$SHARE/dehydrated-confconsole.domains
export LEXICON_CONFIG_DIR=$SHARE

export TKL_CERTFILE="/usr/local/share/ca-certificates/cert.crt"
export TKL_KEYFILE="/etc/ssl/private/cert.key"
Expand All @@ -63,7 +65,7 @@ chown -R $HTTP_USR "$(dirname $HTTP_PID)" "$(dirname $LOG)"
usage() {
echo "$@"
cat<<EOF
Syntax: $APP [--force|-f] [-r|--register] [--log-info|-i] [--help|-h]
Syntax: $APP [--force|-f] [--register|-r] [--challenge|-c <type>] [--provider|-p <name>] [--log-info|-i] [--help|-h]
TurnKey Linux wrapper script for dehydrated.
Expand All @@ -82,23 +84,31 @@ Environment variables:
Options:
--force|-f - pass force switch to dehydrated
--force|-f - Pass --force switch to dehydrated.
This will force dehydrated to update certs
regardless of expiry. The included cron job does
this by default (after checking the expiry of
/etc/ssl/private/cert.pem).
--register|-r - Accept Terms of Service (ToS) and register a
Let's Encrypt account. (Note if an LE account
already registered, this option makes no difference
so is safe to always use).
This will force dehydrated to update certs regardless of
expiry. The included cron job does this by default (after
checking the expiry of /etc/ssl/private/cert.pem)
Let's Encrypt ToS can currently be found here:
$LICENSE
--register|-r - Accept Terms of Service (ToS) and register a Let's Encrypt
account. (Note if an LE account already registered, this
option makes no difference so is safe to always use).
--challenge|-c <type> - Instruct dehydrated to use specific challenge type.
Let's Encrypt ToS can currently be found here:
$LICENSE
--provider|-p <name> - Specify DNS provider name to use with dns-01
challenge. Refer to lexicon documentation for the
list of supported providers.
--log-info|-i - INFO will be logged (default logging is WARNING & FATAL
only)
--log-info|-i - INFO will be logged (default logging is
WARNING & FATAL only).
--help|-h - print this information and exit
--help|-h - Print this information and exit.
For more info on advanced usage, please see
Expand Down Expand Up @@ -209,6 +219,14 @@ while [[ $# -gt 0 ]]; do
case $arg in
-f|--force) args="$args --force";;
-r|--register) REGISTER=y;;
-c|--challenge) if [[ ! -z $2 && ! $2 =~ ^- ]]; then
CTYPE=${2,,}
shift
fi;;
-p|--provider) if [[ ! -z $2 && ! $2 =~ ^- ]]; then
export PROVIDER=${2,,}
shift
fi;;
-i|--log-info) LOG_INFO=y;;
-h|--help) usage;;
*) usage "FATAL: unsupported or unknown argument: $1";;
Expand All @@ -235,7 +253,13 @@ copy_if_not_found "$DOMAINS_TXT" "$SH_DOMAINS"
[ -z "$HOOK" ] && fatal "hook script not defined in $CONFIG"
[ "$HOOK" != "$CC_HOOK" ] && warning "$CONFIG is not using $CC_HOOK"

copy_if_not_found "$HOOK" "$SH_HOOK"
case $CTYPE in
http-01) cp "$SH_HOOK" "$HOOK"
sed -i 's/^CHALLENGETYPE.*/CHALLENGETYPE=\"http-01\"/' "$CONFIG";;
dns-01) cp "$SH_HOOK_DNS" "$HOOK"
sed -i 's/^CHALLENGETYPE.*/CHALLENGETYPE=\"dns-01\"/' "$CONFIG";;
*) copy_if_not_found "$HOOK" "$SH_HOOK";;
esac

chmod +x $HOOK

Expand Down Expand Up @@ -285,7 +309,7 @@ else
fi

[ "$AUTHBIND_USR" = "$HTTP_USR" ] || chown $HTTP_USR $AUTHBIND80
systemctl start add-water
[ "$CTYPE" != "dns-01" ] && systemctl start add-water
info "running dehydrated"
if [ "$DEBUG" = "y" ] || [ "$LOG_INFO" = "y" ]; then
dehydrated --cron $args --config $CONFIG 2>&1 | tee -a $DEBUG_LOG
Expand Down
87 changes: 87 additions & 0 deletions plugins.d/Lets_Encrypt/dns_01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/python3
import sys
import subprocess
import re

from subprocess import PIPE
from os import path
from shutil import which

LEXICON_CONF = '/usr/share/confconsole/letsencrypt/lexicon.yml'

LEXICON_CONF_NOTE = '''# Configure according to lexicon documentation https://dns-lexicon.readthedocs.io/
# Note that documentation began around v.3.3.28 of lexicon. Therefore not all of
# the features might be available to you!
'''

LEXICON_CONF_MAX_LINES = 7

def load_config():
''' Loads lexicon config if present '''
config = []
if not path.isfile(LEXICON_CONF):
while len(config) < LEXICON_CONF_MAX_LINES:
config.append('')
return config
else:
with open(LEXICON_CONF, 'r') as fob:
for line in fob:
line = line.rstrip()
if line and not line.startswith('#'):
config.append(line)

while len(config) > LEXICON_CONF_MAX_LINES:
config.pop()
while len(config) < LEXICON_CONF_MAX_LINES:
config.append('')
return config

def save_config(config):
''' Saves lexicon configuration '''
with open(LEXICON_CONF, 'w') as fob:
fob.write(LEXICON_CONF_NOTE)
for line in config:
line = line.rstrip()
if line:
fob.write(line + '\n')

def get_providers():
lexicon_bin = which('lexicon')
if not lexicon_bin:
ret = console.yesno(
'lexicon tool is required to use dns-01 challenge, '
'however it is not found on your system.\n\n'
'Do you wish to install it now?',
autosize=True
)
if ret != 'ok':
return None, 'Please install lexicon to use dns-01 challenge.'

apt = subprocess.run(['apt-get', '-y', 'install', 'lexicon'],
encoding=sys.stdin.encoding,
stderr=PIPE)
if apt.returncode != 0:
return None, apt.stderr.strip()

lexicon_bin = which('lexicon')
if not lexicon_bin:
return None, 'lexicon is not found on your system, is it installed?'

proc = subprocess.run([lexicon_bin, '--help'],
encoding=sys.stdin.encoding,
capture_output=True)
if proc.returncode != 0:
return None, proc.stderr.strip()

match = re.search(r"(?<={).*(?=})", proc.stdout.strip())
if not match:
return None, 'Could not obtain DNS providers list from lexicon!'

providers = []
for provider in match.group().split(','):
if len(provider) > 0:
providers.append((provider, '%s provider' % provider))

if providers:
return providers, None
return None, 'DNS providers list is empty!'
98 changes: 83 additions & 15 deletions plugins.d/Lets_Encrypt/get_certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import requests
import sys
import subprocess

from subprocess import PIPE
from os import path, remove
from shutil import copyfile
from shutil import copyfile, which

import dns_01

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

Expand Down Expand Up @@ -72,6 +75,10 @@ def invalid_domains(domains):
if len(domain) > 254:
return ('Error in {}: Domain names must not exceed 254'
' characters'.format(domain))
if domain.count('.') < 1:
return ('Error in {}: Domain may not have less'
' than 2 segments'
''.format(domain))
for part in domain.split('.'):
if not 0 < len(part) < 64:
return ('Error in {}: Domain segments may not be larger'
Expand Down Expand Up @@ -100,16 +107,6 @@ def run():
console.msgbox('Error', msg, autosize=True)
return

ret = console.yesno(
'DNS must be configured before obtaining certificates. '
'Incorrectly configured dns and excessive attempts could '
'lead to being temporarily blocked from requesting '
'certificates.\n\nDo you wish to continue?',
autosize=True
)
if ret != 'ok':
return

ret = console.yesno(
"Before getting a Let's Encrypt certificate, you must agree"
' to the current Terms of Service.\n\n'
Expand All @@ -132,6 +129,74 @@ def run():
)
return

ret, challenge = console.menu('Challenge type',
'Select challenge type to use', [
('http-01', 'Requires public web access to this system'),
('dns-01', 'Requires your DNS provider to provide an API')
])
if ret != 'ok':
return

if challenge == 'http-01':
ret = console.yesno(
'DNS must be configured before obtaining certificates. '
'Incorrectly configured DNS and excessive attempts could '
'lead to being temporarily blocked from requesting '
'certificates.\n\nDo you wish to continue?',
autosize=True
)
if ret != 'ok':
return

if challenge == 'dns-01':
config = dns_01.load_config()
fields = [
('', 1, 0, config[0], 1, 10, field_width, 255),
('', 2, 0, config[1], 2, 10, field_width, 255),
('', 3, 0, config[2], 3, 10, field_width, 255),
('', 4, 0, config[3], 4, 10, field_width, 255),
('', 5, 0, config[4], 5, 10, field_width, 255),
('', 6, 0, config[5], 6, 10, field_width, 255),
('', 7, 0, config[6], 7, 10, field_width, 255),
]
ret, values = console.form('Lexicon configuration',
'Review and adjust current lexicon '
'configuration as necessary.\n\n'
'You can follow configuration reference at:\n'
'https://dns-lexicon.readthedocs.io/',
fields, autosize=True)
if ret != 'ok':
return

if config != values:
dns_01.save_config(values)

providers, err = dns_01.get_providers()
if err:
console.msgbox('Error', err, autosize=True)
return

ret, provider = console.menu('DNS providers list',
'Select DNS provider you\'d like to use',
providers)
if ret != 'ok':
return
elif provider == 'auto' and not which('nslookup'):
ret = console.yesno(
'nslookup tool is required to use dns-01 challenge with auto provider.\n\n'
'Do you wish to install it now?',
autosize=True
)
if ret != 'ok':
return

apt = subprocess.run(['apt-get', '-y', 'install', 'dnsutils'],
encoding=sys.stdin.encoding,
stderr=PIPE)
if apt.returncode != 0:
console.msgbox('Error', apt.stderr.strip(), autosize=True)
return

domains = load_domains()
m = invalid_domains(domains)

Expand Down Expand Up @@ -179,10 +244,13 @@ def run():

# User has accepted ToS as part of this process, so pass '--register'
# switch to Dehydrated wrapper
proc = subprocess.run(
['bash', path.join(
path.dirname(PLUGIN_PATH), 'dehydrated-wrapper'),
'--register'],
dehydrated_bin = ['bash', path.join(
path.dirname(PLUGIN_PATH), 'dehydrated-wrapper'),
'--register', '--challenge', challenge]
if challenge == 'dns-01':
dehydrated_bin.append('--provider')
dehydrated_bin.append(provider)
proc = subprocess.run(dehydrated_bin,
encoding=sys.stdin.encoding,
stderr=PIPE)
if proc.returncode == 0:
Expand Down
1 change: 1 addition & 0 deletions share/letsencrypt/dehydrated-confconsole.config
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ BASEDIR=/var/lib/dehydrated
WELLKNOWN="${BASEDIR}/acme-challenges"
DOMAINS_TXT="/etc/dehydrated/confconsole.domains.txt"
HOOK="/etc/dehydrated/confconsole.hook.sh"
CHALLENGETYPE="http-01"
Loading

0 comments on commit 07b5b1a

Please sign in to comment.