14 Commits

Author SHA1 Message Date
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
5 changed files with 136 additions and 28 deletions

View File

@@ -1,8 +1,17 @@
# Change Log
This file contains a log of major changes in dehydrated
## [x.x.x] - xxxx-xx-xx
...
## [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)
## 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

View File

@@ -85,6 +85,8 @@ 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
```
## Chat

View File

@@ -291,6 +291,8 @@ store_configvars() {
__OPENSSL_CNF="${OPENSSL_CNF}"
__RENEW_DAYS="${RENEW_DAYS}"
__IP_VERSION="${IP_VERSION}"
__ACME_PROFILE="${ACME_PROFILE}"
__ORDER_TIMEOUT=${ORDER_TIMEOUT}
}
reset_configvars() {
@@ -309,6 +311,8 @@ reset_configvars() {
OPENSSL_CNF="${__OPENSSL_CNF}"
RENEW_DAYS="${__RENEW_DAYS}"
IP_VERSION="${__IP_VERSION}"
ACME_PROFILE="${__ACME_PROFILE}"
ORDER_TIMEOUT=${__ORDER_TIMEOUT}
}
hookscript_bricker_hook() {
@@ -334,6 +338,7 @@ 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"
}
# Setup default config values, search for and load configuration files
@@ -355,6 +360,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 +379,7 @@ load_config() {
HOOK=
PREFERRED_CHAIN=
HOOK_CHAIN="no"
RENEW_DAYS="30"
RENEW_DAYS="32"
KEYSIZE="4096"
WELLKNOWN=
PRIVATE_KEY_RENEW="yes"
@@ -388,9 +395,12 @@ load_config() {
IP_VERSION=
CHAINCACHE=
AUTO_CLEANUP="no"
AUTO_CLEANUP_DELETE="no"
DEHYDRATED_USER=
DEHYDRATED_GROUP=
API="auto"
ACME_PROFILE=""
ORDER_TIMEOUT=0
if [[ -z "${CONFIG:-}" ]]; then
echo "#" >&2
@@ -481,6 +491,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 +558,8 @@ 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}"
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 +603,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 +615,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 +752,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
@@ -1009,13 +1066,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 >/dev/null 2>&1; 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 +1100,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 +1139,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)"
@@ -1268,25 +1330,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)"
@@ -1510,7 +1577,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..."
@@ -1775,7 +1842,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)
echo " + ${config_var} = ${config_value}"
declare -- "${config_var}=${config_value}"
;;
@@ -1792,16 +1859,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
@@ -1851,7 +1920,10 @@ 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
skip_exit_hook=yes
@@ -1895,8 +1967,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 +1984,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
@@ -2364,6 +2435,23 @@ 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}
;;
*)
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,17 @@
# 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

View File

@@ -26,7 +26,7 @@ single certificate valid for both "example.net" and "example.com" through the \f
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
@@ -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 .