1 Commits

Author SHA1 Message Date
Lukas Schauer
911a822c0c added option to pass environment variables over sudo 2024-08-04 12:07:34 +02:00
9 changed files with 127 additions and 3260 deletions

View File

@@ -2,33 +2,8 @@
This file contains a log of major changes in dehydrated This file contains a log of major changes in dehydrated
## [x.x.x] - xxxx-xx-xx ## [x.x.x] - xxxx-xx-xx
## Fixed
- Various bugfixes around IP certificate orders
- Implement workaround for OpenSSL regression which broke the time-based validity check
## Added ## Added
- Added a configuration parameter to allow for timeouts during domain validation processing (`VALIDATION_TIMEOUT`, defaults to 0 = no timeout) - New config variable `DEHYDRATED_SUDO_ENV` to allow passing environment variables over sudo calls
- Added documentation for IP certificates
- Added support for DNS-PERSIST-01 challenge type
## 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 ## [0.7.1] - 2022-10-31
## Changed ## Changed

View File

@@ -1,25 +1,22 @@
<a href="https://zerossl.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/dehydrated-io/dehydrated/blob/master/docs/banner-dark.svg" /><source media="(prefers-color-scheme: light)" srcset="https://github.com/dehydrated-io/dehydrated/blob/master/docs/banner-light.svg" /><img alt="ZeroSSL" src="https://github.com/dehydrated-io/dehydrated/blob/master/docs/banner-light.svg" width="100%" /></picture></a> # dehydrated [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8)
<img align="left" src="https://github.com/dehydrated-io/dehydrated/blob/master/docs/logo.png" /> ![](docs/logo.png)
Dehydrated is a client for signing certificates with an ACME-server (e.g. ZeroSSL, Let's Encrypt, etc.) implemented as a relatively simple (zsh-compatible) bash-script. 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! 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. 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, awk, mktemp (all found pre-installed 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).
<br clear="left"/> Current features:
## Current features
- Signing of a list of domains (including wildcard domains!) - Signing of a list of domains (including wildcard domains!)
- Signing of a custom CSR (either standalone or completely automated using hooks!) - 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 - Renewal if a certificate is about to expire or defined set of domains changed
- Certificate revocation - Certificate revocation
- and lots more.. - and lots more..
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). but please check for duplicates first (feel free to comment on those to get things rolling).
## Getting started ## Getting started
@@ -86,11 +83,8 @@ Parameters:
--preferred-chain issuer-cn Use alternative certificate chain identified by issuer CN --preferred-chain issuer-cn Use alternative certificate chain identified by issuer CN
--out (-o) certs/directory Output certificates into the specified directory --out (-o) certs/directory Output certificates into the specified directory
--alpn alpn-certs/directory Output alpn verification certificates into the specified directory --alpn alpn-certs/directory Output alpn verification certificates into the specified directory
--challenge (-t) http-01|dns-01|dns-persist-01|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, dns-persist-01 and tls-alpn-01 are supported --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 --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 ## Chat
@@ -98,17 +92,3 @@ Parameters:
Dehydrated has an official IRC-channel `#dehydrated` on libera.chat that can be used for general discussion and suggestions. 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`. The channel can also be accessed with Matrix using the official libera.chat bridge at `#dehydrated:libera.chat`.
## About this repository
> [!NOTE]
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
> For more information about our services, including free and paid SSL/TLS certificates, visit https://zerossl.com.
<p align="center">
<a href="https://zerossl.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://zerossl.com/assets/images/zerossl_logo_white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://zerossl.com/assets/images/zerossl_logo.svg">
<img src="https://zerossl.com/assets/images/zerossl_logo.svg" alt="ZeroSSL" width="256">
</picture>
</a>
</p>

View File

@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
exec 3>&- exec 3>&-
exec 4>&- exec 4>&-
VERSION="0.7.3" VERSION="0.7.2"
# Find directory in which this script is stored by traversing all symbolic links # Find directory in which this script is stored by traversing all symbolic links
SOURCE="${0}" SOURCE="${0}"
@@ -49,11 +49,11 @@ noglob_clear() {
# Generate json.sh path matching string # Generate json.sh path matching string
json_path() { json_path() {
if [ ! "${1}" = "-p" ]; then if [ ! "${1}" = "-p" ]; then
printf '"%s"' "${1}" printf '"%s"' "${1}"
else else
printf '%s' "${2}" printf '%s' "${2}"
fi fi
} }
# Get string value from json dictionary # Get string value from json dictionary
@@ -252,43 +252,6 @@ ip_to_ptr() {
fi fi
} }
# IPv6 conversion helpers
ipv6_expand() {
# expand double colons until 8 segments exist
# replace remaining double colon with single colon
# pad all segments to 4 characters with leading zeros
_sed \
-e ':addsegs; /^([^:]*:){0,7}[^:]*$/{ s/::/:0000::/g; t addsegs; }' \
-e 's/::/:/' \
-e ':padsegs; s/(:|^)([^:]{0,3})(:|$)/\10\2\3/g; t padsegs;'
}
ipv6_shorten() {
# remove leading zeros from all segments
# find the longest matching run of zeros and replace with double colons (this could be prettier..)
_sed \
-e ':unpadsegs;/(^|:)0/{s/(^|:)0([^:])/\1\2/g;t unpadsegs;}' \
-e '/(^|:)(0(:|$)){8}/{ s/(^|:)(0(:|$)){8}/::/; t end; }' \
-e '/(^|:)(0(:|$)){7}/{ s/(^|:)(0(:|$)){7}/::/; t end; }' \
-e '/(^|:)(0(:|$)){6}/{ s/(^|:)(0(:|$)){6}/::/; t end; }' \
-e '/(^|:)(0(:|$)){5}/{ s/(^|:)(0(:|$)){5}/::/; t end; }' \
-e '/(^|:)(0(:|$)){4}/{ s/(^|:)(0(:|$)){4}/::/; t end; }' \
-e '/(^|:)(0(:|$)){3}/{ s/(^|:)(0(:|$)){3}/::/; t end; }' \
-e '/(^|:)(0(:|$)){2}/{ s/(^|:)(0(:|$)){2}/::/; t end; }' \
-e ':end'
}
ipv6_normalize() {
for domain in $(cat); do
if [[ "${domain}" =~ : ]]; then
printf "%s" "${domain}" | ipv6_expand | ipv6_shorten
else
printf "%s" "${domain}"
fi
printf " "
done | sed -e 's/ $//'
}
# Create (identifiable) temporary files # Create (identifiable) temporary files
_mktemp() { _mktemp() {
mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX" mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
@@ -328,10 +291,6 @@ store_configvars() {
__OPENSSL_CNF="${OPENSSL_CNF}" __OPENSSL_CNF="${OPENSSL_CNF}"
__RENEW_DAYS="${RENEW_DAYS}" __RENEW_DAYS="${RENEW_DAYS}"
__IP_VERSION="${IP_VERSION}" __IP_VERSION="${IP_VERSION}"
__ACME_PROFILE="${ACME_PROFILE}"
__ORDER_TIMEOUT=${ORDER_TIMEOUT}
__VALIDATION_TIMEOUT=${VALIDATION_TIMEOUT}
__KEEP_GOING=${KEEP_GOING}
} }
reset_configvars() { reset_configvars() {
@@ -350,10 +309,6 @@ reset_configvars() {
OPENSSL_CNF="${__OPENSSL_CNF}" OPENSSL_CNF="${__OPENSSL_CNF}"
RENEW_DAYS="${__RENEW_DAYS}" RENEW_DAYS="${__RENEW_DAYS}"
IP_VERSION="${__IP_VERSION}" IP_VERSION="${__IP_VERSION}"
ACME_PROFILE="${__ACME_PROFILE}"
ORDER_TIMEOUT=${__ORDER_TIMEOUT}
VALIDATION_TIMEOUT=${__VALIDATION_TIMEOUT}
KEEP_GOING="${__KEEP_GOING}"
} }
hookscript_bricker_hook() { hookscript_bricker_hook() {
@@ -366,14 +321,12 @@ hookscript_bricker_hook() {
# verify configuration values # verify configuration values
verify_config() { verify_config() {
[[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" || "${CHALLENGETYPE}" == "dns-persist-01" || "${CHALLENGETYPE}" == "tls-alpn-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 [[ "${COMMAND:-}" =~ sign_domains|sign_csr ]]; then if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then _exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue."
_exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue." fi
fi if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
if [[ "${CHALLENGETYPE}" = "http-01" ]] && [[ ! -d "${WELLKNOWN}" ]]; then _exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
fi
fi fi
[[ "${KEY_ALGO}" == "rsa" || "${KEY_ALGO}" == "prime256v1" || "${KEY_ALGO}" == "secp384r1" || "${KEY_ALGO}" == "secp521r1" ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... cannot continue." [[ "${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 if [[ -n "${IP_VERSION}" ]]; then
@@ -381,8 +334,6 @@ verify_config() {
fi fi
[[ "${API}" == "auto" || "${API}" == "1" || "${API}" == "2" ]] || _exiterr "Unsupported API version defined in config: ${API}" [[ "${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" [[ "${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 # Setup default config values, search for and load configuration files
@@ -404,8 +355,6 @@ load_config() {
CA_LETSENCRYPT_TEST="https://acme-staging-v02.api.letsencrypt.org/directory" CA_LETSENCRYPT_TEST="https://acme-staging-v02.api.letsencrypt.org/directory"
CA_BUYPASS="https://api.buypass.com/acme/directory" CA_BUYPASS="https://api.buypass.com/acme/directory"
CA_BUYPASS_TEST="https://api.test4.buypass.no/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 # Default values
CA="letsencrypt" CA="letsencrypt"
@@ -423,7 +372,7 @@ load_config() {
HOOK= HOOK=
PREFERRED_CHAIN= PREFERRED_CHAIN=
HOOK_CHAIN="no" HOOK_CHAIN="no"
RENEW_DAYS="32" RENEW_DAYS="30"
KEYSIZE="4096" KEYSIZE="4096"
WELLKNOWN= WELLKNOWN=
PRIVATE_KEY_RENEW="yes" PRIVATE_KEY_RENEW="yes"
@@ -439,14 +388,10 @@ load_config() {
IP_VERSION= IP_VERSION=
CHAINCACHE= CHAINCACHE=
AUTO_CLEANUP="no" AUTO_CLEANUP="no"
AUTO_CLEANUP_DELETE="no"
DEHYDRATED_USER= DEHYDRATED_USER=
DEHYDRATED_GROUP= DEHYDRATED_GROUP=
DEHYDRATED_SUDO_ENV="no"
API="auto" API="auto"
ACME_PROFILE=""
ORDER_TIMEOUT=0
VALIDATION_TIMEOUT=0
KEEP_GOING="no"
if [[ -z "${CONFIG:-}" ]]; then if [[ -z "${CONFIG:-}" ]]; then
echo "#" >&2 echo "#" >&2
@@ -498,7 +443,11 @@ load_config() {
if [[ -z "${DEHYDRATED_GROUP}" ]]; then if [[ -z "${DEHYDRATED_GROUP}" ]]; then
if [[ "${EUID}" != "${TARGET_UID}" ]]; then if [[ "${EUID}" != "${TARGET_UID}" ]]; then
echo "# INFO: Running $0 as ${DEHYDRATED_USER}" echo "# INFO: Running $0 as ${DEHYDRATED_USER}"
has_sudo && exec sudo -u "${DEHYDRATED_USER}" "${0}" "${ORIGARGS[@]}" if [ "${DEHYDRATED_SUDO_ENV}" = "yes" ]; then
has_sudo && exec sudo -E -H -u "${DEHYDRATED_USER}" "${0}" "${ORIGARGS[@]}"
else
has_sudo && exec sudo -u "${DEHYDRATED_USER}" "${0}" "${ORIGARGS[@]}"
fi
fi fi
else else
TARGET_GID="$(getent group "${DEHYDRATED_GROUP}" | cut -d':' -f3)" || _exiterr "DEHYDRATED_GROUP ${DEHYDRATED_GROUP} is invalid" TARGET_GID="$(getent group "${DEHYDRATED_GROUP}" | cut -d':' -f3)" || _exiterr "DEHYDRATED_GROUP ${DEHYDRATED_GROUP} is invalid"
@@ -508,7 +457,11 @@ load_config() {
fi fi
if [[ "${EUID}" != "${TARGET_UID}" ]] || [[ "${EGID}" != "${TARGET_GID}" ]]; then if [[ "${EUID}" != "${TARGET_UID}" ]] || [[ "${EGID}" != "${TARGET_GID}" ]]; then
echo "# INFO: Running $0 as ${DEHYDRATED_USER}/${DEHYDRATED_GROUP}" echo "# INFO: Running $0 as ${DEHYDRATED_USER}/${DEHYDRATED_GROUP}"
has_sudo && exec sudo -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" "${ORIGARGS[@]}" if [ "${DEHYDRATED_SUDO_ENV}" = "yes" ]; then
has_sudo && exec sudo -E -H -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" "${ORIGARGS[@]}"
else
has_sudo && exec sudo -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" "${ORIGARGS[@]}"
fi
fi fi
fi fi
elif [[ -n "${DEHYDRATED_GROUP}" ]]; then elif [[ -n "${DEHYDRATED_GROUP}" ]]; then
@@ -537,10 +490,6 @@ load_config() {
CA="${CA_BUYPASS}" CA="${CA_BUYPASS}"
elif [ "${CA}" = "buypass-test" ]; then elif [ "${CA}" = "buypass-test" ]; then
CA="${CA_BUYPASS_TEST}" CA="${CA_BUYPASS_TEST}"
elif [ "${CA}" = "google" ]; then
CA="${CA_GOOGLE}"
elif [ "${CA}" = "google-test" ]; then
CA="${CA_GOOGLE_TEST}"
fi fi
if [[ -z "${OLDCA}" ]] && [[ "${CA}" = "https://acme-v02.api.letsencrypt.org/directory" ]]; then if [[ -z "${OLDCA}" ]] && [[ "${CA}" = "https://acme-v02.api.letsencrypt.org/directory" ]]; then
@@ -604,10 +553,6 @@ load_config() {
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}" [[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}" [[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
[[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}" [[ -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 if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ] && [ "${PARAM_FORCE:-no}" = "no" ]; then
_exiterr "Argument --force-validation can only be used in combination with --force (-x)" _exiterr "Argument --force-validation can only be used in combination with --force (-x)"
@@ -651,10 +596,6 @@ init_system() {
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint." _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 # 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} CA_REG=${CA_NEW_REG/new-reg/reg}
if [[ -n "${ACME_PROFILE}" ]]; then
_exiterr "ACME profiles are not supported in ACME v1."
fi
else else
CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" && CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" &&
CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" && CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" &&
@@ -663,35 +604,6 @@ init_system() {
CA_REQUIRES_EAB="$(printf "%s" "${CA_DIRECTORY}" | get_json_bool_value -p '"meta","externalAccountRequired"' || echo false)" && 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)" || 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." _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 fi
# Export some environment variables to be used in hook script # Export some environment variables to be used in hook script
@@ -788,7 +700,7 @@ init_system() {
echo "ZeroSSL requires contact email to be set or EAB_KID/EAB_HMAC_KEY to be manually configured" echo "ZeroSSL requires contact email to be set or EAB_KID/EAB_HMAC_KEY to be manually configured"
FAILED=true FAILED=true
else else
zeroapi="$(curl ${ip_version:-} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" ${CURL_OPTS} -s "https://api.zerossl.com/acme/eab-credentials-email" -d "email=${CONTACT_EMAIL}" | jsonsh)" zeroapi="$(curl -s "https://api.zerossl.com/acme/eab-credentials-email" -d "email=${CONTACT_EMAIL}" | jsonsh)"
EAB_KID="$(printf "%s" "${zeroapi}" | get_json_string_value eab_kid)" EAB_KID="$(printf "%s" "${zeroapi}" | get_json_string_value eab_kid)"
EAB_HMAC_KEY="$(printf "%s" "${zeroapi}" | get_json_string_value eab_hmac_key)" EAB_HMAC_KEY="$(printf "%s" "${zeroapi}" | get_json_string_value eab_hmac_key)"
if [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then if [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then
@@ -800,14 +712,6 @@ init_system() {
fi fi
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 # Check if external account is required
if [[ "${FAILED}" = "false" ]]; then if [[ "${FAILED}" = "false" ]]; then
if [[ "${CA_REQUIRES_EAB}" = "true" ]]; then if [[ "${CA_REQUIRES_EAB}" = "true" ]]; then
@@ -976,14 +880,14 @@ http_request() {
set +e set +e
# shellcheck disable=SC2086 # shellcheck disable=SC2086
if [[ "${1}" = "head" ]]; then if [[ "${1}" = "head" ]]; then
statuscode="$(curl ${ip_version:-} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" ${CURL_OPTS} -s -w "%{http_code}" -o "${tempcont}" -H 'Cache-Control: no-cache' "${2}" -I)" statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
curlret="${?}" curlret="${?}"
touch "${tempheaders}" touch "${tempheaders}"
elif [[ "${1}" = "get" ]]; then elif [[ "${1}" = "get" ]]; then
statuscode="$(curl ${ip_version:-} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" ${CURL_OPTS} -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" -H 'Cache-Control: no-cache' "${2}")" statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" "${2}")"
curlret="${?}" curlret="${?}"
elif [[ "${1}" = "post" ]]; then elif [[ "${1}" = "post" ]]; then
statuscode="$(curl ${ip_version:-} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" ${CURL_OPTS} -s -w "%{http_code}" -o "${tempcont}" "${2}" -D "${tempheaders}" -H 'Cache-Control: no-cache' -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="${?}" curlret="${?}"
else else
set -e set -e
@@ -1114,13 +1018,13 @@ signed_request() {
# Extracts all subject names from a CSR # Extracts all subject names from a CSR
# Outputs either the CN, or the SANs, one per line # Outputs either the CN, or the SANs, one per line
extract_altnames() { extract_altnames() {
csrfile="${1}" # path to CSR file csr="${1}" # the CSR itself (not a file)
if ! "${OPENSSL}" req -in "${csrfile}" -verify -noout >/dev/null; then if ! <<<"${csr}" "${OPENSSL}" req -verify -noout >/dev/null 2>&1; then
_exiterr "Certificate signing request isn't valid" _exiterr "Certificate signing request isn't valid"
fi fi
reqtext="$("${OPENSSL}" req -in "${csrfile}" -noout -text)" reqtext="$( <<<"${csr}" "${OPENSSL}" req -noout -text )"
if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then
# SANs used, extract these # SANs used, extract these
altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )" altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )"
@@ -1148,7 +1052,7 @@ get_last_cn() {
# Create certificate for domain(s) and outputs it FD 3 # Create certificate for domain(s) and outputs it FD 3
sign_csr() { sign_csr() {
csrfile="${1}" # path to CSR file csr="${1}" # the CSR itself (not a file)
if { true >&3; } 2>/dev/null; then if { true >&3; } 2>/dev/null; then
: # fd 3 looks OK : # fd 3 looks OK
@@ -1178,25 +1082,16 @@ sign_csr() {
# Request new order and store authorization URIs # Request new order and store authorization URIs
local challenge_identifiers="" local challenge_identifiers=""
for altname in ${altnames}; do for altname in ${altnames}; do
if [[ "${altname}" =~ ^ip: ]]; then if [[ "${altname}" =~ ^ip: ]]; then
ip="${altname:3}" challenge_identifiers+="$(printf '{"type": "ip", "value": "%s"}, ' "${altname:3}")"
if [[ "${ip}" =~ : ]]; then else
ip="$(ipv6_normalize <<< "${ip}")" challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
fi fi
challenge_identifiers+="$(printf '{"type": "ip", "value": "%s"}, ' "${ip}")"
else
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
fi
done done
challenge_identifiers="[${challenge_identifiers%, }]" challenge_identifiers="[${challenge_identifiers%, }]"
echo " + Requesting new certificate order from CA..." echo " + Requesting new certificate order from CA..."
local order_payload='{"identifiers": '"${challenge_identifiers}" order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
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)" result="$(signed_request "${order_location}" "" | jsonsh)"
order_authorizations="$(echo "${result}" | get_json_array_values authorizations)" order_authorizations="$(echo "${result}" | get_json_array_values authorizations)"
@@ -1248,7 +1143,7 @@ sign_csr() {
if [ -z "${challengeindex}" ]; then if [ -z "${challengeindex}" ]; then
allowed_validations="$(echo "${response}" | grep -E '^\["challenges",[0-9]+,"type"\]' | sed -e 's/\[[^\]*\][[:space:]]*//g' -e 's/^"//' -e 's/"$//' | tr '\n' ' ')" allowed_validations="$(echo "${response}" | grep -E '^\["challenges",[0-9]+,"type"\]' | sed -e 's/\[[^\]*\][[:space:]]*//g' -e 's/^"//' -e 's/"$//' | tr '\n' ' ')"
_exiterr "Validating this certificate is not possible using ${CHALLENGETYPE}. Possible validation methods are: ${allowed_validations}. Please check with your CA for more information about supported validation methods." _exiterr "Validating this certificate is not possible using ${CHALLENGETYPE}. Possible validation methods are: ${allowed_validations}"
fi fi
challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")" challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")"
@@ -1283,10 +1178,6 @@ sign_csr() {
# Generate DNS entry content for dns-01 validation # Generate DNS entry content for dns-01 validation
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)" keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
;; ;;
"dns-persist-01")
# Pre-existing persistent DNS record is expected; no deploy/cleanup by dehydrated.
keyauth_hook=""
;;
"tls-alpn-01") "tls-alpn-01")
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')" keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')"
generate_alpn_certificate "${identifier}" "${identifier_type}" "${keyauth_hook}" generate_alpn_certificate "${identifier}" "${identifier_type}" "${keyauth_hook}"
@@ -1307,20 +1198,18 @@ sign_csr() {
# Deploy challenge tokens # Deploy challenge tokens
if [[ ${num_pending_challenges} -ne 0 ]]; then if [[ ${num_pending_challenges} -ne 0 ]]; then
if [[ "${CHALLENGETYPE}" != "dns-persist-01" ]]; then echo " + Deploying challenge tokens..."
echo " + Deploying challenge tokens..." if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then # shellcheck disable=SC2068
# shellcheck disable=SC2068 "${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
"${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' elif [[ -n "${HOOK}" ]]; then
elif [[ -n "${HOOK}" ]]; then # Run hook script to deploy the challenge token
# Run hook script to deploy the challenge token local idx=0
local idx=0 while [ ${idx} -lt ${num_pending_challenges} ]; do
while [ ${idx} -lt ${num_pending_challenges} ]; do # shellcheck disable=SC2086
# shellcheck disable=SC2086 "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
"${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' idx=$((idx+1))
idx=$((idx+1)) done
done
fi
fi fi
fi fi
@@ -1338,14 +1227,8 @@ sign_csr() {
reqstatus="$(echo "${result}" | get_json_string_value status)" reqstatus="$(echo "${result}" | get_json_string_value status)"
local waited=0
while [[ "${reqstatus}" = "pending" ]] || [[ "${reqstatus}" = "processing" ]]; do 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 sleep 1
waited=$((waited+1))
if [[ "${API}" -eq 2 ]]; then if [[ "${API}" -eq 2 ]]; then
result="$(signed_request "${challenge_uris[${idx}]}" "" | jsonsh)" result="$(signed_request "${challenge_uris[${idx}]}" "" | jsonsh)"
else else
@@ -1367,26 +1250,24 @@ sign_csr() {
done done
if [[ ${num_pending_challenges} -ne 0 ]]; then if [[ ${num_pending_challenges} -ne 0 ]]; then
if [[ "${CHALLENGETYPE}" != "dns-persist-01" ]]; then echo " + Cleaning challenge tokens..."
echo " + Cleaning challenge tokens..."
# Clean challenge tokens using chained hook # Clean challenge tokens using chained hook
# shellcheck disable=SC2068 # shellcheck disable=SC2068
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[@]} || _exiterr 'clean_challenge hook returned with non-zero exit code') [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[@]} || _exiterr 'clean_challenge hook returned with non-zero exit code')
# Clean remaining challenge tokens if validation has failed # Clean remaining challenge tokens if validation has failed
local idx=0 local idx=0
while [ ${idx} -lt ${num_pending_challenges} ]; do while [ ${idx} -lt ${num_pending_challenges} ]; do
# Delete challenge file # Delete challenge file
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
# Delete alpn verification certificates # Delete alpn verification certificates
[[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem" [[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem"
# Clean challenge token using non-chained hook # Clean challenge token using non-chained hook
# shellcheck disable=SC2086 # shellcheck disable=SC2086
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code') [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code')
idx=$((idx+1)) idx=$((idx+1))
done done
fi
if [[ "${reqstatus}" != "valid" ]]; then if [[ "${reqstatus}" != "valid" ]]; then
echo " + Challenge validation has failed :(" echo " + Challenge validation has failed :("
@@ -1396,30 +1277,25 @@ sign_csr() {
# Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem
echo " + Requesting certificate..." echo " + Requesting certificate..."
csr64="$("${OPENSSL}" req -in "${csrfile}" -config "${OPENSSL_CNF}" -outform DER | urlbase64)" csr64="$( <<<"${csr}" "${OPENSSL}" req -config "${OPENSSL_CNF}" -outform DER | urlbase64)"
if [[ ${API} -eq 1 ]]; then if [[ ${API} -eq 1 ]]; then
crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)" 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}" )" crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
else else
result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | jsonsh)" result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | jsonsh)"
waited=0
while :; do while :; do
orderstatus="$(echo "${result}" | get_json_string_value status)" orderstatus="$(echo "${result}" | get_json_string_value status)"
case "${orderstatus}" case "${orderstatus}"
in in
"processing" | "pending") "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}..." echo " + Order is ${orderstatus}..."
sleep 2; sleep 2;
waited=$((waited+2))
;; ;;
"valid") "valid")
break; break;
;; ;;
*) *)
_exiterr "Order has invalid/unknown status: ${orderstatus}" _exiterr "Order in status ${orderstatus}"
;; ;;
esac esac
result="$(signed_request "${order_location}" "" | jsonsh)" result="$(signed_request "${order_location}" "" | jsonsh)"
@@ -1620,7 +1496,7 @@ sign_domain() {
fi fi
done done
if [[ "${domain}" =~ ^ip: ]]; then if [[ "${domain}" =~ ^ip: ]]; then
SUBJ="/" SUBJ="/CN=${domain:3}/"
else else
SUBJ="/CN=${domain}/" SUBJ="/CN=${domain}/"
fi fi
@@ -1643,7 +1519,7 @@ sign_domain() {
crt_path="${certdir}/cert-${timestamp}.pem" crt_path="${certdir}/cert-${timestamp}.pem"
# shellcheck disable=SC2086 # 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 # Create fullchain.pem
echo " + Creating fullchain.pem..." echo " + Creating fullchain.pem..."
@@ -1690,42 +1566,6 @@ sign_domain() {
echo " + Done!" 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) # Usage: --version (-v)
# Description: Print version information # Description: Print version information
command_version() { command_version() {
@@ -1845,12 +1685,6 @@ parse_domains_txt() {
(grep -vE '^(#|$)' || true) (grep -vE '^(#|$)' || true)
} }
# normalize SAN lists
# normalize IPv6 adresses, and sort alphabetically
normalize_san_list() {
cat | awk '{print tolower($0)}' | _sed 's/ $//' | _sed 's/^ //' | ipv6_normalize | tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//'
}
# Usage: --cron (-c) # Usage: --cron (-c)
# Description: Sign/renew non-existent/changed/expiring certificates. # Description: Sign/renew non-existent/changed/expiring certificates.
command_sign_domains() { command_sign_domains() {
@@ -1947,10 +1781,10 @@ command_sign_domains() {
); do ); do
config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)" config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
config_value="$(echo "${cfgline:1}" | cut -d'=' -f2- | tr -d "'")" config_value="$(echo "${cfgline:1}" | cut -d'=' -f2- | tr -d "'")"
# All settings that are allowed here should also be stored and # All settings that are allowed here should also be stored and
# restored in store_configvars() and reset_configvars() # restored in store_configvars() and reset_configvars()
case "${config_var}" in 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|ACME_PROFILE|ORDER_TIMEOUT|VALIDATION_TIMEOUT|KEEP_GOING) 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)
echo " + ${config_var} = ${config_value}" echo " + ${config_var} = ${config_value}"
declare -- "${config_var}=${config_value}" declare -- "${config_var}=${config_value}"
;; ;;
@@ -1967,18 +1801,16 @@ command_sign_domains() {
skip="no" skip="no"
# Allow for external CSR generation # Allow for external CSR generation
local csrfile="" local csr=""
if [[ -n "${HOOK}" ]]; then if [[ -n "${HOOK}" ]]; then
csr="$("${HOOK}" "generate_csr" "${domain}" "${certdir}" "${domain} ${morenames}")" || _exiterr 'generate_csr hook returned with non-zero exit code' 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 if grep -qE "\-----BEGIN (NEW )?CERTIFICATE REQUEST-----" <<< "${csr}"; then
csrfile="$(_mktemp)" altnames="$(extract_altnames "${csr}")"
cat > "${csrfile}" <<< "${csr}"
altnames="$(extract_altnames "${csrfile}")"
domain="$(cut -d' ' -f1 <<< "${altnames}")" domain="$(cut -d' ' -f1 <<< "${altnames}")"
morenames="$(cut -s -d' ' -f2- <<< "${altnames}")" morenames="$(cut -s -d' ' -f2- <<< "${altnames}")"
echo " + Using CSR from hook script (real names: ${altnames})" echo " + Using CSR from hook script (real names: ${altnames})"
else else
csrfile="" csr=""
fi fi
fi fi
@@ -1986,9 +1818,9 @@ command_sign_domains() {
if [[ -e "${cert}" && "${force_renew}" = "no" ]]; then if [[ -e "${cert}" && "${force_renew}" = "no" ]]; then
printf " + Checking domain name(s) of existing cert..." printf " + Checking domain name(s) of existing cert..."
certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep -E '(DNS|IP( Address)*):' | _sed 's/(DNS|IP( Address)*)://g' | tr -d ' ' | tr ',' ' ' | normalize_san_list )" certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep -E '(DNS|IP( Address*)):' | _sed 's/(DNS|IP( Address)*)://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')"
givennames="$(echo "${domain}" "${morenames}" | _sed 's/ip://g' | normalize_san_list )" givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //')"
if [[ "${certnames}" = "${givennames}" ]]; then if [[ "${certnames}" = "${givennames}" ]]; then
echo " unchanged." echo " unchanged."
else else
@@ -2007,7 +1839,7 @@ command_sign_domains() {
valid="$("${OPENSSL}" x509 -enddate -noout -in "${cert}" | cut -d= -f2- )" valid="$("${OPENSSL}" x509 -enddate -noout -in "${cert}" | cut -d= -f2- )"
printf " + Valid till %s " "${valid}" printf " + Valid till %s " "${valid}"
if ("${OPENSSL}" x509 -checkend $((RENEW_DAYS * 86400)) -in "${cert}" 2>&1 | grep -q "will not expire"); then if ("${OPENSSL}" x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}" > /dev/null 2>&1); then
printf "(Longer than %d days). " "${RENEW_DAYS}" printf "(Longer than %d days). " "${RENEW_DAYS}"
if [[ "${force_renew}" = "yes" ]]; then if [[ "${force_renew}" = "yes" ]]; then
echo "Ignoring because renew was forced!" echo "Ignoring because renew was forced!"
@@ -2028,12 +1860,9 @@ command_sign_domains() {
# Sign certificate for this domain # Sign certificate for this domain
if [[ ! "${skip}" = "yes" ]]; then if [[ ! "${skip}" = "yes" ]]; then
update_ocsp="yes" update_ocsp="yes"
if [[ -n "${csrfile}" ]]; then [[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr"
cat "${csrfile}" > "${certdir}/cert-${timestamp}.csr"
rm "${csrfile}"
fi
# shellcheck disable=SC2086 # shellcheck disable=SC2086
if [[ "${KEEP_GOING:-}" = "yes" ]]; then if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
skip_exit_hook=yes skip_exit_hook=yes
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} & sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} &
wait $! || exit_with_errorcode=1 wait $! || exit_with_errorcode=1
@@ -2044,13 +1873,27 @@ command_sign_domains() {
fi fi
if [[ "${OCSP_FETCH}" = "yes" ]]; then if [[ "${OCSP_FETCH}" = "yes" ]]; then
if [[ "${KEEP_GOING:-}" = "yes" ]]; then local ocsp_url
skip_exit_hook=yes ocsp_url="$(get_ocsp_url "${cert}")"
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}" &
wait $! || exit_with_errorcode=1 if [[ ! -e "${certdir}/ocsp.der" ]]; then
skip_exit_hook=no 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')
else else
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}" echo " + OCSP stapling file is still valid (skipping update)"
fi fi
fi fi
done done
@@ -2061,8 +1904,8 @@ command_sign_domains() {
[[ -n "${HOOK}" ]] && ("${HOOK}" "exit_hook" || echo 'exit_hook returned with non-zero exit code!' >&2) [[ -n "${HOOK}" ]] && ("${HOOK}" "exit_hook" || echo 'exit_hook returned with non-zero exit code!' >&2)
if [[ "${AUTO_CLEANUP}" == "yes" ]]; then if [[ "${AUTO_CLEANUP}" == "yes" ]]; then
echo " + Running automatic cleanup" echo "+ Running automatic cleanup"
PARAM_CLEANUPDELETE="${AUTO_CLEANUP_DELETE:-no}" command_cleanup noinit | _sed 's/^/ + /g' command_cleanup noinit
fi fi
exit "${exit_with_errorcode}" exit "${exit_with_errorcode}"
@@ -2078,18 +1921,19 @@ command_sign_csr() {
exec 3>&1 1>&2 exec 3>&1 1>&2
# load csr # load csr
local csrfile="${1}" csrfile="${1}"
if [ ! -r "${csrfile}" ]; then if [ ! -r "${csrfile}" ]; then
_exiterr "Could not read certificate signing request ${csrfile}" _exiterr "Could not read certificate signing request ${csrfile}"
fi fi
csr="$(cat "${csrfile}")"
# extract names # extract names
altnames="$(extract_altnames "${csrfile}")" altnames="$(extract_altnames "${csr}")"
# gen cert # gen cert
certfile="$(_mktemp)" certfile="$(_mktemp)"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
sign_csr "${csrfile}" ${altnames} 3> "${certfile}" sign_csr "${csr}" ${altnames} 3> "${certfile}"
# print cert # print cert
echo "# CERT #" >&3 echo "# CERT #" >&3
@@ -2514,8 +2358,8 @@ main() {
PARAM_ALPNCERTDIR="${1}" PARAM_ALPNCERTDIR="${1}"
;; ;;
# PARAM_Usage: --challenge (-t) http-01|dns-01|dns-persist-01|tls-alpn-01 # PARAM_Usage: --challenge (-t) http-01|dns-01|tls-alpn-01
# PARAM_Description: Which challenge should be used? Currently http-01, dns-01, dns-persist-01 and tls-alpn-01 are supported # PARAM_Description: Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
--challenge|-t) --challenge|-t)
shift 1 shift 1
check_parameters "${1:-}" check_parameters "${1:-}"
@@ -2529,31 +2373,6 @@ main() {
check_parameters "${1:-}" check_parameters "${1:-}"
PARAM_KEY_ALGO="${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 "Unknown parameter detected: ${1}" >&2
echo >&2 echo >&2

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 87 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -29,24 +29,3 @@ Or when you do have a DNS API, pass the details accordingly to achieve the same
You can delete the TXT record when called with operation `clean_challenge`, when $2 is also the domain name. You can delete the TXT record when called with operation `clean_challenge`, when $2 is also the domain name.
Here are some examples: [Examples for DNS-01 hooks](https://github.com/dehydrated-io/dehydrated/wiki) Here are some examples: [Examples for DNS-01 hooks](https://github.com/dehydrated-io/dehydrated/wiki)
### dns-persist-01 challenge
This script also supports the `dns-persist-01`-type verification. This type of verification requires you to create a persistent `TXT` DNS record containing your Let's Encrypt account information.
Unlike `dns-01`, which requires dynamic DNS record updates for each certificate request, `dns-persist-01` uses a single persistent record that remains in place indefinitely.
You need to create a TXT record named `_validation-persist` in the domain for which you want to request certificates. The record should contain your account URI and other metadata.
Example record:
```
_validation-persist.example.com. IN TXT (
"letsencrypt.org;"
" accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890;"
" policy=wildcard"
)
```
The account URI can be obtained by running `dehydrated --register --accept-terms` and checking the account registration response, or by examining the `accounts/*/registration.json` file after registration.
This record should be set up once and left in place. No hook script is required for `dns-persist-01` as dehydrated does not perform any dynamic DNS updates for this challenge type.

View File

@@ -16,13 +16,16 @@
# Which group should dehydrated run as? This will be implicitly enforced when running as root # Which group should dehydrated run as? This will be implicitly enforced when running as root
#DEHYDRATED_GROUP= #DEHYDRATED_GROUP=
# Should dehydrated pass environment variables over sudo?
#DEHYDRATED_SUDO_ENV="no"
# Resolve names to addresses of IP version only. (curl) # Resolve names to addresses of IP version only. (curl)
# supported values: 4, 6 # supported values: 4, 6
# default: <unset> # default: <unset>
#IP_VERSION= #IP_VERSION=
# URL to certificate authority or internal preset # URL to certificate authority or internal preset
# Presets: letsencrypt, letsencrypt-test, zerossl, buypass, buypass-test, google, google-test # Presets: letsencrypt, letsencrypt-test, zerossl, buypass, buypass-test
# default: letsencrypt # default: letsencrypt
#CA="letsencrypt" #CA="letsencrypt"
@@ -92,8 +95,8 @@
# Chain clean_challenge|deploy_challenge arguments together into one hook call per certificate (default: no) # Chain clean_challenge|deploy_challenge arguments together into one hook call per certificate (default: no)
#HOOK_CHAIN="no" #HOOK_CHAIN="no"
# Minimum days before expiration to automatically renew certificate (default: 32) # Minimum days before expiration to automatically renew certificate (default: 30)
#RENEW_DAYS="32" #RENEW_DAYS="30"
# Regenerate private keys instead of just signing new certificates on renewal (default: yes) # Regenerate private keys instead of just signing new certificates on renewal (default: yes)
#PRIVATE_KEY_RENEW="yes" #PRIVATE_KEY_RENEW="yes"
@@ -125,20 +128,8 @@
# Automatic cleanup (default: no) # Automatic cleanup (default: no)
#AUTO_CLEANUP="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) # ACME API version (default: auto)
#API=auto #API=auto
# Preferred issuer chain (default: <unset> -> uses default chain) # Preferred issuer chain (default: <unset> -> uses default chain)
#PREFERRED_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

@@ -1,97 +0,0 @@
## IP Certificates
In addition to issuing certificates for domain names, the ACME protocol also supports certificates
for IP addresses. Dehydrated has included support for IP identifiers for quite some time, but this
feature only became practically useful once Lets Encrypt made IP certificate issuance publicly
available.
IP certificates can be helpful in scenarios where a service is accessed directly via an address
rather than a hostname, for example in internal networks, appliances, temporary systems, or
environments without reliable DNS.
### Limitations and requirements
Currently, there are a few important constraints to be aware of:
- Validation is only possible using http-01 challenges. This means you must have a web server publicly reachable on the IP address you want to certify.
- Let's Encrypt only issues IP certificates via the shortlived ACME profile. Certificates issued through this profile are currently valid for 7 days.
Because of the short lifetime, its important to renew these certificates frequently and adjust
any automated jobs accordingly.
### Preparing an IP certificate in dehydrated
For convenience, create the certificate directory and a per-certificate configuration file in advance.
Example for an IPv6 address:
```bash
ip="2001:0db8:0:3::1337"
```
Or for IPv4:
```bash
ip="224.13.37.42"
```
Then set up the certificate directory and configuration and add the ip to domains.txt:
```bash
# Create certificate directory
mkdir -p "certs/ip:${ip}"
# Use the shortlived ACME profile for this certificate
echo "ACME_PROFILE=shortlived" >> "certs/ip:${ip}/config"
# Renew this certificate every 4 days
echo "RENEW_DAYS=4" >> "certs/ip:${ip}/config"
# Add IP to domains.txt
echo ip:${ip} >> domains.txt
```
Keep in mind that you also can use aliases for better readability in your directory structure.
See the `domains.txt` documentation for more information.
### Requesting the certificate
Once the directory and configuration are in place, you can request and renew the certificate as usual:
```bash
dehydrated -c
```
Dehydrated will automatically include the IP identifier and use the configured ACME profile.
### Renewal considerations
Since short-lived certificates expire after one week, make sure that:
- Your renewal job runs frequently enough (for example daily or every few days)
- Monitoring or alerting accounts for the much shorter validity period
- Failing to renew in time will result in expired certificates much sooner than with standard domain certificates.
### IPv6 address normalization
To ensure compatibility with Let's Encrypt's seemingly somewhat non-standard handling of IP identifiers,
dehydrated internally normalizes IPv6 addresses before using them as certificate names.
This process first expands and reformats IPv6 notation into a consistent representation, eliminating
shorthand forms such as :: compression. Afterwards it re-shortens the IPv6 address in a way that is
accepted by Let's Encrypt. Doing so guarantees that:
- IPv6 addresses are compatible with Let's Encrypt
- Matching of existing and configured identifiers works, without dependency on special formatting in domains.txt
This happens internally and should be invisible to most users, but if you are running this against
a custom ACME server you might want to be aware of this behaviour.
Example formatting:
- Original IPv6 address: `2001:db8:0:3:0:0:0:1337` (not accepted by Let's Encrypt)
- Fully expanded IPv6 address: `2001:0db8:0000:0003:0000:0000:0000:1337` (also not accepted)
- Re-shortened IPv6 address: `2001:db8:0:3::1337` (gets accepted)

View File

@@ -26,7 +26,7 @@ single certificate valid for both "example.net" and "example.com" through the \f
Alternative Name\fR (SAN) field. Alternative Name\fR (SAN) field.
For the next step, one way of verifying domain name ownership needs to be For the next step, one way of verifying domain name ownership needs to be
configured. Dehydrated implements \fIhttp-01\fR, \fIdns-01\fR, and \fIdns-persist-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 The \fIhttp-01\fR verification provides proof of ownership by providing a
challenge token. In order to do that, the directory referenced in the challenge token. In order to do that, the directory referenced in the
@@ -44,12 +44,6 @@ the software or the DNS provider at hand, there are many third party hooks
available for dehydrated. See \fIdns-verification.md\fR for hooks for popular available for dehydrated. See \fIdns-verification.md\fR for hooks for popular
DNS servers and DNS hosters. DNS servers and DNS hosters.
The \fIdns-persist-01\fR verification works by providing a persistent DNS record
containing account information. Unlike \fIdns-01\fR, this requires setting up a
static TXT record once that remains in place indefinitely. No dynamic DNS
updates are performed during certificate requests. See \fIdns-verification.md\fR
for details on setting up the required DNS record.
Finally, the certificates need to be requested and updated on a regular basis. Finally, the certificates need to be requested and updated on a regular basis.
This can happen through a cron job or a timer. Initially, you may enforce this This can happen through a cron job or a timer. Initially, you may enforce this
by invoking \fIdehydrated -c\fR manually. by invoking \fIdehydrated -c\fR manually.
@@ -145,7 +139,7 @@ secp384r1
The program exits 0 if everything was fine, 1 if an error occurred. The program exits 0 if everything was fine, 1 if an error occurred.
.SH BUGS .SH BUGS
Please report any bugs that you may encounter at the project web site Please report any bugs that you may encounter at the project web site
.UR https://github.com/dehydrated-io/dehydrated/issues .UR https://github.com/lukas2511/dehydrated/issues
.UE . .UE .
.SH AUTHOR .SH AUTHOR
Dehydrated was written by Lukas Schauer. This man page was contributed by Dehydrated was written by Lukas Schauer. This man page was contributed by
@@ -157,5 +151,5 @@ distribution for licensing information.
.SH SEE ALSO .SH SEE ALSO
Full documentation along with configuration examples are provided in the \fIdocs\fR Full documentation along with configuration examples are provided in the \fIdocs\fR
directory of the distribution, or at directory of the distribution, or at
.UR https://github.com/dehydrated-io/dehydrated/tree/master/docs .UR https://github.com/lukas2511/dehydrated/tree/master/docs
.UE . .UE .