21 Commits

Author SHA1 Message Date
Raphael Michel 48866b0e85 Allow to override user agent with CURL_OPTS (closes #986) 2026-04-30 16:31:05 +02:00
Lukas Schauer 9a08e51dbd added note about checking with CA for supported validation methods 2026-04-30 16:15:24 +02:00
Youfu Zhang cfd637d769 Add DNS-PERSIST-01 challenge support
- Add dns-persist-01 to allowed challenge types in verify_config()
- Implement dns-persist-01 case in challenge preparation (no dynamic token)
- Skip deployment and cleanup for dns-persist-01
- Update help text and documentation
- Add man page and README updates
- Update CHANGELOG
2026-04-30 16:10:16 +02:00
radub2012 c63d1cb528 Update zeroapi curl call to use ip_version + CURL_OPTS + dehydrated user-agent (closes #995) 2026-04-30 16:10:15 +02:00
Dominik Rimpf b9bff54bd6 fix: simplify SAN comparison + SAN regex (closes #996) 2026-04-30 16:10:14 +02:00
Lukas Schauer c93c0df78d readme branding 2026-04-30 16:10:13 +02:00
Lukas Schauer 7ea8aaab5c some documentation 2026-02-04 00:34:39 +01:00
Lukas Schauer 6f5c9dba64 ipv6 address formatting for letsencrypt compatibility and better detection of changed certificate names 2026-02-04 00:25:41 +01:00
Lukas Schauer 4a340caf29 clean up some whitespace 2026-02-03 22:02:52 +01:00
Lukas Schauer 2e6933464e remove noout flag from time-based validity check 2026-02-03 22:01:15 +01:00
Lukas Schauer 1dbbc64ce9 implement workaround for openssl regression (fixes #981)
The introduction of the `-multi` option to the x509 subcommand
introduced a regression to the `-checkend` behaviour, preventing
openssl to correctly indicate the certificate expiry status via
its exit code.

This commit introduces a (maybe temporary) workaround by instead
checking the output string.
2025-10-24 09:22:31 +02:00
Lukas Schauer 12877bb238 throw error with information about OCSP deprecation if certificate doesn't indicate OCSP support 2025-07-05 11:13:45 +02:00
Lukas Schauer ad43e250b2 allow KEEP_GOING to also skip over ocsp stapling errors, update ocsp error message with a hint about deprecation on some CAs 2025-07-05 10:55:33 +02:00
Lukas Schauer 8e9e5ef9c7 also allow setting KEEP_GOING as a config option 2025-07-05 10:54:29 +02:00
Lukas Schauer a7deeaedbc set empty subject for ip-certificates
as suggested by @candlerb in #783
2025-07-05 10:28:13 +02:00
Victor Coss 3d95f18000 Don't allow CDN's to send cached responses
A lot of CA's use a CDN service to protect and speed up their ACME service. These CDN services can sometimes miss-behave and send cached results. For example DigiCert's ACME service uses the Imperva CDN. It will send cached results on the DNS validation, challenge endpoint, resulting in it being stuck in the processing status, thus dehydrated is hung and never gets the certificate.
2025-06-17 19:52:29 +02:00
Lukas Schauer ce9eb300e2 implemented domain validation timeout 2025-06-17 19:51:27 +02:00
Lukas Schauer 9cfcd66f15 small addition to 0.7.2 changelog 2025-05-18 02:28:57 +02:00
Lukas Schauer 73bb54a4b2 updated changelog 2025-05-18 02:16:14 +02:00
Lukas Schauer 3a71a7ad94 only validate existance of wellknown directory or hook script when actually necessary (fixes #965) 2025-05-18 02:07:04 +02:00
Lukas Schauer 0290338853 post-v0.7.2-release 2025-05-18 01:36:16 +02:00
9 changed files with 3126 additions and 88 deletions
+17
View File
@@ -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`)
+24 -6
View File
@@ -1,22 +1,25 @@
# dehydrated [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8) <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>
![](docs/logo.png) <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
View File
@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
exec 3>&- exec 3>&-
exec 4>&- exec 4>&-
VERSION="0.7.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
+1387
View File
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

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