52 Commits

Author SHA1 Message Date
Lukas Schauer
911a822c0c added option to pass environment variables over sudo 2024-08-04 12:07:34 +02:00
Wilfried Teiken
4fd777e87e Ignore output of 'openssl req -verify'.
Newer versions of openssl seem to send the verify outout to stdout instead of
stderr in the past. Ignore that output when retrieving altnames.
2023-12-05 02:36:40 +01:00
Lukas Schauer
e3ef43c816 fix zsh compatibility (fixes #896) 2023-01-16 22:41:05 +01:00
Alexander Sulfrian
67b111a7b0 Replace all escaped slashes in json strings (closes #866)
${var/pattern/string} will only replace the first occurence. We should
use ${var//pattern/string} to replace all escaped slashes.
2022-10-31 16:27:16 +01:00
Daniel Molkentin
fa68ad8b23 improve man page based on feedback from debian-l10n-english (fixes #873, closes #875)
Also propagate changes to dehydrated help and README.md
2022-10-31 16:22:04 +01:00
Lukas Schauer
5c4adf6baa added note about dehydrated irc channel 2022-10-31 15:46:28 +01:00
Lukas Schauer
35bfea55b6 increase dehydrated version for git master use 2022-10-31 15:46:07 +01:00
Lukas Schauer
ea84199863 release 0.7.1 (it finally happened!) 2022-10-31 15:12:38 +01:00
Krayon
6091ba4bc2 Add missing checks and fix hexdump output (closes #878) 2022-10-31 15:12:04 +01:00
Lukas Schauer
6fb8eba56a implemented workaround for retrying on badNonce errors 2022-09-07 15:09:57 +02:00
Simon Deziel
19c7fbbf47 egrep is deprecated
egrep has been deprecated since 2007 and warns it's obsolete since:
https://git.savannah.gnu.org/cgit/grep.git/commit/?id=a9515624709865d480e3142fd959bccd1c9372d1

Signed-off-by: Simon Deziel <simon@sdeziel.info>
2022-04-07 21:49:56 +02:00
Lukas Schauer
7128e6b63c rfc8738: fix CN on certs with mixed ip+dns 2022-04-07 01:34:21 +02:00
Lukas Schauer
861f4c733d rfc8738: only replace ip with reverse dns thingy if tls-alpn-01 is used 2022-04-07 01:33:48 +02:00
Lukas Schauer
ad3f08084c implemented rfc 8738 support 2022-04-06 22:23:43 +02:00
Lukas Schauer
784fb806c8 really reverted regression in somehow broken array expansion from e963438c.. 2021-11-02 09:05:19 +01:00
Lukas Schauer
b2574b16d1 reverted regression in somehow broken array expansion from e963438c (fixes #850) 2021-11-02 09:01:00 +01:00
Lukas Schauer
da641588ce removed old logo 2021-11-01 19:25:17 +01:00
Lukas Schauer
8e6ddf6286 readme and (temporary) logo update 2021-11-01 19:22:50 +01:00
Lukas Schauer
8e5977890a fix regression from e963438c (fixes #849) 2021-11-01 18:57:57 +01:00
Lukas Schauer
3bcf0c7f5a use noglob helpers for domains.txt.d parsing 2021-11-01 18:57:49 +01:00
Lukas Schauer
b347bc9086 added some changes to changelog 2021-10-31 22:58:06 +01:00
Lukas Schauer
08477170e9 Exit with error if somebody is trying to use EC account keys with ACME v1 2021-10-31 22:36:40 +01:00
Lukas Schauer
f4cf92bae5 extend ec algorithms with secp521r1 (not yet supported by LetsEncrypt, but maybe by other CAs) 2021-10-31 22:31:09 +01:00
Lukas Schauer
93573cda3c experimental support for ec account keys (fixes #827) 2021-10-31 22:29:44 +01:00
Lukas Schauer
607a6088d3 Avoid writing ec-parameters to private-key file (fixes #830) 2021-10-31 20:20:40 +01:00
Lukas Schauer
880c99aa63 Better solution for issue #845 2021-10-31 20:11:31 +01:00
Lukas Schauer
7ac25358ef Show error if chain is configured for a CA which doesn't offer alternate chains (fixes #845) 2021-10-31 20:06:50 +01:00
Lukas Schauer
5733863b93 added warning about possible behaviour-change with new domains.txt.d feature 2021-10-31 19:55:41 +01:00
Marc Schütz
f6a84a88fa Support reading domains from drop-in snippets in domains.txt.d 2021-10-31 19:48:28 +01:00
Lukas Schauer
e963438c5a make shellcheck happy again 2021-10-31 19:33:03 +01:00
Stefaan Ghysels
095165ee96 Only check existing certs when necessary 2021-10-31 19:29:00 +01:00
Simon Deziel
199cd59774 Remove debug echo in command_cleanup()
Signed-off-by: Simon Deziel <simon@sdeziel.info>
2021-10-31 19:23:01 +01:00
Elan Ruusamäe
e17456778f Use consistent indent in hook.sh 2021-10-31 19:21:26 +01:00
Brian Bennett
71f6bc617e Better handling around grep/awk 2021-10-31 19:17:49 +01:00
Joao Morais
6ee4ae508e fix command_version on Darwin/macOS 11
Current output of `uname` on Darwin/macOS 11 is only `Darwin`, which
breaks the premisse used in `command_version()`. This update adds
`Darwin` alongside `BSD`.
2021-10-31 19:08:52 +01:00
27o
91cccc0c23 ensure newline before new section in openssl.cnf
openssl.cnf may not end with a newline. The section [SAN] will then not be found as it is added to the last line of openssl.cnf.
2021-04-23 02:03:23 +02:00
Lukas Schauer
ab016803dd expand documentation on using letsencrypt staging ca 2021-04-18 03:48:47 +02:00
Nick
7d8573af12 Update staging.md to use ACMEv2 server (closes #812)
letsencrypt is phasing out the v1 server:

```
  + ERROR: An error occurred while sending get-request to https://acme-staging.api.letsencrypt.org/directory (Status 403)

Details:
HTTP/2 403
server: nginx
date: Thu, 01 Apr 2021 20:48:17 GMT
content-type: application/problem+json
content-length: 189
etag: "600b3710-bd"

{
  "type": "urn:acme:error:serverInternal",
  "detail": "ACMEv1 Brownout in Progress. ACMEv1 will fully turn off on June 1, 2021. Check https://letsencrypt.status.io/ for more details."
}

```
2021-04-18 03:44:41 +02:00
Lukas Schauer
fb06530097 command_sign_csr: redirect fds after init_system (fixes #816) 2021-04-16 14:32:05 +02:00
Lukas Schauer
5c1551e946 remove some dots :) 2021-03-29 20:20:52 +02:00
Marcus Rückert
20c27b291c Add more examples to show case how to create certs
e.g. with different key algorithms
2021-03-29 20:19:41 +02:00
Lukas Schauer
24f66a3473 generic support for weird curl versions with lower-case headers and no whitespace 2021-03-21 20:51:10 +01:00
joele89
21bff55b7c Updating nonce handler for newer versions of F5 2021-03-21 20:46:31 +01:00
Glenn Strauss
374fce0249 document using -t tls-alpn-01 with lighttpd 2021-03-21 20:42:23 +01:00
Glenn Strauss
00941472b2 add -t tls-alpn-01 to command line help 2021-02-18 16:56:05 +01:00
Michel Lespinasse
527933db24 Per-certificate config fixes
- Ensure that all per-certificate settings are saved and restored in
  store_configvars() and reset_configvars() - that's what makes them
  per-certificate in the first place...

- Add OCSP_FETCH and OCSP_DAYS in the documented list of supported
  per-certificate configs, since the code does allow these.
2021-02-18 16:51:14 +01:00
Nathan Felton
33a421f1e4 Support for LibreSSL version of openssl on macOS 2021-02-18 16:47:24 +01:00
Lukas Schauer
dd0bbd2405 update copyright year 2021-02-18 16:47:23 +01:00
Lukas Schauer
26660e11c7 Fixed small unassigned variable issue 2020-12-12 03:12:13 +01:00
Lukas Schauer
316054ad1c Do not revalidate authorizations on forced renewal
This commit introduces a new cli argument `--force-validation` which,
when used in combination with `--force` ignores valid domain
authorizations and forces a revalidation.

This has been implemented since at least LE seems to have changed some
behavior on valid authorizations. Only the previously validated
authorization-type is reusable, causing dehydrated to error out when
changing from recently validated authorization types while still trying
to force-renew certificates for whatever reason (e.g. changing algorithms).
2020-12-12 03:01:59 +01:00
Lukas Schauer
29b67962ac fix CN extraction for older openssl versions 2020-12-11 18:02:51 +01:00
Lukas Schauer
3a7795589b bump changelog for new draft releases 2020-12-10 16:56:13 +01:00
14 changed files with 537 additions and 294 deletions

View File

@@ -1,6 +1,25 @@
# Change Log
This file contains a log of major changes in dehydrated
## [x.x.x] - xxxx-xx-xx
## Added
- New config variable `DEHYDRATED_SUDO_ENV` to allow passing environment variables over sudo calls
## [0.7.1] - 2022-10-31
## Changed
- `--force` no longer forces domain name revalidation by default, a new argument `--force-validation` has been added for that
- Added support for EC secp521r1 algorithm (works with e.g. zerossl)
- `EC PARAMETERS` are no longer written to privkey.pem (didn't seem necessary and was causing issues with various software)
## Fixed
- Requests resulting in `badNonce` errors are now automatically retried (fixes operation with LE staging servers)
- Deprecated `egrep` usage has been removed
## Added
- Implemented EC for account keys
- Domain list now also read from domains.txt.d subdirectory (behaviour might change, see docs)
- Implemented RFC 8738 (validating/signing certificates for IP addresses instead of domain names) support (this will not work with most public CAs, if any!)
## [0.7.0] - 2020-12-10
## Added
- Support for external account bindings

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2018 Lukas Schauer
Copyright (c) 2015-2021 Lukas Schauer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,9 +1,6 @@
# 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)
Quick note: dehydrated moved, the license will NOT change, and I will still take care of the project.
See https://lukas.im/2020/01/30/selling-dehydrated/index.html for more details.
![](docs/logo.jpg)
![](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.
This client supports both ACME v1 and the new ACME v2 including support for wildcard certificates!
@@ -17,6 +14,7 @@ Current features:
- 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
- Certificate revocation
- 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,
but please check for duplicates first (feel free to comment on those to get things rolling).
@@ -73,7 +71,8 @@ Parameters:
--ca url/preset Use specified CA URL or preset
--alias certalias Use specified name for certificate directory (and per-certificate config) instead of the primary domain (only used if --domain is specified)
--keep-going (-g) Keep going after encountering an error while creating/renewing multiple certificates in cron mode
--force (-x) Force renew of certificate even if it is longer valid than value in RENEW_DAYS
--force (-x) Force certificate renewal even if it is not due to expire within RENEW_DAYS
--force-validation Force revalidation of domain names (used in combination with --force)
--no-lock (-n) Don't use lockfile (potentially dangerous!)
--lock-suffix example.com Suffix lockfile name with a string (useful for with -d)
--ocsp Sets option in CSR indicating OCSP stapling to be mandatory
@@ -84,28 +83,12 @@ Parameters:
--preferred-chain issuer-cn Use alternative certificate chain identified by issuer CN
--out (-o) certs/directory Output certificates into the specified directory
--alpn alpn-certs/directory Output alpn verification certificates into the specified directory
--challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported
--challenge (-t) http-01|dns-01|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
```
## Donate
## Chat
I'm a student hacker with a few (unfortunately) quite expensive hobbies (self-hosting, virtualization clusters, routing,
high-speed networking, embedded hardware, etc.).
I'm really having fun playing around with hard- and software and I'm steadily learning new things.
Without those hobbies I probably would never have started working on dehydrated to begin with :)
Dehydrated has an official IRC-channel `#dehydrated` on libera.chat that can be used for general discussion and suggestions.
I'd really appreciate if you could [donate a bit of money](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8)
so I can buy cool stuff (while still being able to afford food :D).
If you have hardware laying around that you think I'd enjoy playing with (e.g. decommissioned but still modern-ish servers,
10G networking hardware, enterprise grade routers or APs, interesting ARM/MIPS boards, etc.) and that you would be willing
to ship to me please contact me at `donations@dehydrated.io` or on Twitter [@lukas2511](https://twitter.com/lukas2511).
If you want your name to be added to the [donations list](https://dehydrated.io/donations.html) please add a note or send me an
email `donations@dehydrated.io`. I respect your privacy and won't publish your name without permission.
Other ways of donating:
- [My Amazon Wishlist](http://www.amazon.de/registry/wishlist/1TUCFJK35IO4Q)
- Monero: 4Kkf4tF4r9DakxLj37HDXLJgmpVfQoFhT7JLDvXwtUZZMTbsK9spsAPXivWPAFcDUj6jHhY8hJSHX8Cb8ndMhKeQHPSkBZZiK89Fx8NTHk
- Bitcoin: 12487bHxcrREffTGwUDnoxF1uYxCA7ztKK
The channel can also be accessed with Matrix using the official libera.chat bridge at `#dehydrated:libera.chat`.

View File

@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
exec 3>&-
exec 4>&-
VERSION="0.7.0"
VERSION="0.7.2"
# Find directory in which this script is stored by traversing all symbolic links
SOURCE="${0}"
@@ -31,6 +31,22 @@ SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
BASEDIR="${SCRIPTDIR}"
ORIGARGS=("${@}")
noglob_set() {
if [[ -n "${ZSH_VERSION:-}" ]]; then
set +o noglob
else
set +f
fi
}
noglob_clear() {
if [[ -n "${ZSH_VERSION:-}" ]]; then
set -o noglob
else
set -f
fi
}
# Generate json.sh path matching string
json_path() {
if [ ! "${1}" = "-p" ]; then
@@ -55,7 +71,6 @@ get_json_array_values() {
# Get sub-dictionary from json
get_json_dict_value() {
local filter
echo "$(json_path "${1:-}" "${2:-}")"
filter="$(printf 's/.*\[%s\][[:space:]]*\(.*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")"
sed -n "${filter}" | jsonsh
}
@@ -88,7 +103,7 @@ jsonsh() {
awk_egrep () {
local pattern_string=$1
gawk '{
awk '{
while ($0) {
start=match($0, pattern);
token=substr($0, start, RLENGTH);
@@ -103,14 +118,15 @@ jsonsh() {
local ESCAPE
local CHAR
if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1
if echo "test string" | grep -Eao --color=never "test" >/dev/null 2>&1
then
GREP='egrep -ao --color=never'
GREP='grep -Eao --color=never'
else
GREP='egrep -ao'
GREP='grep -Eao'
fi
if echo "test string" | egrep -o "test" >/dev/null 2>&1
# shellcheck disable=SC2196
if echo "test string" | grep -Eao "test" >/dev/null 2>&1
then
ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
CHAR='[^[:cntrl:]"\\]'
@@ -126,10 +142,11 @@ jsonsh() {
local SPACE='[[:space:]]+'
# Force zsh to expand $A into multiple words
local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')
if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$"
if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi
local is_wordsplit_disabled
is_wordsplit_disabled="$(unsetopt 2>/dev/null | grep -c '^shwordsplit$' || true)"
if [ "${is_wordsplit_disabled}" != "0" ]; then setopt shwordsplit; fi
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | grep -Ev "^$SPACE$"
if [ "${is_wordsplit_disabled}" != "0" ]; then unsetopt shwordsplit; fi
}
parse_array () {
@@ -194,17 +211,14 @@ jsonsh() {
}
parse_value () {
local jpath="${1:+$1,}${2:-}" isleaf=0 isempty=0 print=0
local jpath="${1:+$1,}${2:-}"
case "$token" in
'{') parse_object "$jpath" ;;
'[') parse_array "$jpath" ;;
# At this point, the only valid single-character tokens are digits.
''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;;
*) value=$token
*) value="${token//\\\///}"
# replace solidus ("\/") in json strings with normalized value: "/"
value=$(echo "$value" | sed 's#\\/#/#g')
isleaf=1
[ "$value" = '""' ] && isempty=1
;;
esac
[ "$value" = '' ] && return
@@ -227,16 +241,26 @@ jsonsh() {
tokenize | parse
}
# Convert IP addresses to their reverse dns variants.
# Used for ALPN certs as validation for IPs uses this in SNI since IPs aren't allowed there.
ip_to_ptr() {
ip="$(cat)"
if [[ "${ip}" =~ : ]]; then
printf "%sip6.arpa" "$(printf "%s" "${ip}" | awk -F: 'BEGIN {OFS=""; }{addCount = 9 - NF; for(i=1; i<=NF;i++){if(length($i) == 0){ for(j=1;j<=addCount;j++){$i = ($i "0000");} } else { $i = substr(("0000" $i), length($i)+5-4);}}; print}' | rev | sed -e "s/./&./g")"
else
printf "%s.in-addr.arpa" "$(printf "%s" "${ip}" | awk -F. '{print $4"."$3"." $2"."$1}')"
fi
}
# Create (identifiable) temporary files
_mktemp() {
# shellcheck disable=SC2068
mktemp ${@:-} "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
}
# Check for script dependencies
check_dependencies() {
# look for required binaries
for binary in grep mktemp diff sed awk curl cut; do
for binary in grep mktemp diff sed awk curl cut head tail hexdump; do
bin_path="$(command -v "${binary}" 2>/dev/null)" || _exiterr "This script requires ${binary}."
[[ -x "${bin_path}" ]] || _exiterr "${binary} found in PATH but it's not executable"
done
@@ -254,7 +278,10 @@ check_dependencies() {
store_configvars() {
__KEY_ALGO="${KEY_ALGO}"
__OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}"
__OCSP_FETCH="${OCSP_FETCH}"
__OCSP_DAYS="${OCSP_DAYS}"
__PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}"
__PRIVATE_KEY_ROLLOVER="${PRIVATE_KEY_ROLLOVER}"
__KEYSIZE="${KEYSIZE}"
__CHALLENGETYPE="${CHALLENGETYPE}"
__HOOK="${HOOK}"
@@ -269,7 +296,10 @@ store_configvars() {
reset_configvars() {
KEY_ALGO="${__KEY_ALGO}"
OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}"
OCSP_FETCH="${__OCSP_FETCH}"
OCSP_DAYS="${__OCSP_DAYS}"
PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}"
PRIVATE_KEY_ROLLOVER="${__PRIVATE_KEY_ROLLOVER}"
KEYSIZE="${__KEYSIZE}"
CHALLENGETYPE="${__CHALLENGETYPE}"
HOOK="${__HOOK}"
@@ -298,7 +328,7 @@ verify_config() {
if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
fi
[[ "${KEY_ALGO}" == "rsa" || "${KEY_ALGO}" == "prime256v1" || "${KEY_ALGO}" == "secp384r1" ]] || _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
[[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue."
fi
@@ -332,6 +362,8 @@ load_config() {
CERTDIR=
ALPNCERTDIR=
ACCOUNTDIR=
ACCOUNT_KEYSIZE="4096"
ACCOUNT_KEY_ALGO=rsa
CHALLENGETYPE="http-01"
CONFIG_D=
CURL_OPTS=
@@ -358,6 +390,7 @@ load_config() {
AUTO_CLEANUP="no"
DEHYDRATED_USER=
DEHYDRATED_GROUP=
DEHYDRATED_SUDO_ENV="no"
API="auto"
if [[ -z "${CONFIG:-}" ]]; then
@@ -379,7 +412,7 @@ load_config() {
fi
# Allow globbing
[[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f
noglob_set
for check_config_d in "${CONFIG_D}"/*.sh; do
if [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then
@@ -392,7 +425,7 @@ load_config() {
done
# Disable globbing
[[ -n "${ZSH_VERSION:-}" ]] && set -o noglob || set -f
noglob_clear
fi
# Check for missing dependencies
@@ -410,7 +443,11 @@ load_config() {
if [[ -z "${DEHYDRATED_GROUP}" ]]; then
if [[ "${EUID}" != "${TARGET_UID}" ]]; then
echo "# INFO: Running $0 as ${DEHYDRATED_USER}"
has_sudo && exec sudo -u "${DEHYDRATED_USER}" "${0}" "${ORIGARGS[@]}"
if [ "${DEHYDRATED_SUDO_ENV}" = "yes" ]; then
has_sudo && exec sudo -E -H -u "${DEHYDRATED_USER}" "${0}" "${ORIGARGS[@]}"
else
has_sudo && exec sudo -u "${DEHYDRATED_USER}" "${0}" "${ORIGARGS[@]}"
fi
fi
else
TARGET_GID="$(getent group "${DEHYDRATED_GROUP}" | cut -d':' -f3)" || _exiterr "DEHYDRATED_GROUP ${DEHYDRATED_GROUP} is invalid"
@@ -420,7 +457,11 @@ load_config() {
fi
if [[ "${EUID}" != "${TARGET_UID}" ]] || [[ "${EGID}" != "${TARGET_GID}" ]]; then
echo "# INFO: Running $0 as ${DEHYDRATED_USER}/${DEHYDRATED_GROUP}"
has_sudo && exec sudo -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" "${ORIGARGS[@]}"
if [ "${DEHYDRATED_SUDO_ENV}" = "yes" ]; then
has_sudo && exec sudo -E -H -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" "${ORIGARGS[@]}"
else
has_sudo && exec sudo -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" "${ORIGARGS[@]}"
fi
fi
fi
elif [[ -n "${DEHYDRATED_GROUP}" ]]; then
@@ -473,6 +514,7 @@ load_config() {
fi
fi
# shellcheck disable=SC1090
[[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json"
@@ -512,6 +554,10 @@ load_config() {
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
[[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}"
if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ] && [ "${PARAM_FORCE:-no}" = "no" ]; then
_exiterr "Argument --force-validation can only be used in combination with --force (-x)"
fi
if [ ! "${1:-}" = "noverify" ]; then
verify_config
fi
@@ -539,8 +585,8 @@ init_system() {
grep -q newOrder <<< "${CA_DIRECTORY}" && API=2 || API=1
fi
if [[ ${API} -eq 1 ]]; then
# shellcheck disable=SC2015
# shellcheck disable=SC2015
if [[ "${API}" = "1" ]]; then
CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" &&
CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" &&
CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" &&
@@ -551,7 +597,6 @@ init_system() {
# Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part
CA_REG=${CA_NEW_REG/new-reg/reg}
else
# shellcheck disable=SC2015
CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" &&
CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" &&
CA_NEW_ACCOUNT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newAccount)" &&
@@ -559,8 +604,6 @@ init_system() {
CA_REQUIRES_EAB="$(printf "%s" "${CA_DIRECTORY}" | get_json_bool_value -p '"meta","externalAccountRequired"' || echo false)" &&
CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revokeCert)" ||
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
# Since acct URI is missing from directory we will assume it is the same as CA_NEW_ACCOUNT without the new part
CA_ACCOUNT=${CA_NEW_ACCOUNT/new-acct/acct}
fi
# Export some environment variables to be used in hook script
@@ -582,26 +625,63 @@ init_system() {
if [[ ! "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then
printf '\n' >&2
printf 'To use dehydrated with this certificate authority you have to agree to their terms of service which you can find here: %s\n\n' "${CA_TERMS}" >&2
printf 'To accept these terms of service run `%s --register --accept-terms`.\n' "${0}" >&2
printf 'To accept these terms of service run "%s --register --accept-terms".\n' "${0}" >&2
exit 1
fi
echo "+ Generating account key..."
generated="true"
local tmp_account_key="$(_mktemp)"
_openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}"
local tmp_account_key
tmp_account_key="$(_mktemp)"
if [[ ${API} -eq 1 && ! "${ACCOUNT_KEY_ALGO}" = "rsa" ]]; then
_exiterr "ACME API version 1 does not support EC account keys"
fi
case "${ACCOUNT_KEY_ALGO}" in
rsa) _openssl genrsa -out "${tmp_account_key}" "${ACCOUNT_KEYSIZE}";;
prime256v1|secp384r1|secp521r1) _openssl ecparam -genkey -name "${ACCOUNT_KEY_ALGO}" -out "${tmp_account_key}" -noout;;
esac
cat "${tmp_account_key}" > "${ACCOUNT_KEY}"
rm "${tmp_account_key}"
register_new_key="yes"
fi
fi
"${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null || _exiterr "Account key is not valid, cannot continue."
# Get public components from private key and calculate thumbprint
pubExponent64="$(printf '%x' "$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)"
pubMod64="$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)"
if ("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null); then
# Get public components from private key and calculate thumbprint
pubExponent64="$(printf '%x' "$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)"
pubMod64="$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)"
thumbprint="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
account_key_info="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}")"
account_key_sigalgo=RS256
elif ("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null); then
curve="$("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -noout -text 2>/dev/null | grep 'NIST CURVE' | cut -d':' -f2 | tr -d ' ')"
pubkey="$("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -noout -text 2>/dev/null | tr -d '\n ' | grep -Eo 'pub:.*ASN1' | _sed -e 's/^pub://' -e 's/ASN1$//' | tr -d ':')"
if [ "${curve}" = "P-256" ]; then
account_key_sigalgo="ES256"
elif [ "${curve}" = "P-384" ]; then
account_key_sigalgo="ES384"
elif [ "${curve}" = "P-521" ]; then
account_key_sigalgo="ES512"
else
_exiterr "Unknown account key curve: ${curve}"
fi
ec_x_offset=2
ec_x_len=$((${#pubkey}/2 - 1))
ec_x="${pubkey:$ec_x_offset:$ec_x_len}"
ec_x64="$(printf "%s" "${ec_x}" | hex2bin | urlbase64)"
ec_y_offset=$((ec_x_offset+ec_x_len))
ec_y_len=$((${#pubkey}-ec_y_offset))
ec_y="${pubkey:$ec_y_offset:$ec_y_len}"
ec_y64="$(printf "%s" "${ec_y}" | hex2bin | urlbase64)"
account_key_info="$(printf '{"crv":"%s","kty":"EC","x":"%s","y":"%s"}' "${curve}" "${ec_x64}" "${ec_y64}")"
else
_exiterr "Account key is not valid, cannot continue."
fi
thumbprint="$(printf '%s' "${account_key_info}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
# If we generated a new private key in the step above we have to register it with the acme-server
if [[ "${register_new_key}" = "yes" ]]; then
@@ -654,7 +734,7 @@ init_system() {
if [[ -n "${EAB_KID:-}" ]] && [[ -n "${EAB_HMAC_KEY:-}" ]]; then
eab_url="${CA_NEW_ACCOUNT}"
eab_protected64="$(printf '{"alg":"HS256","kid":"%s","url":"%s"}' "${EAB_KID}" "${eab_url}" | urlbase64)"
eab_payload64="$(printf "%s" '{"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}' | urlbase64)"
eab_payload64="$(printf "%s" "${account_key_info}" | urlbase64)"
eab_key="$(printf "%s" "${EAB_HMAC_KEY}" | deurlbase64 | bin2hex)"
eab_signed64="$(printf '%s' "${eab_protected64}.${eab_payload64}" | "${OPENSSL}" dgst -binary -sha256 -mac HMAC -macopt "hexkey:${eab_key}" | urlbase64)"
@@ -692,16 +772,16 @@ init_system() {
# Read account information or request from CA if missing
if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then
if [[ ${API} -eq 1 ]]; then
ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | jsonsh | get_json_int_value id)"
ACCOUNT_ID="$(jsonsh < "${ACCOUNT_KEY_JSON}" | get_json_int_value id)"
ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}"
else
if [[ -e "${ACCOUNT_ID_JSON}" ]]; then
ACCOUNT_URL="$(cat "${ACCOUNT_ID_JSON}" | jsonsh | get_json_string_value url)"
ACCOUNT_URL="$(jsonsh < "${ACCOUNT_ID_JSON}" | get_json_string_value url)"
fi
# if account URL is not storred, fetch it from the CA
if [[ -z "${ACCOUNT_URL:-}" ]]; then
echo "+ Fetching account URL..."
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
if [[ -z "${ACCOUNT_URL}" ]]; then
_exiterr "Unknown error on fetching account information"
fi
@@ -713,7 +793,7 @@ init_system() {
if [[ ${API} -eq 1 ]]; then
_exiterr "This is not implemented for ACMEv1! Consider switching to ACMEv2 :)"
else
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{}')"
fi
echo "${ACCOUNT_INFO}" > "${ACCOUNT_KEY_JSON}"
@@ -734,7 +814,7 @@ _exiterr() {
if [ -n "${1:-}" ]; then
echo "ERROR: ${1}" >&2
fi
[[ "${skip_exit_hook:-no}" = "no" ]] && [[ -n "${HOOK:-}" ]] && ("${HOOK}" "exit_hook" "${1}" || echo 'exit_hook returned with non-zero exit code!' >&2)
[[ "${skip_exit_hook:-no}" = "no" ]] && [[ -n "${HOOK:-}" ]] && ("${HOOK}" "exit_hook" "${1:-}" || echo 'exit_hook returned with non-zero exit code!' >&2)
exit 1
}
@@ -762,12 +842,13 @@ deurlbase64() {
# Convert hex string to binary data
hex2bin() {
# Remove spaces, add leading zero, escape as hex string and parse with printf
printf -- "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
# shellcheck disable=SC2059
printf "%b" "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
}
# Convert binary data to hex string
bin2hex() {
hexdump -e '16/1 "%02x"'
hexdump -v -e '/1 "%02x"'
}
# OpenSSL writes to stderr/stdout even when there are no errors. So just
@@ -797,6 +878,7 @@ http_request() {
fi
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)"
curlret="${?}"
@@ -826,6 +908,10 @@ http_request() {
elif [[ -n "${CA_REVOKE_CERT:-}" ]] && [[ "${2}" = "${CA_REVOKE_CERT:-}" ]] && [[ "${statuscode}" = "409" ]]; then
grep -q "Certificate already revoked" "${tempcont}" && return
else
if grep -q "urn:ietf:params:acme:error:badNonce" "${tempcont}"; then
printf "badnonce %s" "$(grep -Eoi "^replay-nonce:.*$" "${tempheaders}" | sed 's/ //' | cut -d: -f2)"
return 0
fi
echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
echo >&2
echo "Details:" >&2
@@ -836,8 +922,8 @@ http_request() {
# An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
if [[ -n "${HOOK}" ]]; then
errtxt="$(cat ${tempcont})"
errheaders="$(cat ${tempheaders})"
errtxt="$(cat "${tempcont}")"
errheaders="$(cat "${tempheaders}")"
"${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" || _exiterr 'request_failure hook returned with non-zero exit code'
fi
@@ -863,16 +949,17 @@ signed_request() {
# Encode payload as urlbase64
payload64="$(printf '%s' "${2}" | urlbase64)"
# Retrieve nonce from acme-server
if [[ ${API} -eq 1 ]]; then
nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
if [ -n "${3:-}" ]; then
nonce="$(printf "%s" "${3}" | tr -d ' \t\n\r')"
else
nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
# Retrieve nonce from acme-server
if [[ ${API} -eq 1 ]]; then
nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | cut -d':' -f2- | tr -d ' \t\n\r')"
else
nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | cut -d':' -f2- | tr -d ' \t\n\r')"
fi
fi
# Build header with just our public key and algorithm information
header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}'
if [[ ${API} -eq 1 ]]; then
# Build another header which also contains the previously received nonce and encode it as urlbase64
protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}'
@@ -880,17 +967,37 @@ signed_request() {
else
# Build another header which also contains the previously received nonce and url and encode it as urlbase64
if [[ -n "${ACCOUNT_URL:-}" ]]; then
protected='{"alg": "RS256", "kid": "'"${ACCOUNT_URL}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
protected='{"alg": "'"${account_key_sigalgo}"'", "kid": "'"${ACCOUNT_URL}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
else
protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
protected='{"alg": "'"${account_key_sigalgo}"'", "jwk": '"${account_key_info}"', "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
fi
protected64="$(printf '%s' "${protected}" | urlbase64)"
fi
# Sign header with nonce and our payload with our private key and encode signature as urlbase64
signed64="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)"
if [[ "${account_key_sigalgo}" = "RS256" ]]; then
signed64="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)"
else
dgstparams="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha${account_key_sigalgo:2} -sign "${ACCOUNT_KEY}" | "${OPENSSL}" asn1parse -inform DER)"
dgst_parm_1="$(echo "$dgstparams" | head -n 2 | tail -n 1 | cut -d':' -f4)"
dgst_parm_2="$(echo "$dgstparams" | head -n 3 | tail -n 1 | cut -d':' -f4)"
# zero-padding (doesn't seem to be necessary, but other clients are doing this as well...
case "${account_key_sigalgo}" in
"ES256") siglen=64;;
"ES384") siglen=96;;
"ES512") siglen=132;;
esac
while [[ ${#dgst_parm_1} -lt $siglen ]]; do dgst_parm_1="0${dgst_parm_1}"; done
while [[ ${#dgst_parm_2} -lt $siglen ]]; do dgst_parm_2="0${dgst_parm_2}"; done
signed64="$(printf "%s%s" "${dgst_parm_1}" "${dgst_parm_2}" | hex2bin | urlbase64)"
fi
if [[ ${API} -eq 1 ]]; then
# Build header with just our public key and algorithm information
header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}'
# Send header + extended header + payload + signature to the acme-server
data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
else
@@ -898,7 +1005,14 @@ signed_request() {
data='{"protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
fi
http_request post "${1}" "${data}"
output="$(http_request post "${1}" "${data}")"
if grep -qE "^badnonce " <<< "${output}"; then
echo " ! Request failed (badNonce), retrying request..." >&2
signed_request "${1:-}" "${2:-}" "$(printf "%s" "${output}" | cut -d' ' -f2)"
else
printf "%s" "${output}"
fi
}
# Extracts all subject names from a CSR
@@ -906,7 +1020,7 @@ signed_request() {
extract_altnames() {
csr="${1}" # the CSR itself (not a file)
if ! <<<"${csr}" "${OPENSSL}" req -verify -noout 2>/dev/null; then
if ! <<<"${csr}" "${OPENSSL}" req -verify -noout >/dev/null 2>&1; then
_exiterr "Certificate signing request isn't valid"
fi
@@ -917,23 +1031,23 @@ extract_altnames() {
# split to one per line:
# shellcheck disable=SC1003
altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )"
# we can only get DNS: ones signed
if grep -qEv '^(DNS|othername):' <<<"${altnames}"; then
_exiterr "Certificate signing request contains non-DNS Subject Alternative Names"
# we can only get DNS/IP: ones signed
if grep -qEv '^(DNS|IP( Address)*|othername):' <<<"${altnames}"; then
_exiterr "Certificate signing request contains non-DNS/IP Subject Alternative Names"
fi
# strip away the DNS: prefix
altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|othername:<unsupported>)//' )"
# strip away the DNS/IP: prefix
altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|IP( Address)*:|othername:<unsupported>)//' )"
printf "%s" "${altnames}" | tr '\n' ' '
else
# No SANs, extract CN
altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.* CN ?= ?([^ /,]*).*/\1/' )"
altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.*[ /]CN ?= ?([^ /,]*).*/\1/' )"
printf "%s" "${altnames}"
fi
}
# Get last issuer CN in certificate chain
get_last_cn() {
<<<"${1}" _sed 'H;/-----BEGIN CERTIFICATE-----/h;$!d;x' | "${OPENSSL}" x509 -noout -issuer | head -n1 | _sed -e 's/.* CN ?= ?([^/,]*).*/\1/'
<<<"${1}" _sed 'H;/-----BEGIN CERTIFICATE-----/h;$!d;x' | "${OPENSSL}" x509 -noout -issuer | head -n1 | _sed -e 's/.*[ /]CN ?= ?([^/,]*).*/\1/'
}
# Create certificate for domain(s) and outputs it FD 3
@@ -968,12 +1082,16 @@ sign_csr() {
# Request new order and store authorization URIs
local challenge_identifiers=""
for altname in ${altnames}; do
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
if [[ "${altname}" =~ ^ip: ]]; then
challenge_identifiers+="$(printf '{"type": "ip", "value": "%s"}, ' "${altname:3}")"
else
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
fi
done
challenge_identifiers="[${challenge_identifiers%, }]"
echo " + Requesting new certificate order from CA..."
order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
result="$(signed_request "${order_location}" "" | jsonsh)"
order_authorizations="$(echo "${result}" | get_json_array_values authorizations)"
@@ -1001,6 +1119,7 @@ sign_csr() {
# Receive authorization ($authorization is authz uri)
response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | jsonsh)"
identifier="$(echo "${response}" | get_json_string_value -p '"identifier","value"')"
identifier_type="$(echo "${response}" | get_json_string_value -p '"identifier","type"')"
echo " + Handling authorization for ${identifier}"
else
# Request new authorization ($authorization is altname)
@@ -1010,9 +1129,13 @@ sign_csr() {
fi
# Check if authorization has already been validated
if [ "$(echo "${response}" | _sed 's/"challenges": \[\{.*\}\]//' | get_json_string_value status)" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then
echo " + Found valid authorization for ${identifier}"
continue
if [ "$(echo "${response}" | get_json_string_value status)" = "valid" ]; then
if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ]; then
echo " + A valid authorization has been found but will be ignored"
else
echo " + Found valid authorization for ${identifier}"
continue
fi
fi
# Find challenge in authorization
@@ -1025,7 +1148,11 @@ sign_csr() {
challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")"
# Gather challenge information
challenge_names[${idx}]="${identifier}"
if [ "${identifier_type:-}" = "ip" ] && [ "${CHALLENGETYPE}" = "tls-alpn-01" ] ; then
challenge_names[${idx}]="$(echo "${identifier}" | ip_to_ptr)"
else
challenge_names[${idx}]="${identifier}"
fi
challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)"
if [[ ${API} -eq 2 ]]; then
@@ -1052,13 +1179,17 @@ sign_csr() {
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
;;
"tls-alpn-01")
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $2}')"
generate_alpn_certificate "${identifier}" "${keyauth_hook}"
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')"
generate_alpn_certificate "${identifier}" "${identifier_type}" "${keyauth_hook}"
;;
esac
keyauths[${idx}]="${keyauth}"
deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}"
if [ "${identifier_type:-}" = "ip" ] && [ "${CHALLENGETYPE}" = "tls-alpn-01" ]; then
deploy_args[${idx}]="$(echo "${identifier}" | ip_to_ptr) ${challenge_tokens[${idx}]} ${keyauth_hook}"
else
deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}"
fi
idx=$((idx+1))
done
@@ -1069,11 +1200,13 @@ sign_csr() {
if [[ ${num_pending_challenges} -ne 0 ]]; then
echo " + Deploying challenge tokens..."
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then
# shellcheck disable=SC2068
"${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
elif [[ -n "${HOOK}" ]]; then
# Run hook script to deploy the challenge token
local idx=0
while [ ${idx} -lt ${num_pending_challenges} ]; do
# shellcheck disable=SC2086
"${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
idx=$((idx+1))
done
@@ -1120,6 +1253,7 @@ sign_csr() {
echo " + Cleaning challenge tokens..."
# Clean challenge tokens using chained hook
# shellcheck disable=SC2068
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[@]} || _exiterr 'clean_challenge hook returned with non-zero exit code')
# Clean remaining challenge tokens if validation has failed
@@ -1130,6 +1264,7 @@ sign_csr() {
# Delete alpn verification certificates
[[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem"
# Clean challenge token using non-chained hook
# shellcheck disable=SC2086
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code')
idx=$((idx+1))
done
@@ -1177,8 +1312,8 @@ sign_csr() {
if [ "${altcn}" = "${PREFERRED_CHAIN}" ]; then
foundaltchain=1
fi
if [ "${foundaltchain}" = "0" ]; then
while read altcrturl; do
if [ "${foundaltchain}" = "0" ] && (grep -Ei '^link:' "${resheaders}" | grep -q -Ei 'rel="alternate"'); then
while read -r altcrturl; do
if [ "${foundaltchain}" = "0" ]; then
altcrt="$(signed_request "${altcrturl}" "")"
altcn="$(get_last_cn "${altcrt}")"
@@ -1266,7 +1401,8 @@ walk_chain() {
# Generate ALPN verification certificate
generate_alpn_certificate() {
local altname="${1}"
local acmevalidation="${2}"
local identifier_type="${2}"
local acmevalidation="${3}"
local alpncertdir="${ALPNCERTDIR}"
if [[ ! -e "${alpncertdir}" ]]; then
@@ -1277,10 +1413,17 @@ generate_alpn_certificate() {
echo " + Generating ALPN certificate and key for ${1}..."
tmp_openssl_cnf="$(_mktemp)"
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
printf "[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
printf "1.3.6.1.5.5.7.1.31=critical,DER:04:20:${acmevalidation}\n" >> "${tmp_openssl_cnf}"
if [[ "${identifier_type}" = "ip" ]]; then
printf "\n[SAN]\nsubjectAltName=IP:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
else
printf "\n[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
fi
printf "1.3.6.1.5.5.7.1.31=critical,DER:04:20:%s\n" "${acmevalidation}" >> "${tmp_openssl_cnf}"
SUBJ="/CN=${altname}/"
[[ "${OSTYPE:0:5}" = "MINGW" ]] && SUBJ="/${SUBJ}"
if [[ "${identifier_type}" = "ip" ]]; then
altname="$(echo "${altname}" | ip_to_ptr)"
fi
_openssl req -x509 -new -sha256 -nodes -newkey rsa:2048 -keyout "${alpncertdir}/${altname}.key.pem" -out "${alpncertdir}/${altname}.crt.pem" -subj "${SUBJ}" -extensions SAN -config "${tmp_openssl_cnf}"
chmod g+r "${alpncertdir}/${altname}.key.pem" "${alpncertdir}/${altname}.crt.pem"
rm -f "${tmp_openssl_cnf}"
@@ -1312,10 +1455,11 @@ sign_domain() {
if [[ ! -r "${certdir}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
echo " + Generating private key..."
privkey="privkey-${timestamp}.pem"
local tmp_privkey="$(_mktemp)"
local tmp_privkey
tmp_privkey="$(_mktemp)"
case "${KEY_ALGO}" in
rsa) _openssl genrsa -out "${tmp_privkey}" "${KEYSIZE}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${tmp_privkey}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${tmp_privkey}" -noout;;
esac
cat "${tmp_privkey}" > "${certdir}/privkey-${timestamp}.pem"
rm "${tmp_privkey}"
@@ -1332,7 +1476,7 @@ sign_domain() {
echo " + Generating private rollover key..."
case "${KEY_ALGO}" in
rsa) _openssl genrsa -out "${certdir}/privkey.roll.pem" "${KEYSIZE}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${certdir}/privkey.roll.pem";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${certdir}/privkey.roll.pem" -noout;;
esac
fi
# delete rolloverkeys if disabled
@@ -1345,17 +1489,25 @@ sign_domain() {
echo " + Generating signing request..."
SAN=""
for altname in ${altnames}; do
SAN="${SAN}DNS:${altname}, "
if [[ "${altname}" =~ ^ip: ]]; then
SAN="${SAN}IP:${altname:3}, "
else
SAN="${SAN}DNS:${altname}, "
fi
done
if [[ "${domain}" =~ ^ip: ]]; then
SUBJ="/CN=${domain:3}/"
else
SUBJ="/CN=${domain}/"
fi
SAN="${SAN%%, }"
local tmp_openssl_cnf
tmp_openssl_cnf="$(_mktemp)"
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}"
printf "\n[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}"
if [ "${OCSP_MUST_STAPLE}" = "yes" ]; then
printf "\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >> "${tmp_openssl_cnf}"
fi
SUBJ="/CN=${domain}/"
if [[ "${OSTYPE:0:5}" = "MINGW" ]]; then
# The subject starts with a /, so MSYS will assume it's a path and convert
# it unless we escape it with another one:
@@ -1426,20 +1578,21 @@ command_version() {
revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")"
echo "GIT-Revision: ${revision}"
echo ""
if [[ "${OSTYPE}" =~ "BSD" ]]; then
# shellcheck disable=SC1091
if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; then
echo "OS: $(uname -sr)"
elif [[ -e /etc/os-release ]]; then
( . /etc/os-release && echo "OS: $PRETTY_NAME" )
elif [[ -e /usr/lib/os-release ]]; then
( . /usr/lib/os-release && echo "OS: $PRETTY_NAME" )
else
echo "OS: $(cat /etc/issue | grep -v ^$ | head -n1 | _sed 's/\\(r|n|l) .*//g')"
echo "OS: $(grep -v '^$' /etc/issue | head -n1 | _sed 's/\\(r|n|l) .*//g')"
fi
echo "Used software:"
[[ -n "${BASH_VERSION:-}" ]] && echo " bash: ${BASH_VERSION}"
[[ -n "${ZSH_VERSION:-}" ]] && echo " zsh: ${ZSH_VERSION}"
echo " curl: ${CURL_VERSION}"
if [[ "${OSTYPE}" =~ "BSD" ]]; then
if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; then
echo " awk, sed, mktemp, grep, diff: BSD base system versions"
else
echo " awk: $(awk -W version 2>&1 | head -n1)"
@@ -1518,6 +1671,20 @@ command_account() {
exit 0
}
# Parse contents of domains.txt and domains.txt.d
parse_domains_txt() {
# Allow globbing temporarily
noglob_set
local inputs=("${DOMAINS_TXT}" "${DOMAINS_TXT}.d"/*.txt)
noglob_clear
cat "${inputs[@]}" |
tr -d '\r' |
awk '{print tolower($0)}' |
_sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' |
(grep -vE '^(#|$)' || true)
}
# Usage: --cron (-c)
# Description: Sign/renew non-existent/changed/expiring certificates.
command_sign_domains() {
@@ -1535,9 +1702,9 @@ command_sign_domains() {
if [[ -n "${PARAM_DOMAIN:-}" ]]; then
DOMAINS_TXT="$(_mktemp)"
if [[ -n "${PARAM_ALIAS:-}" ]]; then
printf -- "${PARAM_DOMAIN} > ${PARAM_ALIAS}" > "${DOMAINS_TXT}"
printf "%s > %s" "${PARAM_DOMAIN}" "${PARAM_ALIAS}" > "${DOMAINS_TXT}"
else
printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
printf "%s" "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
fi
elif [[ -e "${DOMAINS_TXT}" ]]; then
if [[ ! -r "${DOMAINS_TXT}" ]]; then
@@ -1550,17 +1717,17 @@ command_sign_domains() {
# Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire
ORIGIFS="${IFS}"
IFS=$'\n'
for line in $(<"${DOMAINS_TXT}" tr -d '\r' | awk '{print tolower($0)}' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' | (grep -vE '^(#|$)' || true)); do
for line in $(parse_domains_txt); do
reset_configvars
IFS="${ORIGIFS}"
alias="$(grep -Eo '>[^ ]+' <<< "${line}" || true)"
line="$(_sed -e 's/>[^ ]+[ ]*//g' <<< "${line}")"
aliascount="$(grep -Eo '>' <<< "${alias}" | awk 'END {print NR}' || true )"
[ ${aliascount} -gt 1 ] && _exiterr "Only one alias per line is allowed in domains.txt!"
[ "${aliascount}" -gt 1 ] && _exiterr "Only one alias per line is allowed in domains.txt!"
domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)"
[ ${aliascount} -lt 1 ] && alias="${domain}" || alias="${alias#>}"
[ "${aliascount}" -lt 1 ] && alias="${domain}" || alias="${alias#>}"
export alias
if [[ -z "${morenames}" ]];then
@@ -1614,6 +1781,8 @@ command_sign_domains() {
); do
config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
config_value="$(echo "${cfgline:1}" | cut -d'=' -f2- | tr -d "'")"
# 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)
echo " + ${config_var} = ${config_value}"
@@ -1646,12 +1815,12 @@ command_sign_domains() {
fi
# Check domain names of existing certificate
if [[ -e "${cert}" ]]; then
if [[ -e "${cert}" && "${force_renew}" = "no" ]]; then
printf " + Checking domain name(s) of existing cert..."
certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep DNS: | _sed 's/DNS://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')"
givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _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/ $//')"
givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //')"
if [[ "${certnames}" = "${givennames}" ]]; then
echo " unchanged."
else
@@ -1692,13 +1861,14 @@ command_sign_domains() {
if [[ ! "${skip}" = "yes" ]]; then
update_ocsp="yes"
[[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr"
# shellcheck disable=SC2086
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
skip_exit_hook=yes
sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} &
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} &
wait $! || exit_with_errorcode=1
skip_exit_hook=no
else
sign_domain "${certdir}" ${timestamp} ${domain} ${morenames}
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames}
fi
fi
@@ -1744,12 +1914,12 @@ command_sign_domains() {
# Usage: --signcsr (-s) path/to/csr.pem
# Description: Sign a given CSR, output CRT on stdout (advanced usage)
command_sign_csr() {
init_system
# redirect stdout to stderr
# leave stdout over at fd 3 to output the cert
exec 3>&1 1>&2
init_system
# load csr
csrfile="${1}"
if [ ! -r "${csrfile}" ]; then
@@ -1762,6 +1932,7 @@ command_sign_csr() {
# gen cert
certfile="$(_mktemp)"
# shellcheck disable=SC2086
sign_csr "${csr}" ${altnames} 3> "${certfile}"
# print cert
@@ -1866,7 +2037,7 @@ command_cleanup() {
fi
# Allow globbing
[[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f
noglob_set
# Loop over all certificate directories
for certdir in "${CERTDIR}/"*; do
@@ -1907,7 +2078,6 @@ command_cleanup() {
# Check if current file is in use, if unused move to archive directory
filename="$(basename "${file}")"
if [[ ! "${filename}" = "${current}" ]] && [[ -f "${certdir}/${filename}" ]]; then
echo "${filename}"
if [[ "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then
echo "Deleting unused file: ${certname}/${filename}"
rm "${certdir}/${filename}"
@@ -1980,8 +2150,7 @@ main() {
fi
}
# shellcheck disable=SC2199
[[ -z "${@}" ]] && eval set -- "--help"
[[ -z "${*}" ]] && eval set -- "--help"
while (( ${#} )); do
case "${1}" in
@@ -2102,11 +2271,17 @@ main() {
;;
# PARAM_Usage: --force (-x)
# PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS
# PARAM_Description: Force certificate renewal even if it is not due to expire within RENEW_DAYS
--force|-x)
PARAM_FORCE="yes"
;;
# PARAM_Usage: --force-validation
# PARAM_Description: Force revalidation of domain names (used in combination with --force)
--force-validation)
PARAM_FORCE_VALIDATION="yes"
;;
# PARAM_Usage: --no-lock (-n)
# PARAM_Description: Don't use lockfile (potentially dangerous!)
--no-lock|-n)
@@ -2183,8 +2358,8 @@ main() {
PARAM_ALPNCERTDIR="${1}"
;;
# PARAM_Usage: --challenge (-t) http-01|dns-01
# PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
# PARAM_Usage: --challenge (-t) http-01|dns-01|tls-alpn-01
# PARAM_Description: Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
--challenge|-t)
shift 1
check_parameters "${1:-}"

View File

@@ -34,6 +34,30 @@ under your `CERTDIR`.
example.net www.example.net wiki.example.net > certalias
```
This allows to set per certificates options. The options you can change are
explained in [Per Certificate Config](per-certificate-config.md).
If you want to create different certificate types for the same domain
you can use:
```text
*.service.example.org service.example.org > star_service_example_org_rsa
*.service.example.org service.example.org > star_service_example_org_ecdsa
```
Then add a config file `certs/star_service_example_org_rsa/config` with
the value
```
KEY_ALGO="rsa"
```
or respectively
```
KEY_ALGO="ecdsa"
```
### Wildcards
Support for wildcards was added by the ACME v2 protocol.
@@ -70,3 +94,14 @@ This creates two certificates one for `service.example.com` with an
**Note:** The first certificate is valid for both `service.example.com` and for
`*.service.example.com` which can be a useful way to create wildcard
certificates.
### Drop-in directory
If a directory named `domains.txt.d` exists in the same location as
`domains.txt`, the contents of `*.txt` files in that directory are appended to
the list of domains, in alphabetical order of the filenames. This is useful for
automation, as it doesn't require editing an existing file to add new domains.
Warning: Behaviour of this might change as the naming between `domains.txt.d`
and the `DOMAINS_D` config variable (which is used for per-certificate
configuration) is a bit confusing.

View File

@@ -16,6 +16,9 @@
# Which group should dehydrated run as? This will be implicitly enforced when running as root
#DEHYDRATED_GROUP=
# Should dehydrated pass environment variables over sudo?
#DEHYDRATED_SUDO_ENV="no"
# Resolve names to addresses of IP version only. (curl)
# supported values: 4, 6
# default: <unset>

View File

@@ -24,6 +24,15 @@ example.net www.example.net > certalias
# NOTE: It is a certificate for 'service.example.org'
*.service.example.org service.example.org > star_service_example_org
# Optionally you can also append the certificate algorithm here to create
# multiple certificate types for the same domain.
#
# This allows to set per certificates options. How to do this is
# explained in [domains.txt documentation](domains_txt.md).
#
*.service.example.org service.example.org > star_service_example_org_rsa
*.service.example.org service.example.org > star_service_example_org_ecdsa
# Create a certificate for 'service.example.net' with an alternative name of
# '*.service.example.net' (which is a wildcard domain) and store it in the
# directory ${CERTDIR}/service.example.net

View File

@@ -1,199 +1,199 @@
#!/usr/bin/env bash
deploy_challenge() {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
# This hook is called once for every domain that needs to be
# validated, including any alternative names you may have listed.
#
# Parameters:
# - DOMAIN
# The domain name (CN or subject alternative name) being
# validated.
# - TOKEN_FILENAME
# The name of the file containing the token to be served for HTTP
# validation. Should be served by your web server as
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
# - TOKEN_VALUE
# The token value that needs to be served for validation. For DNS
# validation, this is what you want to put in the _acme-challenge
# TXT record. For HTTP validation it is the value that is expected
# be found in the $TOKEN_FILENAME file.
# This hook is called once for every domain that needs to be
# validated, including any alternative names you may have listed.
#
# Parameters:
# - DOMAIN
# The domain name (CN or subject alternative name) being
# validated.
# - TOKEN_FILENAME
# The name of the file containing the token to be served for HTTP
# validation. Should be served by your web server as
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
# - TOKEN_VALUE
# The token value that needs to be served for validation. For DNS
# validation, this is what you want to put in the _acme-challenge
# TXT record. For HTTP validation it is the value that is expected
# be found in the $TOKEN_FILENAME file.
# Simple example: Use nsupdate with local named
# printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
# Simple example: Use nsupdate with local named
# printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
}
clean_challenge() {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
# This hook is called after attempting to validate each domain,
# whether or not validation was successful. Here you can delete
# files or DNS records that are no longer needed.
#
# The parameters are the same as for deploy_challenge.
# This hook is called after attempting to validate each domain,
# whether or not validation was successful. Here you can delete
# files or DNS records that are no longer needed.
#
# The parameters are the same as for deploy_challenge.
# Simple example: Use nsupdate with local named
# printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
# Simple example: Use nsupdate with local named
# printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
}
sync_cert() {
local KEYFILE="${1}" CERTFILE="${2}" FULLCHAINFILE="${3}" CHAINFILE="${4}" REQUESTFILE="${5}"
local KEYFILE="${1}" CERTFILE="${2}" FULLCHAINFILE="${3}" CHAINFILE="${4}" REQUESTFILE="${5}"
# This hook is called after the certificates have been created but before
# they are symlinked. This allows you to sync the files to disk to prevent
# creating a symlink to empty files on unexpected system crashes.
#
# This hook is not intended to be used for further processing of certificate
# files, see deploy_cert for that.
#
# Parameters:
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
# - REQUESTFILE
# The path of the file containing the certificate signing request.
# This hook is called after the certificates have been created but before
# they are symlinked. This allows you to sync the files to disk to prevent
# creating a symlink to empty files on unexpected system crashes.
#
# This hook is not intended to be used for further processing of certificate
# files, see deploy_cert for that.
#
# Parameters:
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
# - REQUESTFILE
# The path of the file containing the certificate signing request.
# Simple example: sync the files before symlinking them
# sync "${KEYFILE}" "${CERTFILE}" "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}"
# Simple example: sync the files before symlinking them
# sync "${KEYFILE}" "${CERTFILE}" "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}"
}
deploy_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
# This hook is called once for each certificate that has been
# produced. Here you might, for instance, copy your new certificates
# to service-specific locations and reload the service.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
# - TIMESTAMP
# Timestamp when the specified certificate was created.
# This hook is called once for each certificate that has been
# produced. Here you might, for instance, copy your new certificates
# to service-specific locations and reload the service.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
# - TIMESTAMP
# Timestamp when the specified certificate was created.
# Simple example: Copy file to nginx config
# cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
# systemctl reload nginx
# Simple example: Copy file to nginx config
# cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
# systemctl reload nginx
}
deploy_ocsp() {
local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}"
local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}"
# This hook is called once for each updated ocsp stapling file that has
# been produced. Here you might, for instance, copy your new ocsp stapling
# files to service-specific locations and reload the service.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - OCSPFILE
# The path of the ocsp stapling file
# - TIMESTAMP
# Timestamp when the specified ocsp stapling file was created.
# This hook is called once for each updated ocsp stapling file that has
# been produced. Here you might, for instance, copy your new ocsp stapling
# files to service-specific locations and reload the service.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - OCSPFILE
# The path of the ocsp stapling file
# - TIMESTAMP
# Timestamp when the specified ocsp stapling file was created.
# Simple example: Copy file to nginx config
# cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
# systemctl reload nginx
# Simple example: Copy file to nginx config
# cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
# systemctl reload nginx
}
unchanged_cert() {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
# This hook is called once for each certificate that is still
# valid and therefore wasn't reissued.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
# This hook is called once for each certificate that is still
# valid and therefore wasn't reissued.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - FULLCHAINFILE
# The path of the file containing the full certificate chain.
# - CHAINFILE
# The path of the file containing the intermediate certificate(s).
}
invalid_challenge() {
local DOMAIN="${1}" RESPONSE="${2}"
local DOMAIN="${1}" RESPONSE="${2}"
# This hook is called if the challenge response has failed, so domain
# owners can be aware and act accordingly.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - RESPONSE
# The response that the verification server returned
# This hook is called if the challenge response has failed, so domain
# owners can be aware and act accordingly.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - RESPONSE
# The response that the verification server returned
# Simple example: Send mail to root
# printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root
# Simple example: Send mail to root
# printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root
}
request_failure() {
local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}"
local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}"
# This hook is called when an HTTP request fails (e.g., when the ACME
# server is busy, returns an error, etc). It will be called upon any
# response code that does not start with '2'. Useful to alert admins
# about problems with requests.
#
# Parameters:
# - STATUSCODE
# The HTML status code that originated the error.
# - REASON
# The specified reason for the error.
# - REQTYPE
# The kind of request that was made (GET, POST...)
# - HEADERS
# HTTP headers returned by the CA
# This hook is called when an HTTP request fails (e.g., when the ACME
# server is busy, returns an error, etc). It will be called upon any
# response code that does not start with '2'. Useful to alert admins
# about problems with requests.
#
# Parameters:
# - STATUSCODE
# The HTML status code that originated the error.
# - REASON
# The specified reason for the error.
# - REQTYPE
# The kind of request that was made (GET, POST...)
# - HEADERS
# HTTP headers returned by the CA
# Simple example: Send mail to root
# printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root
# Simple example: Send mail to root
# printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root
}
generate_csr() {
local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}"
local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}"
# This hook is called before any certificate signing operation takes place.
# It can be used to generate or fetch a certificate signing request with external
# tools.
# The output should be just the certificate signing request formatted as PEM.
#
# Parameters:
# - DOMAIN
# The primary domain as specified in domains.txt. This does not need to
# match with the domains in the CSR, it's basically just the directory name.
# - CERTDIR
# Certificate output directory for this particular certificate. Can be used
# for storing additional files.
# - ALTNAMES
# All domain names for the current certificate as specified in domains.txt.
# Again, this doesn't need to match with the CSR, it's just there for convenience.
# This hook is called before any certificate signing operation takes place.
# It can be used to generate or fetch a certificate signing request with external
# tools.
# The output should be just the certificate signing request formatted as PEM.
#
# Parameters:
# - DOMAIN
# The primary domain as specified in domains.txt. This does not need to
# match with the domains in the CSR, it's basically just the directory name.
# - CERTDIR
# Certificate output directory for this particular certificate. Can be used
# for storing additional files.
# - ALTNAMES
# All domain names for the current certificate as specified in domains.txt.
# Again, this doesn't need to match with the CSR, it's just there for convenience.
# Simple example: Look for pre-generated CSRs
# if [ -e "${CERTDIR}/pre-generated.csr" ]; then
# cat "${CERTDIR}/pre-generated.csr"
# fi
# Simple example: Look for pre-generated CSRs
# if [ -e "${CERTDIR}/pre-generated.csr" ]; then
# cat "${CERTDIR}/pre-generated.csr"
# fi
}
startup_hook() {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -20,8 +20,8 @@ Dehydrated will notify if no account is configured. Run with \fB--register
Next, all domain names must be provided in domains.txt. The format is line
based: If the file contains two lines "example.com" and "example.net",
Dehydrated will request two certificate, one for "example.com" and the other
for "example.net". A single line while "example.com example.net" will request a
dehydrated will request two certificate, one for "example.com" and the other
for "example.net". A single line containing "example.com example.net" will request a
single certificate valid for both "example.net" and "example.com" through the \fISubject
Alternative Name\fR (SAN) field.
@@ -106,7 +106,7 @@ Keep going after encountering an error while creating/renewing multiple
certificates in cron mode
.TP
.BR \-\-force ", " \-x
Force renew of certificate even if it is longer valid than value in RENEW_DAYS
Force certificate renewal even if it is not due to expire within RENEW_DAYS
.TP
.BR \-\-no\-lock ", " \-n
Don't use lockfile (potentially dangerous!)

View File

@@ -11,6 +11,8 @@ Currently supported options:
- KEY_ALGO
- KEYSIZE
- OCSP_MUST_STAPLE
- OCSP_FETCH
- OCSP_DAYS
- CHALLENGETYPE
- HOOK
- HOOK_CHAIN

View File

@@ -8,10 +8,7 @@ you will quickly hit these limits and find yourself locked out.
To avoid this, please set the CA property to the Lets Encrypt staging server URL in your config file:
```bash
CA="https://acme-staging.api.letsencrypt.org/directory"
CA="https://acme-staging-v02.api.letsencrypt.org/directory"
```
# ACMEv2 staging
You can use `CA="https://acme-staging-v02.api.letsencrypt.org/directory"` to test dehydrated with
the ACMEv2 staging endpoint.
Alternatively you can define the CA using the CLI argument `--ca letsencrypt-test` (`letsencrypt-test` is an integrated preset-CA corresponding to the URL above).

View File

@@ -6,6 +6,26 @@ It will do that for any (sub-)domain you want to sign a certificate for.
Dehydrated generates the required verification certificates, but the delivery is out of its scope.
### Example lighttpd config
lighttpd can be configured to recognize ALPN `acme-tls/1` and to respond to such
requests using the specially crafted TLS certificates generated by dehydrated.
Configure lighttpd and dehydrated to use the same path for these certificates.
(Be sure to allow read access to the user account under which the lighttpd
server is running.) `mkdir -p /etc/dehydrated/alpn-certs`
lighttpd.conf:
```
ssl.acme-tls-1 = "/etc/dehydrated/alpn-certs"
```
When renewing certificates, specify `-t tls-alpn-01` and `--alpn /etc/dehydrated/alpn-certs` to dehydrated, e.g.
```
dehydrated -t tls-alpn-01 --alpn /etc/dehydrated/alpn-certs -c --out /etc/lighttpd/certs -d www.example.com
# gracefully reload lighttpd to use the new certificates by sending lighttpd pid SIGUSR1
systemctl reload lighttpd
```
### Example nginx config
On an nginx tcp load-balancer you can use the `ssl_preread` module to map a different port for acme-tls