mirror of
https://github.com/dehydrated-io/dehydrated.git
synced 2026-05-30 10:30:41 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48866b0e85 | |||
| 9a08e51dbd | |||
| cfd637d769 | |||
| c63d1cb528 | |||
| b9bff54bd6 | |||
| c93c0df78d | |||
| 7ea8aaab5c | |||
| 6f5c9dba64 | |||
| 4a340caf29 | |||
| 2e6933464e | |||
| 1dbbc64ce9 | |||
| 12877bb238 | |||
| ad43e250b2 | |||
| 8e9e5ef9c7 | |||
| a7deeaedbc | |||
| 3d95f18000 | |||
| ce9eb300e2 | |||
| 9cfcd66f15 | |||
| 73bb54a4b2 | |||
| 3a71a7ad94 | |||
| 0290338853 |
@@ -1,11 +1,28 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
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
|
||||||
|
## Fixed
|
||||||
|
- Various bugfixes around IP certificate orders
|
||||||
|
- Implement workaround for OpenSSL regression which broke the time-based validity check
|
||||||
|
|
||||||
|
## Added
|
||||||
|
- Added a configuration parameter to allow for timeouts during domain validation processing (`VALIDATION_TIMEOUT`, defaults to 0 = no timeout)
|
||||||
|
- 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
|
## [0.7.2] - 2025-05-18
|
||||||
## Added
|
## Added
|
||||||
- Implemented support for certificate profile selection
|
- Implemented support for certificate profile selection
|
||||||
- Added a configuration parameter to allow for timeouts during order processing (`ORDER_TIMEOUT`, defaults to 0 = no timeout)
|
- 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)
|
- 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
|
## Changed
|
||||||
- Renew certificates with 32 days remaining (instead of 30) to avoid issues with monthly cronjobs (`RENEW_DAYS=32`)
|
- Renew certificates with 32 days remaining (instead of 30) to avoid issues with monthly cronjobs (`RENEW_DAYS=32`)
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
# dehydrated [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8)
|
<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>
|
||||||
|
|
||||||

|
<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. Let's Encrypt) implemented as a relatively simple (zsh-compatible) bash-script.
|
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.
|
||||||
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).
|
||||||
|
|
||||||
Current features:
|
<br clear="left"/>
|
||||||
|
|
||||||
|
## 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..
|
||||||
|
|
||||||
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,
|
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
|
||||||
@@ -83,10 +86,11 @@ 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|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
|
--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
|
||||||
--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
|
--acme-profile profile_name Use specified ACME profile
|
||||||
--order-timeout seconds Amount of seconds to wait for processing of order until erroring out
|
--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
|
||||||
@@ -94,3 +98,17 @@ 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>
|
||||||
|
|||||||
+139
-37
@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
|
|||||||
exec 3>&-
|
exec 3>&-
|
||||||
exec 4>&-
|
exec 4>&-
|
||||||
|
|
||||||
VERSION="0.7.2"
|
VERSION="0.7.3"
|
||||||
|
|
||||||
# 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}"
|
||||||
@@ -252,6 +252,43 @@ 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"
|
||||||
@@ -293,6 +330,8 @@ store_configvars() {
|
|||||||
__IP_VERSION="${IP_VERSION}"
|
__IP_VERSION="${IP_VERSION}"
|
||||||
__ACME_PROFILE="${ACME_PROFILE}"
|
__ACME_PROFILE="${ACME_PROFILE}"
|
||||||
__ORDER_TIMEOUT=${ORDER_TIMEOUT}
|
__ORDER_TIMEOUT=${ORDER_TIMEOUT}
|
||||||
|
__VALIDATION_TIMEOUT=${VALIDATION_TIMEOUT}
|
||||||
|
__KEEP_GOING=${KEEP_GOING}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_configvars() {
|
reset_configvars() {
|
||||||
@@ -313,6 +352,8 @@ reset_configvars() {
|
|||||||
IP_VERSION="${__IP_VERSION}"
|
IP_VERSION="${__IP_VERSION}"
|
||||||
ACME_PROFILE="${__ACME_PROFILE}"
|
ACME_PROFILE="${__ACME_PROFILE}"
|
||||||
ORDER_TIMEOUT=${__ORDER_TIMEOUT}
|
ORDER_TIMEOUT=${__ORDER_TIMEOUT}
|
||||||
|
VALIDATION_TIMEOUT=${__VALIDATION_TIMEOUT}
|
||||||
|
KEEP_GOING="${__KEEP_GOING}"
|
||||||
}
|
}
|
||||||
|
|
||||||
hookscript_bricker_hook() {
|
hookscript_bricker_hook() {
|
||||||
@@ -325,13 +366,15 @@ hookscript_bricker_hook() {
|
|||||||
|
|
||||||
# verify configuration values
|
# verify configuration values
|
||||||
verify_config() {
|
verify_config() {
|
||||||
[[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" || "${CHALLENGETYPE}" == "tls-alpn-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue."
|
[[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" || "${CHALLENGETYPE}" == "dns-persist-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
|
||||||
[[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue."
|
[[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue."
|
||||||
@@ -339,6 +382,7 @@ verify_config() {
|
|||||||
[[ "${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"
|
[[ "${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
|
||||||
@@ -401,6 +445,8 @@ load_config() {
|
|||||||
API="auto"
|
API="auto"
|
||||||
ACME_PROFILE=""
|
ACME_PROFILE=""
|
||||||
ORDER_TIMEOUT=0
|
ORDER_TIMEOUT=0
|
||||||
|
VALIDATION_TIMEOUT=0
|
||||||
|
KEEP_GOING="no"
|
||||||
|
|
||||||
if [[ -z "${CONFIG:-}" ]]; then
|
if [[ -z "${CONFIG:-}" ]]; then
|
||||||
echo "#" >&2
|
echo "#" >&2
|
||||||
@@ -560,6 +606,8 @@ load_config() {
|
|||||||
[[ -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_ACME_PROFILE:-}" ]] && ACME_PROFILE="${PARAM_ACME_PROFILE}"
|
||||||
[[ -n "${PARAM_ORDER_TIMEOUT:-}" ]] && ORDER_TIMEOUT="${PARAM_ORDER_TIMEOUT}"
|
[[ -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)"
|
||||||
@@ -740,7 +788,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 -s "https://api.zerossl.com/acme/eab-credentials-email" -d "email=${CONTACT_EMAIL}" | jsonsh)"
|
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)"
|
||||||
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
|
||||||
@@ -928,14 +976,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:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
|
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)"
|
||||||
curlret="${?}"
|
curlret="${?}"
|
||||||
touch "${tempheaders}"
|
touch "${tempheaders}"
|
||||||
elif [[ "${1}" = "get" ]]; then
|
elif [[ "${1}" = "get" ]]; then
|
||||||
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" "${2}")"
|
statuscode="$(curl ${ip_version:-} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" ${CURL_OPTS} -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" -H 'Cache-Control: no-cache' "${2}")"
|
||||||
curlret="${?}"
|
curlret="${?}"
|
||||||
elif [[ "${1}" = "post" ]]; then
|
elif [[ "${1}" = "post" ]]; then
|
||||||
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -D "${tempheaders}" -H 'Content-Type: application/jose+json' -d "${3}")"
|
statuscode="$(curl ${ip_version:-} -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}")"
|
||||||
curlret="${?}"
|
curlret="${?}"
|
||||||
else
|
else
|
||||||
set -e
|
set -e
|
||||||
@@ -1131,7 +1179,11 @@ sign_csr() {
|
|||||||
local challenge_identifiers=""
|
local challenge_identifiers=""
|
||||||
for altname in ${altnames}; do
|
for altname in ${altnames}; do
|
||||||
if [[ "${altname}" =~ ^ip: ]]; then
|
if [[ "${altname}" =~ ^ip: ]]; then
|
||||||
challenge_identifiers+="$(printf '{"type": "ip", "value": "%s"}, ' "${altname:3}")"
|
ip="${altname:3}"
|
||||||
|
if [[ "${ip}" =~ : ]]; then
|
||||||
|
ip="$(ipv6_normalize <<< "${ip}")"
|
||||||
|
fi
|
||||||
|
challenge_identifiers+="$(printf '{"type": "ip", "value": "%s"}, ' "${ip}")"
|
||||||
else
|
else
|
||||||
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
|
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
|
||||||
fi
|
fi
|
||||||
@@ -1196,7 +1248,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}"
|
_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."
|
||||||
fi
|
fi
|
||||||
challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")"
|
challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")"
|
||||||
|
|
||||||
@@ -1231,6 +1283,10 @@ 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}"
|
||||||
@@ -1251,6 +1307,7 @@ 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
|
||||||
@@ -1265,6 +1322,7 @@ sign_csr() {
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Validate pending challenges
|
# Validate pending challenges
|
||||||
local idx=0
|
local idx=0
|
||||||
@@ -1280,8 +1338,14 @@ 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
|
||||||
@@ -1303,6 +1367,7 @@ 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
|
||||||
@@ -1321,6 +1386,7 @@ sign_csr() {
|
|||||||
[[ -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 :("
|
||||||
@@ -1554,7 +1620,7 @@ sign_domain() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [[ "${domain}" =~ ^ip: ]]; then
|
if [[ "${domain}" =~ ^ip: ]]; then
|
||||||
SUBJ="/CN=${domain:3}/"
|
SUBJ="/"
|
||||||
else
|
else
|
||||||
SUBJ="/CN=${domain}/"
|
SUBJ="/CN=${domain}/"
|
||||||
fi
|
fi
|
||||||
@@ -1624,6 +1690,42 @@ 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() {
|
||||||
@@ -1743,6 +1845,12 @@ 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() {
|
||||||
@@ -1842,7 +1950,7 @@ command_sign_domains() {
|
|||||||
# 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)
|
KEY_ALGO|OCSP_MUST_STAPLE|OCSP_FETCH|OCSP_DAYS|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|PREFERRED_CHAIN|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS|ACME_PROFILE|ORDER_TIMEOUT|VALIDATION_TIMEOUT|KEEP_GOING)
|
||||||
echo " + ${config_var} = ${config_value}"
|
echo " + ${config_var} = ${config_value}"
|
||||||
declare -- "${config_var}=${config_value}"
|
declare -- "${config_var}=${config_value}"
|
||||||
;;
|
;;
|
||||||
@@ -1878,8 +1986,8 @@ 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 ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')"
|
certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep -E '(DNS|IP( Address)*):' | _sed 's/(DNS|IP( Address)*)://g' | tr -d ' ' | tr ',' ' ' | normalize_san_list )"
|
||||||
givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //')"
|
givennames="$(echo "${domain}" "${morenames}" | _sed 's/ip://g' | normalize_san_list )"
|
||||||
|
|
||||||
if [[ "${certnames}" = "${givennames}" ]]; then
|
if [[ "${certnames}" = "${givennames}" ]]; then
|
||||||
echo " unchanged."
|
echo " unchanged."
|
||||||
@@ -1899,7 +2007,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)) -noout -in "${cert}" > /dev/null 2>&1); then
|
if ("${OPENSSL}" x509 -checkend $((RENEW_DAYS * 86400)) -in "${cert}" 2>&1 | grep -q "will not expire"); 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!"
|
||||||
@@ -1925,7 +2033,7 @@ command_sign_domains() {
|
|||||||
rm "${csrfile}"
|
rm "${csrfile}"
|
||||||
fi
|
fi
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
|
if [[ "${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
|
||||||
@@ -1936,27 +2044,13 @@ command_sign_domains() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${OCSP_FETCH}" = "yes" ]]; then
|
if [[ "${OCSP_FETCH}" = "yes" ]]; then
|
||||||
local ocsp_url
|
if [[ "${KEEP_GOING:-}" = "yes" ]]; then
|
||||||
ocsp_url="$(get_ocsp_url "${cert}")"
|
skip_exit_hook=yes
|
||||||
|
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}" &
|
||||||
if [[ ! -e "${certdir}/ocsp.der" ]]; then
|
wait $! || exit_with_errorcode=1
|
||||||
update_ocsp="yes"
|
skip_exit_hook=no
|
||||||
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
|
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}"
|
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}"
|
||||||
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
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -2420,8 +2514,8 @@ main() {
|
|||||||
PARAM_ALPNCERTDIR="${1}"
|
PARAM_ALPNCERTDIR="${1}"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
# PARAM_Usage: --challenge (-t) http-01|dns-01|tls-alpn-01
|
# PARAM_Usage: --challenge (-t) http-01|dns-01|dns-persist-01|tls-alpn-01
|
||||||
# PARAM_Description: Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
|
# PARAM_Description: Which challenge should be used? Currently http-01, dns-01, dns-persist-01 and tls-alpn-01 are supported
|
||||||
--challenge|-t)
|
--challenge|-t)
|
||||||
shift 1
|
shift 1
|
||||||
check_parameters "${1:-}"
|
check_parameters "${1:-}"
|
||||||
@@ -2452,6 +2546,14 @@ main() {
|
|||||||
PARAM_ORDER_TIMEOUT=${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
|
After Width: | Height: | Size: 87 KiB |
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 87 KiB |
@@ -29,3 +29,24 @@ 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.
|
||||||
|
|||||||
@@ -139,3 +139,6 @@
|
|||||||
|
|
||||||
# Amount of seconds to wait for processing of order until erroring out (default: 0 => no timeout)
|
# Amount of seconds to wait for processing of order until erroring out (default: 0 => no timeout)
|
||||||
#ORDER_TIMEOUT=0
|
#ORDER_TIMEOUT=0
|
||||||
|
|
||||||
|
# Skip over errors during certificate orders and updating of OCSP stapling information (default: no)
|
||||||
|
#KEEP_GOING=no
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
## 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 and \fIdns-01\fR verification.
|
configured. Dehydrated implements \fIhttp-01\fR, \fIdns-01\fR, and \fIdns-persist-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,6 +44,12 @@ 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user