31 Commits

Author SHA1 Message Date
Lukas Schauer
1dbbc64ce9 implement workaround for openssl regression (fixes #981)
The introduction of the `-multi` option to the x509 subcommand
introduced a regression to the `-checkend` behaviour, preventing
openssl to correctly indicate the certificate expiry status via
its exit code.

This commit introduces a (maybe temporary) workaround by instead
checking the output string.
2025-10-24 09:22:31 +02:00
Lukas Schauer
12877bb238 throw error with information about OCSP deprecation if certificate doesn't indicate OCSP support 2025-07-05 11:13:45 +02:00
Lukas Schauer
ad43e250b2 allow KEEP_GOING to also skip over ocsp stapling errors, update ocsp error message with a hint about deprecation on some CAs 2025-07-05 10:55:33 +02:00
Lukas Schauer
8e9e5ef9c7 also allow setting KEEP_GOING as a config option 2025-07-05 10:54:29 +02:00
Lukas Schauer
a7deeaedbc set empty subject for ip-certificates
as suggested by @candlerb in #783
2025-07-05 10:28:13 +02:00
Victor Coss
3d95f18000 Don't allow CDN's to send cached responses
A lot of CA's use a CDN service to protect and speed up their ACME service. These CDN services can sometimes miss-behave and send cached results. For example DigiCert's ACME service uses the Imperva CDN. It will send cached results on the DNS validation, challenge endpoint, resulting in it being stuck in the processing status, thus dehydrated is hung and never gets the certificate.
2025-06-17 19:52:29 +02:00
Lukas Schauer
ce9eb300e2 implemented domain validation timeout 2025-06-17 19:51:27 +02:00
Lukas Schauer
9cfcd66f15 small addition to 0.7.2 changelog 2025-05-18 02:28:57 +02:00
Lukas Schauer
73bb54a4b2 updated changelog 2025-05-18 02:16:14 +02:00
Lukas Schauer
3a71a7ad94 only validate existance of wellknown directory or hook script when actually necessary (fixes #965) 2025-05-18 02:07:04 +02:00
Lukas Schauer
0290338853 post-v0.7.2-release 2025-05-18 01:36:16 +02:00
Lukas Schauer
fcca67b53c release v0.7.2 2025-05-18 01:34:32 +02:00
Lukas Schauer
cf9e6a33fd Allow for automatic deletion of old files 2025-05-02 15:00:48 +02:00
Lukas Schauer
bec154f070 Added a configuration parameter to allow for timeouts during order processing (fixes #955) 2025-05-02 14:42:57 +02:00
Lukas Schauer
0141d86267 Update README (closes #964) 2025-05-02 14:38:45 +02:00
Lukas Schauer
a86a176805 use temporary csr file instead of stdin (keeps compatibility to older openssl versions) 2025-04-23 11:24:42 +02:00
Lukas Schauer
200cd68e7e updated changelog 2025-04-14 19:49:31 +02:00
Christian Kujau
e973cb2d8a Disable warning when reading CSRs from stdin.
Coming across the same warning that was reported in
[PR#929](https://github.com/dehydrated-io/dehydrated/pull/929 "Suppress
openssl warning about reading from stdin") this is my attempt to disable
this warning. Instead of discarding stderr in total (this can still be
useful), we just use the "-in" parameter as hinted in the warning:

 $ foo=$(cat req.csr)
 $ <<<${foo} openssl req -noout -verify > /dev/null; echo $?
 Warning: Will read cert request from stdin since no -in option is given
 0

 $ <<<${foo} openssl req -in - -noout -verify > /dev/null; echo $?
 0
2025-04-14 19:42:15 +02:00
Lukas Schauer
7c438c484f added google ca to example config and added documentation link to error message 2025-04-14 19:12:59 +02:00
hshh
a94f451014 Add support for Google Trust Services.
Official Documentation: https://cloud.google.com/certificate-manager/docs/public-ca-tutorial
The first registration requires obtaining EAB_KID and EAB_HMAC_KEY according to the document, and setting CONTACT_EMAIL, EAB_HMAC_KEY, EAB_KID in the configuration file.
2025-04-14 18:59:59 +02:00
Bob Idle
a615a55ad6 Update dehydrated repo urls in man page 2025-04-14 18:57:00 +02:00
Lukas Schauer
f6d82e2715 fix small issue with certificate profile selection (use key instead of value) 2025-04-14 18:49:44 +02:00
Lukas Schauer
1a1cb94a61 added changelog + default config entries for certificate profile selection 2025-04-14 18:41:38 +02:00
Youfu Zhang
5ab8c3806d implemented certificate profile selection (draft-aaron-acme-profiles-00)
https://letsencrypt.org/2025/01/09/acme-profiles/
https://datatracker.ietf.org/doc/html/draft-aaron-acme-profiles-00

Signed-off-by: Youfu Zhang <zhangyoufu@gmail.com>
2025-04-14 18:35:10 +02:00
Lukas Schauer
4ea5081640 renew certificates with 32 days remaining (instead of 30) to avoid issues with monthly cronjobs (fixes #963) 2025-04-11 10:33:07 +02:00
Wilfried Teiken
4fd777e87e Ignore output of 'openssl req -verify'.
Newer versions of openssl seem to send the verify outout to stdout instead of
stderr in the past. Ignore that output when retrieving altnames.
2023-12-05 02:36:40 +01:00
Lukas Schauer
e3ef43c816 fix zsh compatibility (fixes #896) 2023-01-16 22:41:05 +01:00
Alexander Sulfrian
67b111a7b0 Replace all escaped slashes in json strings (closes #866)
${var/pattern/string} will only replace the first occurence. We should
use ${var//pattern/string} to replace all escaped slashes.
2022-10-31 16:27:16 +01:00
Daniel Molkentin
fa68ad8b23 improve man page based on feedback from debian-l10n-english (fixes #873, closes #875)
Also propagate changes to dehydrated help and README.md
2022-10-31 16:22:04 +01:00
Lukas Schauer
5c4adf6baa added note about dehydrated irc channel 2022-10-31 15:46:28 +01:00
Lukas Schauer
35bfea55b6 increase dehydrated version for git master use 2022-10-31 15:46:07 +01:00
5 changed files with 244 additions and 65 deletions

View File

@@ -1,6 +1,29 @@
# Change Log
This file contains a log of major changes in dehydrated
## [x.x.x] - xxxx-xx-xx
## Added
- Added a configuration parameter to allow for timeouts during domain validation processing (`VALIDATION_TIMEOUT`, defaults to 0 = no timeout)
## Changed
- Only validate existance of wellknown directory or hook script when actually needed
- Also allow setting `KEEP_GOING` in config file instead of relying on cli arguments
- Allow skipping over OCSP stapling errors, indicate that some CAs no longer support OCSP
- Throw error with information about OCSP deprecation if certificate doesn't indicate OCSP support
## [0.7.2] - 2025-05-18
## Added
- Implemented support for certificate profile selection
- Added a configuration parameter to allow for timeouts during order processing (`ORDER_TIMEOUT`, defaults to 0 = no timeout)
- Allowed for automatic deletion of old files (`AUTO_CLEANUP_DELETE`, disabled by default)
- Added CA presets for Google Trust Services (prod: google, test: google-test)
## Changed
- Renew certificates with 32 days remaining (instead of 30) to avoid issues with monthly cronjobs (`RENEW_DAYS=32`)
## Fixed
- Changed behaviour of `openssl req` stdin handling to fix compatibility with OpenSSL version 3.2+
## [0.7.1] - 2022-10-31
## Changed
- `--force` no longer forces domain name revalidation by default, a new argument `--force-validation` has been added for that

View File

@@ -71,7 +71,7 @@ Parameters:
--ca url/preset Use specified CA URL or preset
--alias certalias Use specified name for certificate directory (and per-certificate config) instead of the primary domain (only used if --domain is specified)
--keep-going (-g) Keep going after encountering an error while creating/renewing multiple certificates in cron mode
--force (-x) Force renew of certificate even if it is longer valid than value in RENEW_DAYS
--force (-x) Force certificate renewal even if it is not due to expire within RENEW_DAYS
--force-validation Force revalidation of domain names (used in combination with --force)
--no-lock (-n) Don't use lockfile (potentially dangerous!)
--lock-suffix example.com Suffix lockfile name with a string (useful for with -d)
@@ -85,4 +85,13 @@ Parameters:
--alpn alpn-certs/directory Output alpn verification certificates into the specified directory
--challenge (-t) http-01|dns-01|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
--acme-profile profile_name Use specified ACME profile
--order-timeout seconds Amount of seconds to wait for processing of order until erroring out
--validation-timeout seconds Amount of seconds to wait for processing of domain validations until erroring out
```
## Chat
Dehydrated has an official IRC-channel `#dehydrated` on libera.chat that can be used for general discussion and suggestions.
The channel can also be accessed with Matrix using the official libera.chat bridge at `#dehydrated:libera.chat`.

View File

@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
exec 3>&-
exec 4>&-
VERSION="0.7.1"
VERSION="0.7.3"
# Find directory in which this script is stored by traversing all symbolic links
SOURCE="${0}"
@@ -143,7 +143,7 @@ jsonsh() {
# Force zsh to expand $A into multiple words
local is_wordsplit_disabled
is_wordsplit_disabled="$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')"
is_wordsplit_disabled="$(unsetopt 2>/dev/null | grep -c '^shwordsplit$' || true)"
if [ "${is_wordsplit_disabled}" != "0" ]; then setopt shwordsplit; fi
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | grep -Ev "^$SPACE$"
if [ "${is_wordsplit_disabled}" != "0" ]; then unsetopt shwordsplit; fi
@@ -217,7 +217,7 @@ jsonsh() {
'[') parse_array "$jpath" ;;
# At this point, the only valid single-character tokens are digits.
''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;;
*) value="${token/\\\///}"
*) value="${token//\\\///}"
# replace solidus ("\/") in json strings with normalized value: "/"
;;
esac
@@ -291,6 +291,10 @@ store_configvars() {
__OPENSSL_CNF="${OPENSSL_CNF}"
__RENEW_DAYS="${RENEW_DAYS}"
__IP_VERSION="${IP_VERSION}"
__ACME_PROFILE="${ACME_PROFILE}"
__ORDER_TIMEOUT=${ORDER_TIMEOUT}
__VALIDATION_TIMEOUT=${VALIDATION_TIMEOUT}
__KEEP_GOING=${KEEP_GOING}
}
reset_configvars() {
@@ -309,6 +313,10 @@ reset_configvars() {
OPENSSL_CNF="${__OPENSSL_CNF}"
RENEW_DAYS="${__RENEW_DAYS}"
IP_VERSION="${__IP_VERSION}"
ACME_PROFILE="${__ACME_PROFILE}"
ORDER_TIMEOUT=${__ORDER_TIMEOUT}
VALIDATION_TIMEOUT=${__VALIDATION_TIMEOUT}
KEEP_GOING="${__KEEP_GOING}"
}
hookscript_bricker_hook() {
@@ -322,11 +330,13 @@ hookscript_bricker_hook() {
# verify configuration values
verify_config() {
[[ "${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
if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
if [[ "${COMMAND:-}" =~ sign_domains|sign_csr ]]; then
if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
_exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue."
fi
if [[ "${CHALLENGETYPE}" = "http-01" ]] && [[ ! -d "${WELLKNOWN}" ]]; then
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
fi
fi
[[ "${KEY_ALGO}" == "rsa" || "${KEY_ALGO}" == "prime256v1" || "${KEY_ALGO}" == "secp384r1" || "${KEY_ALGO}" == "secp521r1" ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... cannot continue."
if [[ -n "${IP_VERSION}" ]]; then
@@ -334,6 +344,8 @@ verify_config() {
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"
[[ "${ORDER_TIMEOUT}" =~ ^[0-9]+$ ]] || _exiterr "ORDER_TIMEOUT must be a number"
[[ "${VALIDATION_TIMEOUT}" =~ ^[0-9]+$ ]] || _exiterr "VALIDATION_TIMEOUT must be a number"
}
# Setup default config values, search for and load configuration files
@@ -355,6 +367,8 @@ load_config() {
CA_LETSENCRYPT_TEST="https://acme-staging-v02.api.letsencrypt.org/directory"
CA_BUYPASS="https://api.buypass.com/acme/directory"
CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory"
CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory"
CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory"
# Default values
CA="letsencrypt"
@@ -372,7 +386,7 @@ load_config() {
HOOK=
PREFERRED_CHAIN=
HOOK_CHAIN="no"
RENEW_DAYS="30"
RENEW_DAYS="32"
KEYSIZE="4096"
WELLKNOWN=
PRIVATE_KEY_RENEW="yes"
@@ -388,9 +402,14 @@ load_config() {
IP_VERSION=
CHAINCACHE=
AUTO_CLEANUP="no"
AUTO_CLEANUP_DELETE="no"
DEHYDRATED_USER=
DEHYDRATED_GROUP=
API="auto"
ACME_PROFILE=""
ORDER_TIMEOUT=0
VALIDATION_TIMEOUT=0
KEEP_GOING="no"
if [[ -z "${CONFIG:-}" ]]; then
echo "#" >&2
@@ -481,6 +500,10 @@ load_config() {
CA="${CA_BUYPASS}"
elif [ "${CA}" = "buypass-test" ]; then
CA="${CA_BUYPASS_TEST}"
elif [ "${CA}" = "google" ]; then
CA="${CA_GOOGLE}"
elif [ "${CA}" = "google-test" ]; then
CA="${CA_GOOGLE_TEST}"
fi
if [[ -z "${OLDCA}" ]] && [[ "${CA}" = "https://acme-v02.api.letsencrypt.org/directory" ]]; then
@@ -544,6 +567,10 @@ load_config() {
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
[[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}"
[[ -n "${PARAM_ACME_PROFILE:-}" ]] && ACME_PROFILE="${PARAM_ACME_PROFILE}"
[[ -n "${PARAM_ORDER_TIMEOUT:-}" ]] && ORDER_TIMEOUT="${PARAM_ORDER_TIMEOUT}"
[[ -n "${PARAM_VALIDATION_TIMEOUT:-}" ]] && VALIDATION_TIMEOUT="${PARAM_VALIDATION_TIMEOUT}"
[[ -n "${PARAM_KEEP_GOING:-}" ]] && KEEP_GOING="${PARAM_KEEP_GOING}"
if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ] && [ "${PARAM_FORCE:-no}" = "no" ]; then
_exiterr "Argument --force-validation can only be used in combination with --force (-x)"
@@ -587,6 +614,10 @@ init_system() {
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
# Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part
CA_REG=${CA_NEW_REG/new-reg/reg}
if [[ -n "${ACME_PROFILE}" ]]; then
_exiterr "ACME profiles are not supported in ACME v1."
fi
else
CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" &&
CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" &&
@@ -595,6 +626,35 @@ init_system() {
CA_REQUIRES_EAB="$(printf "%s" "${CA_DIRECTORY}" | get_json_bool_value -p '"meta","externalAccountRequired"' || echo false)" &&
CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revokeCert)" ||
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
# Checking ACME profile
if [[ -n "${ACME_PROFILE}" ]]; then
# Extract available profiles from CA directory
declare -A available_profiles=()
while IFS=$'\t' read -r path value; do
if [[ "${value}" =~ ^\"([^\"]+)\"$ ]]; then
value=${BASH_REMATCH[1]}
fi
if [[ "${path}" =~ ^\[\"([^\"]+)\"\]$ ]]; then
available_profiles[${BASH_REMATCH[1]}]=$value
fi
done <<< "$(printf "%s" "${CA_DIRECTORY}" | get_json_dict_value -p '"meta","profiles"' 2>/dev/null)"
if [[ ${#available_profiles[@]} -eq 0 ]]; then
_exiterr "ACME profile not supported by this CA"
fi
# Check if the requested profile is available
found_profile="no"
for profile in "${!available_profiles[@]}"; do
if [[ "${profile}" == "${ACME_PROFILE}" ]]; then
found_profile="yes"
break
fi
done
if [[ "${found_profile}" == "no" ]]; then
_exiterr "ACME profile '${ACME_PROFILE}' not found, available profiles:$(for key in "${!available_profiles[@]}"; do printf "\n %s: %s" "${key}" "${available_profiles[$key]}"; done)"
fi
fi
fi
# Export some environment variables to be used in hook script
@@ -703,6 +763,14 @@ init_system() {
fi
fi
# Google special sauce
if [[ "${CA}" = "${CA_GOOGLE}" ]]; then
if [[ -z "${CONTACT_EMAIL}" ]] || [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then
echo "Google requires contact email, EAB_KID and EAB_HMAC_KEY to be manually configured (see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial)"
FAILED=true
fi
fi
# Check if external account is required
if [[ "${FAILED}" = "false" ]]; then
if [[ "${CA_REQUIRES_EAB}" = "true" ]]; then
@@ -871,14 +939,14 @@ http_request() {
set +e
# shellcheck disable=SC2086
if [[ "${1}" = "head" ]]; then
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${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}" -H 'Cache-Control: no-cache' "${2}" -I)"
curlret="${?}"
touch "${tempheaders}"
elif [[ "${1}" = "get" ]]; then
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${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}" -H 'Cache-Control: no-cache' "${2}")"
curlret="${?}"
elif [[ "${1}" = "post" ]]; then
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}")"
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -D "${tempheaders}" -H 'Cache-Control: no-cache' -H 'Content-Type: application/jose+json' -d "${3}")"
curlret="${?}"
else
set -e
@@ -1009,13 +1077,13 @@ signed_request() {
# Extracts all subject names from a CSR
# Outputs either the CN, or the SANs, one per line
extract_altnames() {
csr="${1}" # the CSR itself (not a file)
csrfile="${1}" # path to CSR file
if ! <<<"${csr}" "${OPENSSL}" req -verify -noout 2>/dev/null; then
if ! "${OPENSSL}" req -in "${csrfile}" -verify -noout >/dev/null; then
_exiterr "Certificate signing request isn't valid"
fi
reqtext="$( <<<"${csr}" "${OPENSSL}" req -noout -text )"
reqtext="$("${OPENSSL}" req -in "${csrfile}" -noout -text)"
if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then
# SANs used, extract these
altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )"
@@ -1043,7 +1111,7 @@ get_last_cn() {
# Create certificate for domain(s) and outputs it FD 3
sign_csr() {
csr="${1}" # the CSR itself (not a file)
csrfile="${1}" # path to CSR file
if { true >&3; } 2>/dev/null; then
: # fd 3 looks OK
@@ -1082,7 +1150,12 @@ sign_csr() {
challenge_identifiers="[${challenge_identifiers%, }]"
echo " + Requesting new certificate order from CA..."
order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
local order_payload='{"identifiers": '"${challenge_identifiers}"
if [[ -n "${ACME_PROFILE}" ]]; then
order_payload="${order_payload}"',"profile":"'"${ACME_PROFILE}"'"'
fi
order_payload="${order_payload}"'}'
order_location="$(signed_request "${CA_NEW_ORDER}" "${order_payload}" 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
result="$(signed_request "${order_location}" "" | jsonsh)"
order_authorizations="$(echo "${result}" | get_json_array_values authorizations)"
@@ -1218,8 +1291,14 @@ sign_csr() {
reqstatus="$(echo "${result}" | get_json_string_value status)"
local waited=0
while [[ "${reqstatus}" = "pending" ]] || [[ "${reqstatus}" = "processing" ]]; do
if [ ${VALIDATION_TIMEOUT} -gt 0 ] && [ ${waited} -gt ${VALIDATION_TIMEOUT} ]; then
_exiterr "Timed out waiting for processing of domain validation (still ${reqstatus})"
fi
echo " + Validation is ${reqstatus}..."
sleep 1
waited=$((waited+1))
if [[ "${API}" -eq 2 ]]; then
result="$(signed_request "${challenge_uris[${idx}]}" "" | jsonsh)"
else
@@ -1268,25 +1347,30 @@ sign_csr() {
# Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem
echo " + Requesting certificate..."
csr64="$( <<<"${csr}" "${OPENSSL}" req -config "${OPENSSL_CNF}" -outform DER | urlbase64)"
csr64="$("${OPENSSL}" req -in "${csrfile}" -config "${OPENSSL_CNF}" -outform DER | urlbase64)"
if [[ ${API} -eq 1 ]]; then
crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)"
crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
else
result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | jsonsh)"
waited=0
while :; do
orderstatus="$(echo "${result}" | get_json_string_value status)"
case "${orderstatus}"
in
"processing" | "pending")
if [ ${ORDER_TIMEOUT} -gt 0 ] && [ ${waited} -gt ${ORDER_TIMEOUT} ]; then
_exiterr "Timed out waiting for processing of order (still ${orderstatus})"
fi
echo " + Order is ${orderstatus}..."
sleep 2;
waited=$((waited+2))
;;
"valid")
break;
;;
*)
_exiterr "Order in status ${orderstatus}"
_exiterr "Order has invalid/unknown status: ${orderstatus}"
;;
esac
result="$(signed_request "${order_location}" "" | jsonsh)"
@@ -1487,7 +1571,7 @@ sign_domain() {
fi
done
if [[ "${domain}" =~ ^ip: ]]; then
SUBJ="/CN=${domain:3}/"
SUBJ="/"
else
SUBJ="/CN=${domain}/"
fi
@@ -1510,7 +1594,7 @@ sign_domain() {
crt_path="${certdir}/cert-${timestamp}.pem"
# shellcheck disable=SC2086
sign_csr "$(< "${certdir}/cert-${timestamp}.csr")" ${altnames} 3>"${crt_path}"
sign_csr "${certdir}/cert-${timestamp}.csr" ${altnames} 3>"${crt_path}"
# Create fullchain.pem
echo " + Creating fullchain.pem..."
@@ -1557,6 +1641,42 @@ sign_domain() {
echo " + Done!"
}
# Update OCSP stapling file
update_ocsp_stapling() {
local certdir="${1}"
local update_ocsp="${2}"
local cert="${3}"
local chain="${4}"
local ocsp_url="$(get_ocsp_url "${cert}")"
if [[ -z "${ocsp_url}" ]]; then
echo " ! ERROR: OCSP stapling requested but no OCSP url found in certificate." >&2
echo " ! Keep in mind that some CAs ended support for OCSP: https://letsencrypt.org/2024/12/05/ending-ocsp/" >&2
return 1
fi
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 $((OCSP_DAYS*24*3600)) 2>&1 | grep -q "${cert}: good"); then
update_ocsp="yes"
fi
if [[ "${update_ocsp}" = "yes" ]]; then
echo " + Updating OCSP stapling file"
ocsp_timestamp="$(date +%s)"
if grep -qE "^(openssl (0|(1\.0))\.)|(libressl (1|2|3)\.)" <<< "$(${OPENSSL} version | awk '{print tolower($0)}')"; then
ocsp_log="$("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respout "${certdir}/ocsp-${ocsp_timestamp}.der" -url "${ocsp_url}" -header "HOST" "$(echo "${ocsp_url}" | _sed -e 's/^http(s?):\/\///' -e 's/\/.*$//g')" 2>&1)" || _exiterr "Fetching of OCSP information failed. Please note that some CAs (e.g. LetsEncrypt) do no longer support OCSP. Error message: ${ocsp_log}"
else
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 "Fetching of OCSP information failed. Please note that some CAs (e.g. LetsEncrypt) do no longer support OCSP. Error message: ${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}" || _exiterr 'deploy_ocsp hook returned with non-zero exit code')
else
echo " + OCSP stapling file is still valid (skipping update)"
fi
}
# Usage: --version (-v)
# Description: Print version information
command_version() {
@@ -1775,7 +1895,7 @@ command_sign_domains() {
# All settings that are allowed here should also be stored and
# restored in store_configvars() and reset_configvars()
case "${config_var}" in
KEY_ALGO|OCSP_MUST_STAPLE|OCSP_FETCH|OCSP_DAYS|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|PREFERRED_CHAIN|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS)
KEY_ALGO|OCSP_MUST_STAPLE|OCSP_FETCH|OCSP_DAYS|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|PREFERRED_CHAIN|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS|ACME_PROFILE|ORDER_TIMEOUT|VALIDATION_TIMEOUT|KEEP_GOING)
echo " + ${config_var} = ${config_value}"
declare -- "${config_var}=${config_value}"
;;
@@ -1792,16 +1912,18 @@ command_sign_domains() {
skip="no"
# Allow for external CSR generation
local csr=""
local csrfile=""
if [[ -n "${HOOK}" ]]; then
csr="$("${HOOK}" "generate_csr" "${domain}" "${certdir}" "${domain} ${morenames}")" || _exiterr 'generate_csr hook returned with non-zero exit code'
if grep -qE "\-----BEGIN (NEW )?CERTIFICATE REQUEST-----" <<< "${csr}"; then
altnames="$(extract_altnames "${csr}")"
csrfile="$(_mktemp)"
cat > "${csrfile}" <<< "${csr}"
altnames="$(extract_altnames "${csrfile}")"
domain="$(cut -d' ' -f1 <<< "${altnames}")"
morenames="$(cut -s -d' ' -f2- <<< "${altnames}")"
echo " + Using CSR from hook script (real names: ${altnames})"
else
csr=""
csrfile=""
fi
fi
@@ -1830,7 +1952,7 @@ command_sign_domains() {
valid="$("${OPENSSL}" x509 -enddate -noout -in "${cert}" | cut -d= -f2- )"
printf " + Valid till %s " "${valid}"
if ("${OPENSSL}" x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}" > /dev/null 2>&1); then
if ("${OPENSSL}" x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}" 2>&1 | grep -q "will not expire"); then
printf "(Longer than %d days). " "${RENEW_DAYS}"
if [[ "${force_renew}" = "yes" ]]; then
echo "Ignoring because renew was forced!"
@@ -1851,9 +1973,12 @@ command_sign_domains() {
# Sign certificate for this domain
if [[ ! "${skip}" = "yes" ]]; then
update_ocsp="yes"
[[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr"
if [[ -n "${csrfile}" ]]; then
cat "${csrfile}" > "${certdir}/cert-${timestamp}.csr"
rm "${csrfile}"
fi
# shellcheck disable=SC2086
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
if [[ "${KEEP_GOING:-}" = "yes" ]]; then
skip_exit_hook=yes
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} &
wait $! || exit_with_errorcode=1
@@ -1864,27 +1989,13 @@ command_sign_domains() {
fi
if [[ "${OCSP_FETCH}" = "yes" ]]; then
local ocsp_url
ocsp_url="$(get_ocsp_url "${cert}")"
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 $((OCSP_DAYS*24*3600)) 2>&1 | grep -q "${cert}: good"); then
update_ocsp="yes"
fi
if [[ "${update_ocsp}" = "yes" ]]; then
echo " + Updating OCSP stapling file"
ocsp_timestamp="$(date +%s)"
if grep -qE "^(openssl (0|(1\.0))\.)|(libressl (1|2|3)\.)" <<< "$(${OPENSSL} version | awk '{print tolower($0)}')"; then
ocsp_log="$("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respout "${certdir}/ocsp-${ocsp_timestamp}.der" -url "${ocsp_url}" -header "HOST" "$(echo "${ocsp_url}" | _sed -e 's/^http(s?):\/\///' -e 's/\/.*$//g')" 2>&1)" || _exiterr "Error while fetching OCSP information: ${ocsp_log}"
else
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}" || _exiterr 'deploy_ocsp hook returned with non-zero exit code')
if [[ "${KEEP_GOING:-}" = "yes" ]]; then
skip_exit_hook=yes
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}" &
wait $! || exit_with_errorcode=1
skip_exit_hook=no
else
echo " + OCSP stapling file is still valid (skipping update)"
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}"
fi
fi
done
@@ -1895,8 +2006,8 @@ command_sign_domains() {
[[ -n "${HOOK}" ]] && ("${HOOK}" "exit_hook" || echo 'exit_hook returned with non-zero exit code!' >&2)
if [[ "${AUTO_CLEANUP}" == "yes" ]]; then
echo "+ Running automatic cleanup"
command_cleanup noinit
echo " + Running automatic cleanup"
PARAM_CLEANUPDELETE="${AUTO_CLEANUP_DELETE:-no}" command_cleanup noinit | _sed 's/^/ + /g'
fi
exit "${exit_with_errorcode}"
@@ -1912,19 +2023,18 @@ command_sign_csr() {
exec 3>&1 1>&2
# load csr
csrfile="${1}"
local csrfile="${1}"
if [ ! -r "${csrfile}" ]; then
_exiterr "Could not read certificate signing request ${csrfile}"
fi
csr="$(cat "${csrfile}")"
# extract names
altnames="$(extract_altnames "${csr}")"
altnames="$(extract_altnames "${csrfile}")"
# gen cert
certfile="$(_mktemp)"
# shellcheck disable=SC2086
sign_csr "${csr}" ${altnames} 3> "${certfile}"
sign_csr "${csrfile}" ${altnames} 3> "${certfile}"
# print cert
echo "# CERT #" >&3
@@ -2262,7 +2372,7 @@ main() {
;;
# PARAM_Usage: --force (-x)
# PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS
# PARAM_Description: Force certificate renewal even if it is not due to expire within RENEW_DAYS
--force|-x)
PARAM_FORCE="yes"
;;
@@ -2364,6 +2474,31 @@ main() {
check_parameters "${1:-}"
PARAM_KEY_ALGO="${1}"
;;
# PARAM_Usage: --acme-profile profile_name
# PARAM_Description: Use specified ACME profile
--acme-profile)
shift 1
check_parameters "${1:-}"
PARAM_ACME_PROFILE="${1}"
;;
# PARAM_Usage: --order-timeout seconds
# PARAM_Description: Amount of seconds to wait for processing of order until erroring out
--order-timeout)
shift 1
check_parameters "${1:-}"
PARAM_ORDER_TIMEOUT=${1}
;;
# PARAM_Usage: --validation-timeout seconds
# PARAM_Description: Amount of seconds to wait for processing of domain validations until erroring out
--validation-timeout)
shift 1
check_parameters "${1:-}"
PARAM_VALIDATION_TIMEOUT=${1}
;;
*)
echo "Unknown parameter detected: ${1}" >&2
echo >&2

View File

@@ -22,7 +22,7 @@
#IP_VERSION=
# URL to certificate authority or internal preset
# Presets: letsencrypt, letsencrypt-test, zerossl, buypass, buypass-test
# Presets: letsencrypt, letsencrypt-test, zerossl, buypass, buypass-test, google, google-test
# default: letsencrypt
#CA="letsencrypt"
@@ -92,8 +92,8 @@
# Chain clean_challenge|deploy_challenge arguments together into one hook call per certificate (default: no)
#HOOK_CHAIN="no"
# Minimum days before expiration to automatically renew certificate (default: 30)
#RENEW_DAYS="30"
# Minimum days before expiration to automatically renew certificate (default: 32)
#RENEW_DAYS="32"
# Regenerate private keys instead of just signing new certificates on renewal (default: yes)
#PRIVATE_KEY_RENEW="yes"
@@ -125,8 +125,20 @@
# Automatic cleanup (default: no)
#AUTO_CLEANUP="no"
# Delete files during automatic cleanup instead of moving to archive (default: no)
#AUTO_CLEANUP_DELETE="no"
# ACME API version (default: auto)
#API=auto
# Preferred issuer chain (default: <unset> -> uses default chain)
#PREFERRED_CHAIN=
# Request certificate with specific profile (default: <unset>)
#ACME_PROFILE=
# Amount of seconds to wait for processing of order until erroring out (default: 0 => no timeout)
#ORDER_TIMEOUT=0
# Skip over errors during certificate orders and updating of OCSP stapling information (default: no)
#KEEP_GOING=no

View File

@@ -20,13 +20,13 @@ Dehydrated will notify if no account is configured. Run with \fB--register
Next, all domain names must be provided in domains.txt. The format is line
based: If the file contains two lines "example.com" and "example.net",
Dehydrated will request two certificate, one for "example.com" and the other
for "example.net". A single line while "example.com example.net" will request a
dehydrated will request two certificate, one for "example.com" and the other
for "example.net". A single line containing "example.com example.net" will request a
single certificate valid for both "example.net" and "example.com" through the \fISubject
Alternative Name\fR (SAN) field.
For the next step, one way of verifying domain name ownership needs to be
configured. Dehydrated implements \fIhttp-01\fR and \fIdns-01\fR verification.
configured. Dehydrated implements \fIhttp-01\fR and \fIdns-01\fR verification.
The \fIhttp-01\fR verification provides proof of ownership by providing a
challenge token. In order to do that, the directory referenced in the
@@ -106,7 +106,7 @@ Keep going after encountering an error while creating/renewing multiple
certificates in cron mode
.TP
.BR \-\-force ", " \-x
Force renew of certificate even if it is longer valid than value in RENEW_DAYS
Force certificate renewal even if it is not due to expire within RENEW_DAYS
.TP
.BR \-\-no\-lock ", " \-n
Don't use lockfile (potentially dangerous!)
@@ -139,7 +139,7 @@ secp384r1
The program exits 0 if everything was fine, 1 if an error occurred.
.SH BUGS
Please report any bugs that you may encounter at the project web site
.UR https://github.com/lukas2511/dehydrated/issues
.UR https://github.com/dehydrated-io/dehydrated/issues
.UE .
.SH AUTHOR
Dehydrated was written by Lukas Schauer. This man page was contributed by
@@ -151,5 +151,5 @@ distribution for licensing information.
.SH SEE ALSO
Full documentation along with configuration examples are provided in the \fIdocs\fR
directory of the distribution, or at
.UR https://github.com/lukas2511/dehydrated/tree/master/docs
.UR https://github.com/dehydrated-io/dehydrated/tree/master/docs
.UE .