mirror of
https://github.com/dehydrated-io/dehydrated.git
synced 2026-05-06 07:03:28 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
911a822c0c |
27
CHANGELOG
27
CHANGELOG
@@ -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
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -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 [](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" />
|

|
||||||
|
|
||||||
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>
|
|
||||||
|
|||||||
403
dehydrated
403
dehydrated
@@ -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
|
||||||
|
|||||||
1387
docs/banner-dark.svg
1387
docs/banner-dark.svg
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 |
@@ -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.
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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 Let’s 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, it’s 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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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 .
|
||||||
|
|||||||
Reference in New Issue
Block a user