mirror of
https://github.com/dehydrated-io/dehydrated.git
synced 2026-04-27 18:57:02 +02:00
implemented initial support for tls-alpn-01 verification
This commit is contained in:
@@ -6,7 +6,7 @@ This file contains a log of major changes in dehydrated
|
|||||||
- OCSP refresh interval is now configurable
|
- OCSP refresh interval is now configurable
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
- ??
|
- Initial support for tls-alpn-01 validation
|
||||||
|
|
||||||
## [0.6.2] - 2018-04-25
|
## [0.6.2] - 2018-04-25
|
||||||
## Added
|
## Added
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ Parameters:
|
|||||||
--config (-f) path/to/config Use specified config file
|
--config (-f) path/to/config Use specified config file
|
||||||
--hook (-k) path/to/hook.sh Use specified script for hooks
|
--hook (-k) path/to/hook.sh Use specified script for hooks
|
||||||
--out (-o) certs/directory Output certificates into the specified directory
|
--out (-o) certs/directory Output certificates into the specified directory
|
||||||
|
--alpn alpn-certs/directory Output alpn verification certificates into the specified directory
|
||||||
--challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported
|
--challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported
|
||||||
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
|
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
|
||||||
```
|
```
|
||||||
|
|||||||
45
dehydrated
45
dehydrated
@@ -94,7 +94,7 @@ hookscript_bricker_hook() {
|
|||||||
|
|
||||||
# verify configuration values
|
# verify configuration values
|
||||||
verify_config() {
|
verify_config() {
|
||||||
[[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue."
|
[[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" || "${CHALLENGETYPE}" == "tls-alpn-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue."
|
||||||
if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
|
if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
|
||||||
_exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue."
|
_exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue."
|
||||||
fi
|
fi
|
||||||
@@ -126,6 +126,7 @@ load_config() {
|
|||||||
CA="https://acme-v02.api.letsencrypt.org/directory"
|
CA="https://acme-v02.api.letsencrypt.org/directory"
|
||||||
OLDCA=
|
OLDCA=
|
||||||
CERTDIR=
|
CERTDIR=
|
||||||
|
ALPNCERTDIR=
|
||||||
ACCOUNTDIR=
|
ACCOUNTDIR=
|
||||||
CHALLENGETYPE="http-01"
|
CHALLENGETYPE="http-01"
|
||||||
CONFIG_D=
|
CONFIG_D=
|
||||||
@@ -256,6 +257,7 @@ load_config() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs"
|
[[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs"
|
||||||
|
[[ -z "${ALPNCERTDIR}" ]] && ALPNCERTDIR="${BASEDIR}/alpn-certs"
|
||||||
[[ -z "${CHAINCACHE}" ]] && CHAINCACHE="${BASEDIR}/chains"
|
[[ -z "${CHAINCACHE}" ]] && CHAINCACHE="${BASEDIR}/chains"
|
||||||
[[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt"
|
[[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt"
|
||||||
[[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/dehydrated"
|
[[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/dehydrated"
|
||||||
@@ -266,6 +268,7 @@ load_config() {
|
|||||||
|
|
||||||
[[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}"
|
[[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}"
|
||||||
[[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}"
|
[[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}"
|
||||||
|
[[ -n "${PARAM_ALPNCERTDIR:-}" ]] && ALPNCERTDIR="${PARAM_ALPNCERTDIR}"
|
||||||
[[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}"
|
[[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}"
|
||||||
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
|
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
|
||||||
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
|
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
|
||||||
@@ -321,7 +324,7 @@ init_system() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Export some environment variables to be used in hook script
|
# Export some environment variables to be used in hook script
|
||||||
export WELLKNOWN BASEDIR CERTDIR CONFIG COMMAND
|
export WELLKNOWN BASEDIR CERTDIR ALPNCERTDIR CONFIG COMMAND
|
||||||
|
|
||||||
# Checking for private key ...
|
# Checking for private key ...
|
||||||
register_new_key="no"
|
register_new_key="no"
|
||||||
@@ -754,6 +757,10 @@ sign_csr() {
|
|||||||
# Generate DNS entry content for dns-01 validation
|
# Generate DNS entry content for dns-01 validation
|
||||||
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
|
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
|
||||||
;;
|
;;
|
||||||
|
"tls-alpn-01")
|
||||||
|
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $2}')"
|
||||||
|
generate_alpn_certificate "${identifier}" "${keyauth_hook}"
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
keyauths[${idx}]="${keyauth}"
|
keyauths[${idx}]="${keyauth}"
|
||||||
@@ -800,6 +807,7 @@ sign_csr() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
|
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
|
||||||
|
[[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem"
|
||||||
|
|
||||||
if [[ "${reqstatus}" = "valid" ]]; then
|
if [[ "${reqstatus}" = "valid" ]]; then
|
||||||
echo " + Challenge is valid!"
|
echo " + Challenge is valid!"
|
||||||
@@ -821,6 +829,8 @@ sign_csr() {
|
|||||||
while [ ${idx} -lt ${num_pending_challenges} ]; do
|
while [ ${idx} -lt ${num_pending_challenges} ]; do
|
||||||
# Delete challenge file
|
# Delete challenge file
|
||||||
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
|
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
|
||||||
|
# Delete alpn verification certificates
|
||||||
|
[[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem"
|
||||||
# Clean challenge token using non-chained hook
|
# Clean challenge token using non-chained hook
|
||||||
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
|
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
|
||||||
idx=$((idx+1))
|
idx=$((idx+1))
|
||||||
@@ -908,6 +918,27 @@ walk_chain() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Generate ALPN verification certificate
|
||||||
|
generate_alpn_certificate() {
|
||||||
|
local altname="${1}"
|
||||||
|
local acmevalidation="${2}"
|
||||||
|
|
||||||
|
local alpncertdir="${ALPNCERTDIR}"
|
||||||
|
if [[ ! -e "${alpncertdir}" ]]; then
|
||||||
|
echo " + Creating new directory ${alpncertdir} ..."
|
||||||
|
mkdir -p "${alpncertdir}" || _exiterr "Unable to create directory ${alpncertdir}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " + Generating ALPN certificate and key for ${1}..."
|
||||||
|
tmp_openssl_cnf="$(_mktemp)"
|
||||||
|
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
|
||||||
|
printf "[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
|
||||||
|
printf "1.3.6.1.5.5.7.1.30.1=critical,DER:04:20:${acmevalidation}\n" >> "${tmp_openssl_cnf}"
|
||||||
|
SUBJ="/CN=${altname}/"
|
||||||
|
[[ "${OSTYPE:0:5}" = "MINGW" ]] && SUBJ="/${SUBJ}"
|
||||||
|
_openssl req -x509 -new -sha256 -nodes -newkey rsa:2048 -keyout "${alpncertdir}/${altname}.key.pem" -out "${alpncertdir}/${altname}.crt.pem" -subj "${SUBJ}" -extensions SAN -config "${tmp_openssl_cnf}"
|
||||||
|
}
|
||||||
|
|
||||||
# Create certificate for domain(s)
|
# Create certificate for domain(s)
|
||||||
sign_domain() {
|
sign_domain() {
|
||||||
local certdir="${1}"
|
local certdir="${1}"
|
||||||
@@ -1514,7 +1545,7 @@ command_help() {
|
|||||||
command_env() {
|
command_env() {
|
||||||
echo "# dehydrated configuration"
|
echo "# dehydrated configuration"
|
||||||
load_config
|
load_config
|
||||||
typeset -p CA CERTDIR CHALLENGETYPE DOMAINS_D DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE
|
typeset -p CA CERTDIR ALPNCERTDIR CHALLENGETYPE DOMAINS_D DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main method (parses script arguments and calls command_* methods)
|
# Main method (parses script arguments and calls command_* methods)
|
||||||
@@ -1693,6 +1724,14 @@ main() {
|
|||||||
PARAM_CERTDIR="${1}"
|
PARAM_CERTDIR="${1}"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
# PARAM_Usage: --alpn alpn-certs/directory
|
||||||
|
# PARAM_Description: Output alpn verification certificates into the specified directory
|
||||||
|
--alpn)
|
||||||
|
shift 1
|
||||||
|
check_parameters "${1:-}"
|
||||||
|
PARAM_ALPNCERTDIR="${1}"
|
||||||
|
;;
|
||||||
|
|
||||||
# PARAM_Usage: --challenge (-t) http-01|dns-01
|
# PARAM_Usage: --challenge (-t) http-01|dns-01
|
||||||
# PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
|
# PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
|
||||||
--challenge|-t)
|
--challenge|-t)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
# default: https://acme-v01.api.letsencrypt.org/directory
|
# default: https://acme-v01.api.letsencrypt.org/directory
|
||||||
#OLDCA="https://acme-v01.api.letsencrypt.org/directory"
|
#OLDCA="https://acme-v01.api.letsencrypt.org/directory"
|
||||||
|
|
||||||
# Which challenge should be used? Currently http-01 and dns-01 are supported
|
# Which challenge should be used? Currently http-01, dns-01 and tls-alpn-01 are supported
|
||||||
#CHALLENGETYPE="http-01"
|
#CHALLENGETYPE="http-01"
|
||||||
|
|
||||||
# Path to a directory containing additional config files, allowing to override
|
# Path to a directory containing additional config files, allowing to override
|
||||||
@@ -49,6 +49,9 @@
|
|||||||
# Output directory for generated certificates
|
# Output directory for generated certificates
|
||||||
#CERTDIR="${BASEDIR}/certs"
|
#CERTDIR="${BASEDIR}/certs"
|
||||||
|
|
||||||
|
# Output directory for alpn verification certificates
|
||||||
|
#ALPNCERTDIR="${BASEDIR}/alpn-certs"
|
||||||
|
|
||||||
# Directory for account keys and registration information
|
# Directory for account keys and registration information
|
||||||
#ACCOUNTDIR="${BASEDIR}/accounts"
|
#ACCOUNTDIR="${BASEDIR}/accounts"
|
||||||
|
|
||||||
|
|||||||
106
docs/tls-alpn.md
Normal file
106
docs/tls-alpn.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# TLS-ALPN-01
|
||||||
|
|
||||||
|
With `tls-alpn-01`-type verification Let's Encrypt (or the ACME-protocol in general) is checking if you are in control of a domain by accessing
|
||||||
|
your webserver using a custom ALPN and expecting a specially crafted TLS certificate containing a verification token.
|
||||||
|
It will do that for any (sub-)domain you want to sign a certificate for.
|
||||||
|
|
||||||
|
Dehydrated generates the required verification certificates, but the delivery is out of its scope.
|
||||||
|
|
||||||
|
### Example nginx config
|
||||||
|
|
||||||
|
On an nginx tcp load-balancer you can use the `ssl_preread` module to map a different port for acme-tls
|
||||||
|
requests than for e.g. HTTP/2 or HTTP/1.1 requests.
|
||||||
|
|
||||||
|
Your config should look something like this:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
stream {
|
||||||
|
server {
|
||||||
|
map $ssl_preread_alpn_protocols $tls_port {
|
||||||
|
~\bacme-tls/1\b 10443;
|
||||||
|
default 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443;
|
||||||
|
listen [::]:443;
|
||||||
|
proxy_pass 10.13.37.42:$tls_port;
|
||||||
|
ssl_preread on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That way https requests are forwarded to port 443 on the backend server, and acme-tls/1 requests are
|
||||||
|
forwarded to port 10443.
|
||||||
|
|
||||||
|
In the future nginx might support internal routing based on custom ALPNs, but for now you'll have to
|
||||||
|
use a custom responder for the alpn verification certificates (see below).
|
||||||
|
|
||||||
|
### Example responder
|
||||||
|
|
||||||
|
I hacked together a simple responder in Python, it might not be the best, but it works for me:
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import ssl
|
||||||
|
import socketserver
|
||||||
|
import threading
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
|
||||||
|
ALPNDIR="/etc/dehydrated/alpn-certs"
|
||||||
|
PROXY_PROTOCOL=False
|
||||||
|
|
||||||
|
FALLBACK_CERTIFICATE="/etc/ssl/certs/ssl-cert-snakeoil.pem"
|
||||||
|
FALLBACK_KEY="/etc/ssl/private/ssl-cert-snakeoil.key"
|
||||||
|
|
||||||
|
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
|
||||||
|
def create_context(self, certfile, keyfile, first=False):
|
||||||
|
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||||
|
ssl_context.set_ciphers('ECDHE+AESGCM')
|
||||||
|
ssl_context.set_alpn_protocols(["acme-tls/1"])
|
||||||
|
ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
|
||||||
|
if first:
|
||||||
|
ssl_context.set_servername_callback(self.load_certificate)
|
||||||
|
ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
|
||||||
|
return ssl_context
|
||||||
|
|
||||||
|
def load_certificate(self, sslsocket, sni_name, sslcontext):
|
||||||
|
print("Got request for %s" % sni_name)
|
||||||
|
if not re.match(r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$', sni_name):
|
||||||
|
return
|
||||||
|
|
||||||
|
certfile = os.path.join(ALPNDIR, "%s.crt.pem" % sni_name)
|
||||||
|
keyfile = os.path.join(ALPNDIR, "%s.key.pem" % sni_name)
|
||||||
|
|
||||||
|
if not os.path.exists(certfile) or not os.path.exists(keyfile):
|
||||||
|
return
|
||||||
|
|
||||||
|
sslsocket.context = self.create_context(certfile, keyfile)
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
if PROXY_PROTOCOL:
|
||||||
|
buf = b""
|
||||||
|
while b"\r\n" not in buf:
|
||||||
|
buf += self.request.recv(1)
|
||||||
|
|
||||||
|
ssl_context = self.create_context(FALLBACK_CERTIFICATE, FALLBACK_KEY, True)
|
||||||
|
newsock = ssl_context.wrap_socket(self.request, server_side=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
HOST, PORT = "0.0.0.0", 10443
|
||||||
|
|
||||||
|
server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler, bind_and_activate=False)
|
||||||
|
server.allow_reuse_address = True
|
||||||
|
try:
|
||||||
|
server.server_bind()
|
||||||
|
server.server_activate()
|
||||||
|
server.serve_forever()
|
||||||
|
except:
|
||||||
|
server.shutdown()
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user