46 Commits

Author SHA1 Message Date
Lukas Schauer
05eda91a2f release 0.6.5 (fixed apiv1 compatibility...) 2019-06-26 12:33:35 +02:00
Lukas Schauer
4f358e22f4 release 0.6.4 (fixed account id handling, again) 2019-06-25 15:28:09 +02:00
Lukas Schauer
f9d0b1bd70 release 0.6.3 2019-06-25 12:50:45 +02:00
Lukas Schauer
be13dcd454 fixed fetching of account information (fixes #652, fixes #647, fixes #650, closes #648) 2019-06-25 12:19:20 +02:00
Lukas Schauer
74a536c161 added documentation about possible future removal of api version 1 2019-03-04 23:23:40 +01:00
Lukas Schauer
444cea4669 Revert "cleanup: removed api version 1 support (closes #510)"
Since a few CAs out there actually seem to (only) support ACME v1 I
decided to revert the removal and keep ACME v1 around, at least until
it eventually becomes a bigger inconvenience to maintain.

This reverts commit aadf7d5e64.
2019-03-04 23:11:07 +01:00
Lukas Schauer
ea93170959 BSD bugfixes for version command (closes #619) 2019-03-03 21:51:01 +01:00
Lukas Schauer
133e31de0b tiny documentation fix: per-certificate-config can override PRIVATE_KEY_ROLLOVER (closes #614) 2019-03-03 20:38:47 +01:00
Lukas Schauer
aadf7d5e64 cleanup: removed api version 1 support (closes #510) 2019-03-03 20:32:10 +01:00
Lukas Schauer
e4a32acbe2 new hook: sync_cert (closes #609) 2019-03-03 20:22:41 +01:00
Lukas Schauer
1c77730373 call exit_hook with error message (fixes #630) 2019-03-03 20:08:18 +01:00
Lukas Schauer
e623fcc024 implement POST-as-GET (closes #626) 2019-03-03 20:00:13 +01:00
Lukas Schauer
585ed5404b updated oid for tls-alpn verification token (fixes #624) 2019-01-18 13:25:57 +01:00
Lukas Schauer
92aa1ecd5a document DOMAINS_D parameter in example config (fixes #575, closes #582) 2018-10-20 13:05:20 +02:00
Lukas Schauer
5783a2dd45 fixed a bug that resulted in a deleted domains.txt when using incorrect parameters in combination with signcsr (fixes #597) 2018-10-20 12:27:23 +02:00
Lukas Schauer
fba49ba28e implemented initial support for tls-alpn-01 verification 2018-07-26 04:44:29 +02:00
Lukas Schauer
10d4b98e7f Only match Replace-Nonce header at beginning of line 2018-05-09 21:01:57 +02:00
Florent
e4e712c03a Fixes #559 : when HTTP/2 is used, header names are lower case. So adding ignore case option (-i) to grep's. 2018-05-09 21:00:05 +02:00
Lukas Schauer
2a8af8fda7 made ocsp refresh interval configurable 2018-05-07 03:31:43 +02:00
Lukas Schauer
9165cfdebf added dns-txt-foo to troubleshooting.md 2018-05-01 17:54:13 +02:00
Lukas Schauer
b5dddd7a2b prepare for next version 2018-04-27 13:08:44 +02:00
Lukas Schauer
ce3d658377 release 0.6.2 2018-04-25 23:22:40 +02:00
Lukas Schauer
89de83c994 add explanation on HEADERS parameter to request_failure hook (fixes #545) 2018-04-25 22:48:16 +02:00
Lukas Schauer
52c2c19994 added workaround for use with advanced filesystem ACLs (as originally suggested in #467) 2018-04-20 02:55:07 +02:00
Tobias Tiederle
53c458c318 fix parameter 2018-04-15 16:08:30 +02:00
Lukas Schauer
ba5928776f fix behaviour for fetching missing additional account information with unknown keys 2018-04-13 22:09:52 +02:00
Lukas Schauer
dff7d4ea35 allow registration using cli-specified private key (#534) 2018-04-13 21:34:11 +02:00
Lukas Schauer
0262997451 also call clean_challenge hook for http-01 challenges (fixes #536) 2018-04-13 20:59:27 +02:00
Lukas Schauer
8ba56a8048 renamed ocsp hook to deploy_ocsp, exported altnames, added example hook 2018-04-08 22:44:28 +02:00
Ike Johnson
2fca309e94 Add ocsp_update hook
In relation to issue #513
2018-04-08 22:31:56 +02:00
Daniel Molkentin
13b8a3f29f fix date in man page 2018-04-08 22:21:33 +02:00
Lukas Schauer
a67816468a also run request_failure and invalid_challenge when HOOK_CHAIN is defined (fixes #450) 2018-04-08 22:18:30 +02:00
Lukas Schauer
ea46aee44f fixed cleanup for invalid challenges 2018-04-08 22:18:16 +02:00
Lukas Schauer
537877a0e2 allow for widely-used non-standard CSR label (fixes #488) 2018-03-26 00:00:50 +02:00
Lukas Schauer
981179a770 hail hydra! 🐙 2018-03-24 16:18:01 +01:00
Lukas Schauer
ff18d39aa8 strip validationRecord from challenge before grepping for "url" string... (fixes #515) 2018-03-18 20:12:04 +01:00
John L. Villalovos
7c40c727a0 Improve documentation on wildcards
Improve the documentation on how to use wildcard domains.

Also give more examples in the docs/examples/domains.txt file.
2018-03-17 13:27:15 +01:00
Lukas Schauer
9f1ff67870 removed dual use of challenge_identifiers variable (fixes #511) 2018-03-17 01:40:19 +01:00
Lukas Schauer
b116e6bc2b close weird external file descriptors 2018-03-15 13:52:51 +01:00
Lukas Schauer
6083218501 removed some unused code 2018-03-15 13:52:15 +01:00
Lukas Schauer
2533931cf1 don't walk certificate chain for ACMEv2 (certificate contains chain by default) 2018-03-14 18:54:51 +01:00
Lukas Schauer
b93eac3893 fixed CA url in example config 2018-03-13 21:08:20 +01:00
Lukas Schauer
e374d21d45 prepare for next version 2018-03-13 20:59:20 +01:00
Lukas Schauer
70d261a729 release v0.6.1 2018-03-13 20:57:52 +01:00
Lukas Schauer
947dbb9e29 use new acme-v02 endpoint by default 2018-03-13 20:48:42 +01:00
Lukas Schauer
8a414e55bc prepare for next version 2018-03-11 20:22:38 +01:00
12 changed files with 513 additions and 103 deletions

View File

@@ -1,6 +1,47 @@
# Change Log
This file contains a log of major changes in dehydrated
## [0.6.5] - 2019-06-26
## Fixed
- Fixed broken APIv1 compatibility from last update
## [0.6.4] - 2019-06-25
## Changed
- Fetch account ID from Location header instead of account json
## [0.6.3] - 2019-06-25
## Changed
- OCSP refresh interval is now configurable
- Implemented POST-as-GET
- Call exit_hook on errors (with error-message as first parameter)
## Added
- Initial support for tls-alpn-01 validation
- New hook: sync_cert (for syncing certificate files to disk, see example hook description)
## Fixes
- Fetch account information after registration to avoid missing account id
## [0.6.2] - 2018-04-25
## Added
- New deploy_ocsp hook
- Allow account registration with custom key
## Changed
- Don't walk certificate chain for ACMEv2 (certificate contains chain by default)
- Improved documentation on wildcards
## Fixes
- Added workaround for compatibility with filesystem ACLs
- Close unwanted external file-descriptors
- Fixed JSON parsing on force-renewal
- Fixed cleanup of challenge files/dns-entries on validation errors
- A few more minor fixes
## [0.6.1] - 2018-03-13
## Changed
- Use new ACME v2 endpoint by default
## [0.6.0] - 2018-03-11
## Changed
- Challenge validation loop has been modified to loop over authorization identifiers instead of altnames (ACMEv2 + wildcard support)

View File

@@ -2,20 +2,21 @@
![](docs/logo.jpg)
This is a client for signing certificates with an ACME-server (currently only provided by Let's Encrypt) implemented as a relatively simple bash-script.
Dehydrated supports both ACME v1 and the new ACME v2 including support for wildcard certificates!
Dehydrated is a client for signing certificates with an ACME-server (e.g. Let's Encrypt) implemented as a relatively simple (zsh-compatible) bash-script.
This client supports both ACME v1 and the new ACME v2 including support for wildcard certificates!
It uses the `openssl` utility for everything related to actually handling keys and certificates, so you need to have that installed.
Other dependencies are: cURL, sed, grep, mktemp (all found on almost any system, cURL being the only exception)
Other dependencies are: cURL, sed, grep, awk, mktemp (all found pre-installed on almost any system, cURL being the only exception).
Current features:
- Signing of a list of domains
- Signing of a CSR
- Renewal if a certificate is about to expire or SAN (subdomains) changed
- Signing of a list of domains (including wildcard domains!)
- Signing of a custom CSR (either standalone or completely automated using hooks!)
- Renewal if a certificate is about to expire or defined set of domains changed
- Certificate revocation
Please keep in mind that this software and even the acme-protocol are relatively young and may still have some unresolved issues. Feel free to report any issues you find with this script or contribute by submitting a pull request.
Please keep in mind that this software, the ACME-protocol and all supported CA servers out there are relatively young and there might be a few issues. Feel free to report any issues you find with this script or contribute by submitting a pull request,
but please check for duplicates first (feel free to comment on those to get things rolling).
## Getting started
@@ -73,6 +74,7 @@ Parameters:
--config (-f) path/to/config Use specified config file
--hook (-k) path/to/hook.sh Use specified script for hooks
--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
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
```
@@ -87,12 +89,12 @@ Without those hobbies I probably would never have started working on dehydrated
I'd really appreciate if you could [donate a bit of money](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8)
so I can buy cool stuff (while still being able to afford food :D).
If you have hardware laying around that you think I'd enjoy playing with (e.g. decomissioned but still modern-ish servers,
If you have hardware laying around that you think I'd enjoy playing with (e.g. decommissioned but still modern-ish servers,
10G networking hardware, enterprise grade routers or APs, interesting ARM/MIPS boards, etc.) and that you would be willing
to ship to me please contact me at `donations@dehydrated.de` or on Twitter [@lukas2511](https://twitter.com/lukas2511).
to ship to me please contact me at `donations@dehydrated.io` or on Twitter [@lukas2511](https://twitter.com/lukas2511).
If you want your name to be added to the [donations list](https://dehydrated.de/donations.html) please add a note or send me an
email `donations@dehydrated.de`. I respect your privacy and won't publish your name without permission.
If you want your name to be added to the [donations list](https://dehydrated.io/donations.html) please add a note or send me an
email `donations@dehydrated.io`. I respect your privacy and won't publish your name without permission.
Other ways of donating:
- [My Amazon Wishlist](http://www.amazon.de/registry/wishlist/1TUCFJK35IO4Q)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# dehydrated by lukas2511
# Source: https://dehydrated.de
# Source: https://dehydrated.io
#
# This script is licensed under The MIT License (see LICENSE for more information).
@@ -13,7 +13,11 @@ set -o pipefail
umask 077 # paranoid umask, we're creating private keys
VERSION="0.6.0"
# Close weird external file descriptors
exec 3>&-
exec 4>&-
VERSION="0.6.5"
# Find directory in which this script is stored by traversing all symbolic links
SOURCE="${0}"
@@ -90,7 +94,7 @@ hookscript_bricker_hook() {
# verify configuration values
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
_exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue."
fi
@@ -102,6 +106,7 @@ verify_config() {
[[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue."
fi
[[ "${API}" == "auto" || "${API}" == "1" || "${API}" == "2" ]] || _exiterr "Unsupported API version defined in config: ${API}"
[[ "${OCSP_DAYS}" =~ ^[0-9]+$ ]] || _exiterr "OCSP_DAYS must be a number"
}
# Setup default config values, search for and load configuration files
@@ -118,9 +123,10 @@ load_config() {
fi
# Default values
CA="https://acme-v01.api.letsencrypt.org/directory"
CA="https://acme-v02.api.letsencrypt.org/directory"
OLDCA=
CERTDIR=
ALPNCERTDIR=
ACCOUNTDIR=
CHALLENGETYPE="http-01"
CONFIG_D=
@@ -141,6 +147,7 @@ load_config() {
LOCKFILE=
OCSP_MUST_STAPLE="no"
OCSP_FETCH="no"
OCSP_DAYS=5
IP_VERSION=
CHAINCACHE=
AUTO_CLEANUP="no"
@@ -218,6 +225,10 @@ load_config() {
# Check BASEDIR and set default variables
[[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}"
if [[ -z "${OLDCA}" ]] && [[ "${CA}" = "https://acme-v02.api.letsencrypt.org/directory" ]]; then
OLDCA="https://acme-v01.api.letsencrypt.org/directory"
fi
# Create new account directory or symlink to account directory from old CA
CAHASH="$(echo "${CA}" | urlbase64)"
[[ -z "${ACCOUNTDIR}" ]] && ACCOUNTDIR="${BASEDIR}/accounts"
@@ -235,6 +246,7 @@ load_config() {
[[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json"
ACCOUNT_ID_JSON="${ACCOUNTDIR}/${CAHASH}/account_id.json"
if [[ -f "${BASEDIR}/private_key.pem" ]] && [[ ! -f "${ACCOUNT_KEY}" ]]; then
echo "! Moving private_key.pem to ${ACCOUNT_KEY}"
@@ -246,6 +258,7 @@ load_config() {
fi
[[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs"
[[ -z "${ALPNCERTDIR}" ]] && ALPNCERTDIR="${BASEDIR}/alpn-certs"
[[ -z "${CHAINCACHE}" ]] && CHAINCACHE="${BASEDIR}/chains"
[[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt"
[[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/dehydrated"
@@ -256,6 +269,7 @@ load_config() {
[[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}"
[[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}"
[[ -n "${PARAM_ALPNCERTDIR:-}" ]] && ALPNCERTDIR="${PARAM_ALPNCERTDIR}"
[[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}"
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
@@ -311,7 +325,7 @@ init_system() {
fi
# 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 ...
register_new_key="no"
@@ -320,6 +334,8 @@ init_system() {
echo "Using private key ${PARAM_ACCOUNT_KEY} instead of account key"
ACCOUNT_KEY="${PARAM_ACCOUNT_KEY}"
ACCOUNT_KEY_JSON="${PARAM_ACCOUNT_KEY}.json"
ACCOUNT_ID_JSON="${PARAM_ACCOUNT_KEY}_id.json"
[ "${COMMAND:-}" = "register" ] && register_new_key="yes"
else
# Check if private account key exists, if it doesn't exist yet generate a new one (rsa key)
if [[ ! -e "${ACCOUNT_KEY}" ]]; then
@@ -331,7 +347,10 @@ init_system() {
fi
echo "+ Generating account key..."
_openssl genrsa -out "${ACCOUNT_KEY}" "${KEYSIZE}"
local tmp_account_key="$(_mktemp)"
_openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}"
cat "${tmp_account_key}" > "${ACCOUNT_KEY}"
rm "${tmp_account_key}"
register_new_key="yes"
fi
fi
@@ -384,19 +403,29 @@ init_system() {
# Read account information or request from CA if missing
if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then
ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)"
if [[ ${API} -eq 1 ]]; then
ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)"
ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}"
else
if [[ -e "${ACCOUNT_ID_JSON}" ]]; then
ACCOUNT_ID="$(cat "${ACCOUNT_ID_JSON}" | get_json_string_value id)"
else
echo "+ Fetching account ID..."
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
ACCOUNT_ID="${ACCOUNT_URL##*/}"
if [[ -z "${ACCOUNT_ID}" ]]; then
_exiterr "Unknown error on fetching account information"
fi
echo '{"id": "'"${ACCOUNT_ID}"'"}' > "${ACCOUNT_ID_JSON}"
fi
ACCOUNT_URL="${CA_ACCOUNT}/${ACCOUNT_ID}"
fi
else
echo "Fetching missing account information from CA..."
if [[ ${API} -eq 1 ]]; then
ACCOUNT_URL="$(signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "onlyReturnExisting": true}' 4>&1 | grep ^Location: | awk '{print $2}' | tr -d '\r\n')"
ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{"resource": "reg"}')"
_exiterr "This is not implemented for ACMEv1! Consider switching to ACMEv2 :)"
else
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"only-return-existing": true}' 4>&1 | grep ^Location: | awk '{print $2}' | tr -d '\r\n')"
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{}')"
fi
ACCOUNT_ID="${ACCOUNT_URL##*/}"
@@ -416,6 +445,7 @@ _sed() {
# Print error message and exit with error
_exiterr() {
echo "ERROR: ${1}" >&2
[[ -n "${HOOK:-}" ]] && "${HOOK}" "exit_hook" "${1}" || true
exit 1
}
@@ -499,14 +529,14 @@ http_request() {
set +e
if [[ "${1}" = "head" ]]; then
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydr4ted/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
touch "${tempheaders}"
curlret="${?}"
elif [[ "${1}" = "get" ]]; then
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydr4ted/${VERSION} curl/${CURL_VERSION}" -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" "${2}")"
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" "${2}")"
curlret="${?}"
elif [[ "${1}" = "post" ]]; then
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydr4ted/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -D "${tempheaders}" -H 'Content-Type: application/jose+json' -d "${3}")"
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -D "${tempheaders}" -H 'Content-Type: application/jose+json' -d "${3}")"
curlret="${?}"
else
set -e
@@ -526,10 +556,6 @@ http_request() {
# check for already-revoked warning
elif [[ -n "${CA_REVOKE_CERT:-}" ]] && [[ "${2}" = "${CA_REVOKE_CERT:-}" ]] && [[ "${statuscode}" = "409" ]]; then
grep -q "Certificate already revoked" "${tempcont}" && return
# check for redirects on license
elif [[ -n "${CA_TERMS:-}" ]] && [[ "${2}" = "${CA_TERMS:-}" ]] && [[ "${statuscode:0:1}" = "3" ]]; then
# do nothing
:
else
echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
echo >&2
@@ -540,7 +566,7 @@ http_request() {
echo >&2
# An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
if [[ -n "${HOOK}" ]]; then
errtxt="$(cat ${tempcont})"
errheaders="$(cat ${tempheaders})"
"${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}"
@@ -549,13 +575,8 @@ http_request() {
rm -f "${tempcont}"
rm -f "${tempheaders}"
# Wait for hook script to clean the challenge if used
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token:+set}" ]]; then
"${HOOK}" "clean_challenge" '' "${challenge_token}" "${keyauth}"
fi
# remove temporary domains.txt file if used
[[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}"
[[ "${COMMAND:-}" = "sign_domains" && -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}"
exit 1
fi
fi
@@ -575,9 +596,9 @@ signed_request() {
# Retrieve nonce from acme-server
if [[ ${API} -eq 1 ]]; then
nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
else
nonce="$(http_request head "${CA_NEW_NONCE}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
fi
# Build header with just our public key and algorithm information
@@ -663,14 +684,15 @@ sign_csr() {
fi
if [[ -n "${ZSH_VERSION:-}" ]]; then
local -A challenge_identifiers challenge_uris challenge_tokens authorizations keyauths deploy_args
local -A challenge_names challenge_uris challenge_tokens authorizations keyauths deploy_args
else
local -a challenge_identifiers challenge_uris challenge_tokens authorizations keyauths deploy_args
local -a challenge_names challenge_uris challenge_tokens authorizations keyauths deploy_args
fi
# Initial step: Find which authorizations we're dealing with
if [[ ${API} -eq 2 ]]; then
# Request new order and store authorization URIs
local challenge_identifiers=""
for altname in ${altnames}; do
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
done
@@ -702,7 +724,7 @@ sign_csr() {
for authorization in ${authorizations[*]}; do
if [[ "${API}" -eq 2 ]]; then
# Receive authorization ($authorization is authz uri)
response="$(http_request get "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" | clean_json)"
response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | clean_json)"
identifier="$(echo "${response}" | get_json_dict_value identifier | get_json_string_value value)"
echo " + Handling authorization for ${identifier}"
else
@@ -727,12 +749,12 @@ sign_csr() {
fi
# Gather challenge information
challenge_identifiers[${idx}]="${identifier}"
challenge_names[${idx}]="${identifier}"
challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)"
if [[ ${API} -eq 2 ]]; then
challenge_uris[${idx}]="$(echo "${challenge}" | get_json_string_value url)"
challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value url)"
else
challenge_uris[${idx}]="$(echo "${challenge}" | get_json_string_value uri)"
challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value uri)"
fi
# Prepare challenge tokens and deployment parameters
@@ -749,6 +771,10 @@ sign_csr() {
# Generate DNS entry content for dns-01 validation
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
keyauths[${idx}]="${keyauth}"
@@ -777,7 +803,7 @@ sign_csr() {
# Validate pending challenges
local idx=0
while [ ${idx} -lt ${num_pending_challenges} ]; do
echo " + Responding to challenge for ${challenge_identifiers[${idx}]} authorization..."
echo " + Responding to challenge for ${challenge_names[${idx}]} authorization..."
# Ask the acme-server to verify our challenge and wait until it is no longer pending
if [[ ${API} -eq 1 ]]; then
@@ -790,40 +816,46 @@ sign_csr() {
while [[ "${reqstatus}" = "pending" ]]; do
sleep 1
result="$(http_request get "${challenge_uris[${idx}]}")"
if [[ "${API}" -eq 2 ]]; then
result="$(signed_request "${challenge_uris[${idx}]}" "")"
else
result="$(http_request get "${challenge_uris[${idx}]}")"
fi
reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
done
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
# Run hook script to clean the challenge token
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
# shellcheck disable=SC2086
"${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
fi
idx=$((idx+1))
[[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem"
if [[ "${reqstatus}" = "valid" ]]; then
echo " + Challenge is valid!"
else
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "invalid_challenge" "${altname}" "${result}"
[[ -n "${HOOK}" ]] && "${HOOK}" "invalid_challenge" "${altname}" "${result}"
break
fi
idx=$((idx+1))
done
if [[ ${num_pending_challenges} -ne 0 ]]; then
echo " + Cleaning challenge tokens..."
# Clean challenge tokens using chained hook
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[@]}
# Clean remaining challenge tokens if validation has failed
if [[ "${reqstatus}" != "valid" ]]; then
if [[ "${CHALLENGETYPE}" = "http-01" ]] && [[ ${num_pending_challenges} -ne 0 ]]; then
while [ ${idx} -lt ${num_pending_challenges} ]; do
rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
idx=$((idx+1))
done
fi
local idx=0
while [ ${idx} -lt ${num_pending_challenges} ]; do
# Delete challenge file
[[ "${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
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
idx=$((idx+1))
done
if [[ "${reqstatus}" != "valid" ]]; then
echo " + Challenge validation has failed :("
_exiterr "Challenge is invalid! (returned: ${reqstatus}) (result: ${result})"
fi
fi
@@ -836,7 +868,7 @@ sign_csr() {
crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
else
result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | clean_json | get_json_string_value certificate)"
crt="$(http_request get "${result}")"
crt="$(signed_request "${result}" "")"
fi
# Try to load the certificate to detect corruption
@@ -904,6 +936,27 @@ walk_chain() {
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.31=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)
sign_domain() {
local certdir="${1}"
@@ -930,10 +983,13 @@ sign_domain() {
if [[ ! -r "${certdir}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
echo " + Generating private key..."
privkey="privkey-${timestamp}.pem"
local tmp_privkey="$(_mktemp)"
case "${KEY_ALGO}" in
rsa) _openssl genrsa -out "${certdir}/privkey-${timestamp}.pem" "${KEYSIZE}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${certdir}/privkey-${timestamp}.pem";;
rsa) _openssl genrsa -out "${tmp_privkey}" "${KEYSIZE}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${tmp_privkey}";;
esac
cat "${tmp_privkey}" > "${certdir}/privkey-${timestamp}.pem"
rm "${tmp_privkey}"
fi
# move rolloverkey into position (if any)
if [[ -r "${certdir}/privkey.pem" && -r "${certdir}/privkey.roll.pem" && "${PRIVATE_KEY_RENEW}" = "yes" && "${PRIVATE_KEY_ROLLOVER}" = "yes" ]]; then
@@ -986,20 +1042,33 @@ sign_domain() {
# Create fullchain.pem
echo " + Creating fullchain.pem..."
cat "${crt_path}" > "${certdir}/fullchain-${timestamp}.pem"
local issuer_hash
issuer_hash="$(get_issuer_hash "${crt_path}")"
if [ -e "${CHAINCACHE}/${issuer_hash}.chain" ]; then
echo " + Using cached chain!"
cat "${CHAINCACHE}/${issuer_hash}.chain" > "${certdir}/chain-${timestamp}.pem"
if [[ ${API} -eq 1 ]]; then
cat "${crt_path}" > "${certdir}/fullchain-${timestamp}.pem"
local issuer_hash
issuer_hash="$(get_issuer_hash "${crt_path}")"
if [ -e "${CHAINCACHE}/${issuer_hash}.chain" ]; then
echo " + Using cached chain!"
cat "${CHAINCACHE}/${issuer_hash}.chain" > "${certdir}/chain-${timestamp}.pem"
else
echo " + Walking chain..."
local issuer_cert_uri
issuer_cert_uri="$(get_issuer_cert_uri "${crt_path}" || echo "unknown")"
(walk_chain "${crt_path}" > "${certdir}/chain-${timestamp}.pem") || _exiterr "Walking chain has failed, your certificate has been created and can be found at ${crt_path}, the corresponding private key at ${privkey}. If you want you can manually continue on creating and linking all necessary files. If this error occurs again you should manually generate the certificate chain and place it under ${CHAINCACHE}/${issuer_hash}.chain (see ${issuer_cert_uri})"
cat "${certdir}/chain-${timestamp}.pem" > "${CHAINCACHE}/${issuer_hash}.chain"
fi
cat "${certdir}/chain-${timestamp}.pem" >> "${certdir}/fullchain-${timestamp}.pem"
else
echo " + Walking chain..."
local issuer_cert_uri
issuer_cert_uri="$(get_issuer_cert_uri "${crt_path}" || echo "unknown")"
(walk_chain "${crt_path}" > "${certdir}/chain-${timestamp}.pem") || _exiterr "Walking chain has failed, your certificate has been created and can be found at ${crt_path}, the corresponding private key at ${privkey}. If you want you can manually continue on creating and linking all necessary files. If this error occurs again you should manually generate the certificate chain and place it under ${CHAINCACHE}/${issuer_hash}.chain (see ${issuer_cert_uri})"
cat "${certdir}/chain-${timestamp}.pem" > "${CHAINCACHE}/${issuer_hash}.chain"
tmpcert="$(_mktemp)"
tmpchain="$(_mktemp)"
awk '{print >out}; /----END CERTIFICATE-----/{out=tmpchain}' out="${tmpcert}" tmpchain="${tmpchain}" "${certdir}/cert-${timestamp}.pem"
mv "${certdir}/cert-${timestamp}.pem" "${certdir}/fullchain-${timestamp}.pem"
cat "${tmpcert}" > "${certdir}/cert-${timestamp}.pem"
cat "${tmpchain}" > "${certdir}/chain-${timestamp}.pem"
rm "${tmpcert}" "${tmpchain}"
fi
cat "${certdir}/chain-${timestamp}.pem" >> "${certdir}/fullchain-${timestamp}.pem"
# Wait for hook script to sync the files before creating the symlinks
[[ -n "${HOOK}" ]] && "${HOOK}" "sync_cert" "${certdir}/privkey-${timestamp}.pem" "${certdir}/cert-${timestamp}.pem" "${certdir}/fullchain-${timestamp}.pem" "${certdir}/chain-${timestamp}.pem" "${certdir}/cert-${timestamp}.csr"
# Update symlinks
[[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${certdir}/privkey.pem"
@@ -1022,13 +1091,13 @@ command_version() {
load_config noverify
echo "Dehydrated by Lukas Schauer"
echo "https://dehydrated.de"
echo "https://dehydrated.io"
echo ""
echo "Dehydrated version: ${VERSION}"
revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")"
echo "GIT-Revision: ${revision}"
echo ""
if [[ "${OSTYPE}" = "FreeBSD" ]]; then
if [[ "${OSTYPE}" =~ "BSD" ]]; then
echo "OS: $(uname -sr)"
else
echo "OS: $(cat /etc/issue | grep -v ^$ | head -n1 | _sed 's/\\(r|n|l) .*//g')"
@@ -1037,15 +1106,15 @@ command_version() {
[[ -n "${BASH_VERSION:-}" ]] && echo " bash: ${BASH_VERSION}"
[[ -n "${ZSH_VERSION:-}" ]] && echo " zsh: ${ZSH_VERSION}"
echo " curl: $(curl --version 2>&1 | head -n1 | cut -d" " -f1-2)"
if [[ "${OSTYPE}" = "FreeBSD" ]]; then
echo " awk, sed, mktemp: FreeBSD base system versions"
if [[ "${OSTYPE}" =~ "BSD" ]]; then
echo " awk, sed, mktemp, grep, diff: BSD base system versions"
else
echo " awk: $(awk -W version 2>&1 | head -n1)"
echo " sed: $(sed --version 2>&1 | head -n1)"
echo " mktemp: $(mktemp --version 2>&1 | head -n1)"
echo " grep: $(grep --version 2>&1 | head -n1)"
echo " diff: $(diff --version 2>&1 | head -n1)"
fi
echo " grep: $(grep --version 2>&1 | head -n1)"
echo " diff: $(diff --version 2>&1 | head -n1)"
echo " openssl: $("${OPENSSL}" version 2>&1)"
exit 0
@@ -1224,7 +1293,7 @@ command_sign_domains() {
local csr=""
if [[ -n "${HOOK}" ]]; then
csr="$("${HOOK}" "generate_csr" "${domain}" "${certdir}" "${domain} ${morenames}")"
if grep -q "\-----BEGIN CERTIFICATE REQUEST-----" <<< "${csr}"; then
if grep -qE "\-----BEGIN (NEW )?CERTIFICATE REQUEST-----" <<< "${csr}"; then
altnames="$(extract_altnames "${csr}")"
domain="$(cut -d' ' -f1 <<< "${altnames}")"
morenames="$(cut -s -d' ' -f2- <<< "${altnames}")"
@@ -1295,7 +1364,7 @@ command_sign_domains() {
if [[ ! -e "${certdir}/ocsp.der" ]]; then
update_ocsp="yes"
elif ! ("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respin "${certdir}/ocsp.der" -status_age 432000 2>&1 | grep -q "${cert}: good"); then
elif ! ("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respin "${certdir}/ocsp.der" -status_age $((OCSP_DAYS*24*3600)) 2>&1 | grep -q "${cert}: good"); then
update_ocsp="yes"
fi
@@ -1308,6 +1377,7 @@ command_sign_domains() {
ocsp_log="$("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respout "${certdir}/ocsp-${ocsp_timestamp}.der" -url "${ocsp_url}" 2>&1)" || _exiterr "Error while fetching OCSP information: ${ocsp_log}"
fi
ln -sf "ocsp-${ocsp_timestamp}.der" "${certdir}/ocsp.der"
[[ -n "${HOOK}" ]] && altnames="${domain} ${morenames}" "${HOOK}" "deploy_ocsp" "${domain}" "${certdir}/ocsp.der" "${ocsp_timestamp}"
else
echo " + OSCP stapling file is still valid (skipping update)"
fi
@@ -1496,7 +1566,7 @@ command_help() {
command_env() {
echo "# dehydrated configuration"
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 ACCOUNT_ID_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE
}
# Main method (parses script arguments and calls command_* methods)
@@ -1675,6 +1745,14 @@ main() {
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_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
--challenge|-t)

19
docs/acme-v1.md Normal file
View File

@@ -0,0 +1,19 @@
## (Future) Removal of API version 1
The ACME API version 1 was never really standardized and was only supported by Let's Encrypt. Even though the protocol specification was public,
it wasn't really friendly to be integrated into existing CA systems so initial adoption was basically non-existant.
ACME version 2 is being designed to overcome these issues by becoming an official IETF standard and supporting a more traditional approach of account
and order management in the backend, making it friendlier to integrate into existing systems centered around those. It has since become a semi-stable IETF
standard draft which only ever got two breaking changes, Content-Type enforcement and `POST-as-GET`, the latter being announced in October 2018 to be enforced
by November 2019. See https://datatracker.ietf.org/wg/acme/documents/ for a better insight into the draft and its changes.
Next to backend changes that many users won't really care about ACME v2 has all of the features ACME v1 had, but also some additional new features like
e.g. support for [wildcard certificates](domains_txt.md#wildcards).
Since ACME v2 is basically to be considered stable and ACME v1 has no real benefits over v2, there doesn't seem to be much of a reason to keep the old
protocol around, but since there actually are a few Certificate Authorities and resellers that implemented the v1 protocol and didn't yet make the change
to v2, so dehydrated still supports the old protocol for now.
Please keep in mind that support for the old ACME protocol version 1 might get removed at any point of bigger inconvenience, e.g. on code changes that
would require a lot of work or ugly workarounds to keep both versions supported.

View File

@@ -1,22 +1,72 @@
### domains.txt
## domains.txt
dehydrated uses the file `domains.txt` as configuration for which certificates should be requested.
dehydrated uses the file `domains.txt` as configuration for which certificates
should be requested.
The file should have the following format:
```text
example.org
example.com www.example.com
example.net www.example.net wiki.example.net
```
This states that there are the following certificates:
* `example.org` without any *alternative names*
* `example.com` with an *alternative name* of `www.example.com`
* `example.net` with the *alternative names*: `www.example.net` and
`wiki.example.net`
### Aliases
You can define an *alias* for your certificate which will (instead of the
primary domain) be used as the directory name under your `CERTDIR` and for a
per-certificate lookup. This is done using the `>` character. This allows
multiple certificates with identical sets of domains but different
configuration to exist.
Here is an example of using an *alias* called `certalias` for creating the
certificate for `example.net` with *alternative names* `www.example.net` and
`wiki.example.net`. The certificate will be stored in the directory `certalias`
under your `CERTDIR`.
```text
example.net www.example.net wiki.example.net > certalias
```
This states that there should be two certificates `example.com` and `example.net`,
with the other domains in the corresponding line being their alternative names.
### Wildcards
You can define an alias for your certificate which will (instead of the primary domain) be
used as directory name under your certdir and for a per-certificate lookup.
This allows multiple certificates with identical sets of domains but different configuration
to exist.
Support for wildcards was added by the ACME v2 protocol.
Certificates with a wildcard domain as first (or only) name require an alias to be set.
Aliases can't start with `*.`.
Certificates with a wildcard domain as the first (or only) name require an
*alias* to be set. *Aliases* can't start with `*.`.
For example to create the wildcard for `*.service.example.com` your
`domains.txt` could use the *alias* method like this:
```text
*.service.example.com > star_service_example_com
```
This creates a wildcard certificate for only `*.service.example.com` and will
store it in the directory `star_service_example_com` under your `CERTDIR`. As a
note this certificate will **NOT** be valid for `service.example.com` but only
for `*.service.example.com`. So it would, for example, be valid for
`foo.service.example.com`.
Another way to create it is using *alternative names*. For example your
`domains.txt` could do this:
```text
service.example.com *.service.example.com
eggs.example.com *.ham.example.com
```
This creates two certificates one for `service.example.com` with an
*alternative name* of `*.service.example.com` and a second certificate for
`eggs.example.com` with an *alternative name* of `*.ham.example.com`.
**Note:** The first certificate is valid for both `service.example.com` and for
`*.service.example.com` which can be a useful way to create wildcard
certificates.

View File

@@ -21,17 +21,17 @@
# default: <unset>
#IP_VERSION=
# Path to certificate authority (default: https://acme-v01.api.letsencrypt.org/directory)
#CA="https://acme-v01.api.letsencrypt.org/directory"
# Path to certificate authority (default: https://acme-v02.api.letsencrypt.org/directory)
#CA="https://acme-v02.api.letsencrypt.org/directory"
# Path to old certificate authority
# Set this value to your old CA value when upgrading from ACMEv1 to ACMEv2 under a different endpoint.
# If dehydrated detects an account-key for the old CA it will automatically reuse that key
# instead of registering a new one.
# default: <unset>
#OLDCA=
# default: 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"
# Path to a directory containing additional config files, allowing to override
@@ -40,6 +40,11 @@
# default: <unset>
#CONFIG_D=
# Directory for per-domain configuration files.
# If not set, per-domain configurations are sourced from each certificates output directory.
# default: <unset>
#DOMAINS_D=
# Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined)
#BASEDIR=$SCRIPTDIR
@@ -49,6 +54,9 @@
# Output directory for generated certificates
#CERTDIR="${BASEDIR}/certs"
# Output directory for alpn verification certificates
#ALPNCERTDIR="${BASEDIR}/alpn-certs"
# Directory for account keys and registration information
#ACCOUNTDIR="${BASEDIR}/accounts"
@@ -106,6 +114,9 @@
# Fetch OCSP responses (default: no)
#OCSP_FETCH="no"
# OCSP refresh interval (default: 5 days)
#OCSP_DAYS=5
# Issuer chain cache directory (default: $BASEDIR/chains)
#CHAINCACHE="${BASEDIR}/chains"

View File

@@ -1,2 +1,30 @@
# Create certificate for 'example.org' with an alternative name of
# 'www.example.org'. It will be stored in the directory ${CERT_DIR}/example.org
example.org www.example.org
# Create certificate for 'example.com' with alternative names of
# 'www.example.com' & 'wiki.example.com'. It will be stored in the directory
# ${CERT_DIR}/example.com
example.com www.example.com wiki.example.com
# Using the alias 'certalias' create certificate for 'example.net' with
# alternate name 'www.example.net' and store it in the directory
# ${CERTDIR}/certalias
example.net www.example.net > certalias
# Using the alias 'service_example_com' create a wildcard certificate for
# '*.service.example.com' and store it in the directory
# ${CERTDIR}/service_example_com
# NOTE: It is NOT a certificate for 'service.example.com'
*.service.example.com > service_example_com
# Using the alias 'star_service_example_org' create a wildcard certificate for
# '*.service.example.org' with an alternative name of `service.example.org'
# and store it in the directory ${CERTDIR}/star_service_example_org
# NOTE: It is a certificate for 'service.example.org'
*.service.example.org service.example.org > star_service_example_org
# Create a certificate for 'service.example.net' with an alternative name of
# '*.service.example.net' (which is a wildcard domain) and store it in the
# directory ${CERTDIR}/service.example.net
service.example.net *.service.example.net

View File

@@ -37,6 +37,32 @@ clean_challenge() {
# printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
}
sync_cert() {
local KEYFILE="${1}" CERTFILE="${2}" FULLCHAINFILE="${3}" CHAINFILE="${4}" REQUESTFILE="${5}"
# This hook is called after the certificates have been created but before
# they are symlinked. This allows you to sync the files to disk to prevent
# creating a symlink to empty files on unexpected system crashes.
#
# This hook is not intended to be used for further processing of certificate
# files, see deploy_cert for that.
#
# Parameters:
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
# - REQUESTFILE
# The path of the file containing the certificate signing request.
# Simple example: sync the files before symlinking them
# sync "${KEYFILE}" "${CERTFILE} "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}"
}
deploy_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
@@ -64,6 +90,28 @@ deploy_cert() {
# systemctl reload nginx
}
deploy_ocsp() {
local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}"
# This hook is called once for each updated ocsp stapling file that has
# been produced. Here you might, for instance, copy your new ocsp stapling
# files to service-specific locations and reload the service.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - OCSPFILE
# The path of the ocsp stapling file
# - TIMESTAMP
# Timestamp when the specified ocsp stapling file was created.
# Simple example: Copy file to nginx config
# cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
# systemctl reload nginx
}
unchanged_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
@@ -116,6 +164,8 @@ request_failure() {
# The specified reason for the error.
# - REQTYPE
# The kind of request that was made (GET, POST...)
# - HEADERS
# HTTP headers returned by the CA
# Simple example: Send mail to root
# printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root
@@ -154,13 +204,17 @@ startup_hook() {
}
exit_hook() {
local ERROR="${1:-}"
# This hook is called at the end of the cron command and can be used to
# do some final (cleanup or other) tasks.
:
#
# Parameters:
# - ERROR
# Contains error message if dehydrated exits with error
}
HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|sync_cert|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then
"$HANDLER" "$@"
fi

View File

@@ -145,7 +145,7 @@ Please report any bugs that you may encounter at the project web site
Dehydrated was written by Lukas Schauer. This man page was contributed by
Daniel Molkentin.
.SH COPYRIGHT
Copyright 20015-2018 by Lukas Schauer and the respective contributors.
Copyright 2015-2018 by Lukas Schauer and the respective contributors.
Provided under the MIT License. See the LICENSE file that accompanies the
distribution for licensing information.
.SH SEE ALSO

View File

@@ -7,6 +7,7 @@ To use this feature create a `config` file in the certificates output directory
Currently supported options:
- PRIVATE_KEY_RENEW
- PRIVATE_KEY_ROLLOVER
- KEY_ALGO
- KEYSIZE
- OCSP_MUST_STAPLE

106
docs/tls-alpn.md Normal file
View 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()
```

View File

@@ -30,3 +30,23 @@ If you are using HTTP validation make sure that the path you have configured wit
To test this create a file (e.g. `test.txt`) in that directory and try opening it with your browser: `http://example.org/.well-known/acme-challenge/test.txt`. Note that if you have an IPv6 address, the challenge connection will be on IPv6. Be sure that you test HTTP connections on both IPv4 and IPv6. Checking the test file in your browser is often not sufficient because the browser just fails over to IPv4.
If you get any error you'll have to fix your web server configuration.
## DNS invalid challenge since dehydrated 0.6.0 / Why are DNS challenges deployed first and verified later?
Since Let's Encrypt (and in general the ACMEv2 protocol) now supports wildcard domains there is a situation where DNS caching can become a problem.
If somebody wants to validate a certificate with `example.org` and `*.example.org` there are two tokens that have to be deployed on `_acme-challenge.example.org`.
If dehydrated would deploy and verify each token on its own the CA would cache the first token on `_acme-challenge.example.org` and the next challenge would simply fail.
Let's Encrypt uses your DNS TTL with a max limit of 5 minutes, but this doesn't seem to be part of the ACME protocol, just some LE specific configuration,
so with other CAs and certain DNS providers who don't allow low TTLs this could potentially take hours.
Since dehydrated now deploys all challenges first that no longer is a problem. The CA will query and cache both challenges, and both authorizations can be validated.
Some hook-scripts were written in a way that erases the old TXT record rather than adding a new entry, those should be (and many of them already have been) fixed.
There are certain DNS providers which really only allow one TXT record on a domain. This is really odd and you should probably contact your DNS provider and ask them
to fix this.
If for whatever reason you can't switch DNS providers and your DNS provider only supports one TXT record and doesn't want to fix that you could try splitting your
certificate into multiple certificates and add a sleep in the `deploy_cert` hook.
If you can't do that or really don't want to please leave a comment on https://github.com/lukas2511/dehydrated/issues/554,
if many people are having this unfixable problem I might try to implement a workaround.