15 Commits

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

View File

@@ -1,11 +1,27 @@
# Change Log
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
## 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`)

View File

@@ -87,6 +87,7 @@ Parameters:
--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

View File

@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
exec 3>&-
exec 4>&-
VERSION="0.7.2"
VERSION="0.7.3"
# Find directory in which this script is stored by traversing all symbolic links
SOURCE="${0}"
@@ -252,6 +252,43 @@ ip_to_ptr() {
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
ipv6_expand <<< "${domain}" | ipv6_shorten
else
printf "%s" "${domain}"
fi
printf " "
done | sed -e 's/ $//'
}
# Create (identifiable) temporary files
_mktemp() {
mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
@@ -293,6 +330,8 @@ store_configvars() {
__IP_VERSION="${IP_VERSION}"
__ACME_PROFILE="${ACME_PROFILE}"
__ORDER_TIMEOUT=${ORDER_TIMEOUT}
__VALIDATION_TIMEOUT=${VALIDATION_TIMEOUT}
__KEEP_GOING=${KEEP_GOING}
}
reset_configvars() {
@@ -313,6 +352,8 @@ reset_configvars() {
IP_VERSION="${__IP_VERSION}"
ACME_PROFILE="${__ACME_PROFILE}"
ORDER_TIMEOUT=${__ORDER_TIMEOUT}
VALIDATION_TIMEOUT=${__VALIDATION_TIMEOUT}
KEEP_GOING="${__KEEP_GOING}"
}
hookscript_bricker_hook() {
@@ -326,12 +367,14 @@ hookscript_bricker_hook() {
# verify configuration values
verify_config() {
[[ "${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
_exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue."
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."
fi
fi
[[ "${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
[[ "${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}"
[[ "${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
@@ -401,6 +445,8 @@ load_config() {
API="auto"
ACME_PROFILE=""
ORDER_TIMEOUT=0
VALIDATION_TIMEOUT=0
KEEP_GOING="no"
if [[ -z "${CONFIG:-}" ]]; then
echo "#" >&2
@@ -560,6 +606,8 @@ load_config() {
[[ -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
_exiterr "Argument --force-validation can only be used in combination with --force (-x)"
@@ -928,14 +976,14 @@ http_request() {
set +e
# shellcheck disable=SC2086
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:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" -H 'Cache-Control: no-cache' "${2}" -I)"
curlret="${?}"
touch "${tempheaders}"
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:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" -H 'Cache-Control: no-cache' "${2}")"
curlret="${?}"
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:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -D "${tempheaders}" -H 'Cache-Control: no-cache' -H 'Content-Type: application/jose+json' -d "${3}")"
curlret="${?}"
else
set -e
@@ -1131,7 +1179,11 @@ sign_csr() {
local challenge_identifiers=""
for altname in ${altnames}; do
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
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
fi
@@ -1280,8 +1332,14 @@ sign_csr() {
reqstatus="$(echo "${result}" | get_json_string_value status)"
local waited=0
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
waited=$((waited+1))
if [[ "${API}" -eq 2 ]]; then
result="$(signed_request "${challenge_uris[${idx}]}" "" | jsonsh)"
else
@@ -1554,7 +1612,7 @@ sign_domain() {
fi
done
if [[ "${domain}" =~ ^ip: ]]; then
SUBJ="/CN=${domain:3}/"
SUBJ="/"
else
SUBJ="/CN=${domain}/"
fi
@@ -1624,6 +1682,42 @@ sign_domain() {
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)
# Description: Print version information
command_version() {
@@ -1842,7 +1936,7 @@ command_sign_domains() {
# All settings that are allowed here should also be stored and
# restored in store_configvars() and reset_configvars()
case "${config_var}" in
KEY_ALGO|OCSP_MUST_STAPLE|OCSP_FETCH|OCSP_DAYS|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|PREFERRED_CHAIN|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS|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}"
declare -- "${config_var}=${config_value}"
;;
@@ -1878,8 +1972,8 @@ command_sign_domains() {
if [[ -e "${cert}" && "${force_renew}" = "no" ]]; then
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/ $//')"
givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //')"
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/ $//' | awk '{print tolower($0)}' | ipv6_normalize)"
givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //' | ipv6_normalize)"
if [[ "${certnames}" = "${givennames}" ]]; then
echo " unchanged."
@@ -1899,7 +1993,7 @@ command_sign_domains() {
valid="$("${OPENSSL}" x509 -enddate -noout -in "${cert}" | cut -d= -f2- )"
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}"
if [[ "${force_renew}" = "yes" ]]; then
echo "Ignoring because renew was forced!"
@@ -1925,7 +2019,7 @@ command_sign_domains() {
rm "${csrfile}"
fi
# shellcheck disable=SC2086
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
if [[ "${KEEP_GOING:-}" = "yes" ]]; then
skip_exit_hook=yes
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} &
wait $! || exit_with_errorcode=1
@@ -1936,27 +2030,13 @@ command_sign_domains() {
fi
if [[ "${OCSP_FETCH}" = "yes" ]]; then
local ocsp_url
ocsp_url="$(get_ocsp_url "${cert}")"
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 "Error while fetching OCSP information: ${ocsp_log}"
if [[ "${KEEP_GOING:-}" = "yes" ]]; then
skip_exit_hook=yes
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}" &
wait $! || exit_with_errorcode=1
skip_exit_hook=no
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
echo " + OCSP stapling file is still valid (skipping update)"
update_ocsp_stapling "${certdir}" "${update_ocsp}" "${cert}" "${chain}"
fi
fi
done
@@ -2452,6 +2532,14 @@ main() {
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 >&2

View File

@@ -139,3 +139,6 @@
# 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

97
docs/ip-certificates.md Normal file
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)