pax_global_header00006660000000000000000000000064132701706400014512gustar00rootroot0000000000000052 comment=ce3d6583779d9fad597012dd116ab2a8c000e9cb dehydrated-0.6.2/000077500000000000000000000000001327017064000136345ustar00rootroot00000000000000dehydrated-0.6.2/.gitignore000066400000000000000000000001421327017064000156210ustar00rootroot00000000000000private_key.pem private_key.json domains.txt config hook.sh certs/* archive/* accounts/* chains/* dehydrated-0.6.2/CHANGELOG000066400000000000000000000101161327017064000150450ustar00rootroot00000000000000# Change Log This file contains a log of major changes in dehydrated ## [0.6.2] - 2018-04-25 ## Added - New deploy_ocsp hook - Allow account registration with custom key ## Changed - Don't walk certificate chain for ACMEv2 (certificate contains chain by default) - Improved documentation on wildcards ## Fixes - Added workaround for compatibility with filesystem ACLs - Close unwanted external file-descriptors - Fixed JSON parsing on force-renewal - Fixed cleanup of challenge files/dns-entries on validation errors - A few more minor fixes ## [0.6.1] - 2018-03-13 ## Changed - Use new ACME v2 endpoint by default ## [0.6.0] - 2018-03-11 ## Changed - Challenge validation loop has been modified to loop over authorization identifiers instead of altnames (ACMEv2 + wildcard support) - Removed LICENSE parameter from config (terms of service is now acquired directly from the CA directory) ## Added - Support for ACME v02 (including wildcard certificates!) - New hook: generate_csr (see example hook script for more information) - Calling random hook on startup to make it clear to hook script authors that unknown hooks should just be ignored... ## [0.5.0] - 2018-01-13 ## Changed - Certificate chain is now cached (CHAINCACHE) - OpenSSL binary path is now configurable (OPENSSL) - Cleanup now also moves revoked certificates ## Added - New feature for updating contact information (--account) - Allow automatic cleanup on exit (AUTO_CLEANUP) - Initial support for fetching OCSP status to be used for OCSP stapling (OCSP_FETCH) - Certificates can now have aliases to create multiple certificates with identical set of domains (see --alias and domains.txt documentation) - Allow dehydrated to run as specified user (/group) ## [0.4.0] - 2017-02-05 ## Changed - dehydrated now asks you to read and accept the CAs terms of service before creating an account - Skip challenges for already validated domains - Removed need for some special commands (BusyBox compatibility) - Exported a few more variables for use in hook-scripts - fullchain.pem now actually contains the full chain instead of just the certificate with an intermediate cert ## Added - Added private-key rollover functionality - Added `--lock-suffix` option for allowing parallel execution - Added `invalid_challenge` hook - Added `request_failure` hook - Added `exit_hook` hook - Added standalone `register` command ## [0.3.1] - 2016-09-13 ## Changed - Renamed project to `dehydrated`. - Default WELLKNOWN location is now `/var/www/dehydrated` - Config location is renamed to `dehydrated` (e.g. `/etc/dehydrated`) ## [0.3.0] - 2016-09-07 ## Changed - Config is now named `config` instead of `config.sh`! - Location of domains.txt is now configurable via DOMAINS_TXT config variable - Location of certs directory is now configurable via CERTDIR config variable - signcsr command now also outputs chain certificate if --full-chain/-fc is set - Location of account-key(s) changed - Default WELLKNOWN location is now `/var/www/letsencrypt` - New version of Let's Encrypt Subscriber Agreement ## Added - Added option to add CSR-flag indicating OCSP stapling to be mandatory - Initial support for configuration on per-certificate base - Support for per-CA account keys and custom config for output cert directory, license, etc. - Added option to select IP version of name to address resolution - Added option to run letsencrypt.sh without locks ## Fixed - letsencrypt.sh no longer stores account keys from invalid registrations ## [0.2.0] - 2016-05-22 ### Changed - PRIVATE_KEY config parameter has been renamed to ACCOUNT_KEY to avoid confusion with certificate keys - deploy_cert hook now also has the certificates timestamp as standalone parameter - Temporary files are now identifiable (template: letsencrypt.sh-XXXXXX) - Private keys are now regenerated by default ### Added - Added documentation to repository ### Fixed - Fixed bug with uppercase names in domains.txt (script now converts everything to lowercase) - mktemp no longer uses the deprecated `-t` parameter. - Compatibility with "pretty" json ## [0.1.0] - 2016-03-25 ### Changed - This is the first numbered version of letsencrypt.sh dehydrated-0.6.2/LICENSE000066400000000000000000000020751327017064000146450ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2018 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 in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dehydrated-0.6.2/README.md000066400000000000000000000141431327017064000151160ustar00rootroot00000000000000# 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) ![](docs/logo.jpg) 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! It uses the `openssl` utility for everything related to actually handling keys and certificates, so you need to have that installed. Other dependencies are: cURL, sed, grep, awk, mktemp (all found pre-installed on almost any system, cURL being the only exception). Current features: - Signing of a list of domains (including wildcard domains!) - 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 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). ## Getting started For getting started I recommend taking a look at [docs/domains_txt.md](docs/domains_txt.md), [docs/wellknown.md](docs/wellknown.md) and the [Usage](#usage) section on this page (you'll probably only need the `-c` option). Generally you want to set up your WELLKNOWN path first, and then fill in domains.txt. **Please note that you should use the staging URL when experimenting with this script to not hit Let's Encrypt's rate limits.** See [docs/staging.md](docs/staging.md). If you have any problems take a look at our [Troubleshooting](docs/troubleshooting.md) guide. ## Config dehydrated is looking for a config file in a few different places, it will use the first one it can find in this order: - `/etc/dehydrated/config` - `/usr/local/etc/dehydrated/config` - The current working directory of your shell - The directory from which dehydrated was run Have a look at [docs/examples/config](docs/examples/config) to get started, copy it to e.g. `/etc/dehydrated/config` and edit it to fit your needs. ## Usage: ```text Usage: ./dehydrated [-h] [command [argument]] [parameter [argument]] [parameter [argument]] ... Default command: help Commands: --version (-v) Print version information --register Register account key --account Update account contact information --cron (-c) Sign/renew non-existent/changed/expiring certificates. --signcsr (-s) path/to/csr.pem Sign a given CSR, output CRT on stdout (advanced usage) --revoke (-r) path/to/cert.pem Revoke specified certificate --cleanup (-gc) Move unused certificate files to archive directory --help (-h) Show help text --env (-e) Output configuration variables for use in other scripts Parameters: --accept-terms Accept CAs terms of service --full-chain (-fc) Print full chain when using --signcsr --ipv4 (-4) Resolve names to IPv4 addresses only --ipv6 (-6) Resolve names to IPv6 addresses only --domain (-d) domain.tld Use specified domain name(s) instead of domains.txt entry (one certificate!) --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 --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 --privkey (-p) path/to/key.pem Use specified private key instead of account key (useful for revocation) --config (-f) path/to/config Use specified config file --hook (-k) path/to/hook.sh Use specified script for hooks --out (-o) certs/directory Output certificates into the specified directory --challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported --algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 ``` ## Donate 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 :) 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 dehydrated-0.6.2/dehydrated000077500000000000000000001757041327017064000157150ustar00rootroot00000000000000#!/usr/bin/env bash # dehydrated by lukas2511 # Source: https://dehydrated.io # # This script is licensed under The MIT License (see LICENSE for more information). set -e set -u set -o pipefail [[ -n "${ZSH_VERSION:-}" ]] && set -o SH_WORD_SPLIT && set +o FUNCTION_ARGZERO && set -o NULL_GLOB && set -o noglob [[ -z "${ZSH_VERSION:-}" ]] && shopt -s nullglob && set -f umask 077 # paranoid umask, we're creating private keys # Close weird external file descriptors exec 3>&- exec 4>&- VERSION="0.6.2" # Find directory in which this script is stored by traversing all symbolic links SOURCE="${0}" while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" SOURCE="$(readlink "$SOURCE")" [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located done SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" BASEDIR="${SCRIPTDIR}" ORIGARGS="$@" # Create (identifiable) temporary files _mktemp() { # shellcheck disable=SC2068 mktemp ${@:-} "${TMPDIR:-/tmp}/dehydrated-XXXXXX" } # Check for script dependencies check_dependencies() { # just execute some dummy and/or version commands to see if required tools exist and are actually usable "${OPENSSL}" version > /dev/null 2>&1 || _exiterr "This script requires an openssl binary." _sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requires sed with support for extended (modern) regular expressions." command -v grep > /dev/null 2>&1 || _exiterr "This script requires grep." command -v mktemp > /dev/null 2>&1 || _exiterr "This script requires mktemp." command -v diff > /dev/null 2>&1 || _exiterr "This script requires diff." # curl returns with an error code in some ancient versions so we have to catch that set +e CURL_VERSION="$(curl -V 2>&1 | head -n1 | awk '{print $2}')" retcode="$?" set -e if [[ ! "${retcode}" = "0" ]] && [[ ! "${retcode}" = "2" ]]; then _exiterr "This script requires curl." fi } store_configvars() { __KEY_ALGO="${KEY_ALGO}" __OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}" __PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}" __KEYSIZE="${KEYSIZE}" __CHALLENGETYPE="${CHALLENGETYPE}" __HOOK="${HOOK}" __WELLKNOWN="${WELLKNOWN}" __HOOK_CHAIN="${HOOK_CHAIN}" __OPENSSL_CNF="${OPENSSL_CNF}" __RENEW_DAYS="${RENEW_DAYS}" __IP_VERSION="${IP_VERSION}" } reset_configvars() { KEY_ALGO="${__KEY_ALGO}" OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}" PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}" KEYSIZE="${__KEYSIZE}" CHALLENGETYPE="${__CHALLENGETYPE}" HOOK="${__HOOK}" WELLKNOWN="${__WELLKNOWN}" HOOK_CHAIN="${__HOOK_CHAIN}" OPENSSL_CNF="${__OPENSSL_CNF}" RENEW_DAYS="${__RENEW_DAYS}" IP_VERSION="${__IP_VERSION}" } hookscript_bricker_hook() { # Hook scripts should ignore any hooks they don't know. # Calling a random hook to make this clear to the hook script authors... if [[ -n "${HOOK}" ]]; then "${HOOK}" "this_hookscript_is_broken__dehydrated_is_working_fine__please_ignore_unknown_hooks_in_your_script" fi } # verify configuration values verify_config() { [[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue." 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 _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." if [[ -n "${IP_VERSION}" ]]; then [[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue." fi [[ "${API}" == "auto" || "${API}" == "1" || "${API}" == "2" ]] || _exiterr "Unsupported API version defined in config: ${API}" } # Setup default config values, search for and load configuration files load_config() { # Check for config in various locations if [[ -z "${CONFIG:-}" ]]; then for check_config in "/etc/dehydrated" "/usr/local/etc/dehydrated" "${PWD}" "${SCRIPTDIR}"; do if [[ -f "${check_config}/config" ]]; then BASEDIR="${check_config}" CONFIG="${check_config}/config" break fi done fi # Default values CA="https://acme-v02.api.letsencrypt.org/directory" OLDCA= CERTDIR= ACCOUNTDIR= CHALLENGETYPE="http-01" CONFIG_D= CURL_OPTS= DOMAINS_D= DOMAINS_TXT= HOOK= HOOK_CHAIN="no" RENEW_DAYS="30" KEYSIZE="4096" WELLKNOWN= PRIVATE_KEY_RENEW="yes" PRIVATE_KEY_ROLLOVER="no" KEY_ALGO=rsa OPENSSL=openssl OPENSSL_CNF= CONTACT_EMAIL= LOCKFILE= OCSP_MUST_STAPLE="no" OCSP_FETCH="no" IP_VERSION= CHAINCACHE= AUTO_CLEANUP="no" DEHYDRATED_USER= DEHYDRATED_GROUP= API="auto" if [[ -z "${CONFIG:-}" ]]; then echo "#" >&2 echo "# !! WARNING !! No main config file found, using default config!" >&2 echo "#" >&2 elif [[ -f "${CONFIG}" ]]; then echo "# INFO: Using main config file ${CONFIG}" BASEDIR="$(dirname "${CONFIG}")" # shellcheck disable=SC1090 . "${CONFIG}" else _exiterr "Specified config file doesn't exist." fi if [[ -n "${CONFIG_D}" ]]; then if [[ ! -d "${CONFIG_D}" ]]; then _exiterr "The path ${CONFIG_D} specified for CONFIG_D does not point to a directory." fi # Allow globbing [[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f for check_config_d in "${CONFIG_D}"/*.sh; do if [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then echo "# INFO: Using additional config file ${check_config_d}" # shellcheck disable=SC1090 . "${check_config_d}" else _exiterr "Specified additional config ${check_config_d} is not readable or not a file at all." fi done # Disable globbing [[ -n "${ZSH_VERSION:-}" ]] && set -o noglob || set -f fi # Check if we are running & are allowed to run as root if [[ -n "$DEHYDRATED_USER" ]]; then command -v sudo > /dev/null 2>&1 || _exiterr "DEHYDRATED_USER set but sudo not available. Please install sudo." command -v getent > /dev/null 2>&1 || _exiterr "DEHYDRATED_USER set but getent not available. Please install getent." TARGET_UID="$(getent passwd "${DEHYDRATED_USER}" | cut -d':' -f3)" if [[ -z "${DEHYDRATED_GROUP}" ]]; then if [[ "${EUID}" != "${TARGET_UID}" ]]; then echo "# INFO: Running $0 as ${DEHYDRATED_USER}" exec sudo -u "${DEHYDRATED_USER}" "${0}" ${ORIGARGS} fi else TARGET_GID="$(getent group "${DEHYDRATED_GROUP}" | cut -d':' -f3)" if [[ -z "${EGID:-}" ]]; then command -v id > /dev/null 2>&1 || _exiterr "DEHYDRATED_GROUP set, don't know current gid and 'id' not available... Please provide 'id' binary." EGID="$(id -g)" fi if [[ "${EUID}" != "${TARGET_UID}" ]] || [[ "${EGID}" != "${TARGET_GID}" ]]; then echo "# INFO: Running $0 as ${DEHYDRATED_USER}/${DEHYDRATED_GROUP}" exec sudo -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" ${ORIGARGS} fi fi elif [[ -n "${DEHYDRATED_GROUP}" ]]; then _exiterr "DEHYDRATED_GROUP can only be used in combination with DEHYDRATED_USER." fi # Check for missing dependencies check_dependencies # Remove slash from end of BASEDIR. Mostly for cleaner outputs, doesn't change functionality. [[ "$BASEDIR" != "/" ]] && BASEDIR="${BASEDIR%%/}" # Check BASEDIR and set default variables [[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}" if [[ -z "${OLDCA}" ]] && [[ "${CA}" = "https://acme-v02.api.letsencrypt.org/directory" ]]; then OLDCA="https://acme-v01.api.letsencrypt.org/directory" fi # Create new account directory or symlink to account directory from old CA CAHASH="$(echo "${CA}" | urlbase64)" [[ -z "${ACCOUNTDIR}" ]] && ACCOUNTDIR="${BASEDIR}/accounts" if [[ ! -e "${ACCOUNTDIR}/${CAHASH}" ]]; then OLDCAHASH="$(echo "${OLDCA}" | urlbase64)" mkdir -p "${ACCOUNTDIR}" if [[ -n "${OLDCA}" ]] && [[ -e "${ACCOUNTDIR}/${OLDCAHASH}" ]]; then echo "! Reusing account from ${OLDCA}" ln -s "${OLDCAHASH}" "${ACCOUNTDIR}/${CAHASH}" else mkdir "${ACCOUNTDIR}/${CAHASH}" fi fi [[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config" ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem" ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json" if [[ -f "${BASEDIR}/private_key.pem" ]] && [[ ! -f "${ACCOUNT_KEY}" ]]; then echo "! Moving private_key.pem to ${ACCOUNT_KEY}" mv "${BASEDIR}/private_key.pem" "${ACCOUNT_KEY}" fi if [[ -f "${BASEDIR}/private_key.json" ]] && [[ ! -f "${ACCOUNT_KEY_JSON}" ]]; then echo "! Moving private_key.json to ${ACCOUNT_KEY_JSON}" mv "${BASEDIR}/private_key.json" "${ACCOUNT_KEY_JSON}" fi [[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs" [[ -z "${CHAINCACHE}" ]] && CHAINCACHE="${BASEDIR}/chains" [[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt" [[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/dehydrated" [[ -z "${LOCKFILE}" ]] && LOCKFILE="${BASEDIR}/lock" [[ -z "${OPENSSL_CNF}" ]] && OPENSSL_CNF="$("${OPENSSL}" version -d | cut -d\" -f2)/openssl.cnf" [[ -n "${PARAM_LOCKFILE_SUFFIX:-}" ]] && LOCKFILE="${LOCKFILE}-${PARAM_LOCKFILE_SUFFIX}" [[ -n "${PARAM_NO_LOCK:-}" ]] && LOCKFILE="" [[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}" [[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}" [[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}" [[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}" [[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}" [[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}" if [ ! "${1:-}" = "noverify" ]; then verify_config fi store_configvars } # Initialize system init_system() { load_config # Lockfile handling (prevents concurrent access) if [[ -n "${LOCKFILE}" ]]; then LOCKDIR="$(dirname "${LOCKFILE}")" [[ -w "${LOCKDIR}" ]] || _exiterr "Directory ${LOCKDIR} for LOCKFILE ${LOCKFILE} is not writable, aborting." ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting." remove_lock() { rm -f "${LOCKFILE}"; } trap 'remove_lock' EXIT fi # Get CA URLs CA_DIRECTORY="$(http_request get "${CA}")" # Automatic discovery of API version if [[ "${API}" = "auto" ]]; then grep -q newOrder <<< "${CA_DIRECTORY}" && API=2 || API=1 fi if [[ ${API} -eq 1 ]]; then # shellcheck disable=SC2015 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)" && CA_TERMS="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value terms-of-service)" && CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" || _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint." # Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part 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)" && CA_TERMS="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value termsOfService)" && 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 export WELLKNOWN BASEDIR CERTDIR CONFIG COMMAND # Checking for private key ... register_new_key="no" if [[ -n "${PARAM_ACCOUNT_KEY:-}" ]]; then # a private key was specified from the command line so use it for this run echo "Using private key ${PARAM_ACCOUNT_KEY} instead of account key" ACCOUNT_KEY="${PARAM_ACCOUNT_KEY}" ACCOUNT_KEY_JSON="${PARAM_ACCOUNT_KEY}.json" [ "${COMMAND:-}" = "register" ] && register_new_key="yes" else # Check if private account key exists, if it doesn't exist yet generate a new one (rsa key) if [[ ! -e "${ACCOUNT_KEY}" ]]; then 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 exit 1 fi echo "+ Generating account key..." local tmp_account_key="$(_mktemp)" _openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}" 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)" thumbprint="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}" | "${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 echo "+ Registering account key with ACME server..." FAILED=false if [[ ${API} -eq 1 && -z "${CA_NEW_REG}" ]] || [[ ${API} -eq 2 && -z "${CA_NEW_ACCOUNT}" ]]; then echo "Certificate authority doesn't allow registrations." FAILED=true fi # If an email for the contact has been provided then adding it to the registration request if [[ "${FAILED}" = "false" ]]; then if [[ ${API} -eq 1 ]]; then if [[ -n "${CONTACT_EMAIL}" ]]; then (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"${CA_TERMS}"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true else (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"${CA_TERMS}"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true fi else if [[ -n "${CONTACT_EMAIL}" ]]; then (signed_request "${CA_NEW_ACCOUNT}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true else (signed_request "${CA_NEW_ACCOUNT}" '{"termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true fi fi fi if [[ "${FAILED}" = "true" ]]; then echo >&2 echo >&2 echo "Error registering account key. See message above for more information." >&2 rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}" exit 1 fi elif [[ "${COMMAND:-}" = "register" ]]; then echo "+ Account already registered!" exit 0 fi # Read account information or request from CA if missing if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" if [[ ${API} -eq 1 ]]; then ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}" else ACCOUNT_URL="${CA_ACCOUNT}/${ACCOUNT_ID}" fi else echo "Fetching missing account information from CA..." 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 ^Location: | awk '{print $2}' | tr -d '\r\n')" ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{}')" fi ACCOUNT_ID="${ACCOUNT_URL##*/}" echo "${ACCOUNT_INFO}" > "${ACCOUNT_KEY_JSON}" fi } # Different sed version for different os types... _sed() { if [[ "${OSTYPE}" = "Linux" || "${OSTYPE:0:5}" = "MINGW" ]]; then sed -r "${@}" else sed -E "${@}" fi } # Print error message and exit with error _exiterr() { echo "ERROR: ${1}" >&2 exit 1 } # Remove newlines and whitespace from json clean_json() { tr -d '\r\n' | _sed -e 's/ +/ /g' -e 's/\{ /{/g' -e 's/ \}/}/g' -e 's/\[ /[/g' -e 's/ \]/]/g' } # Encode data as url-safe formatted base64 urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_' "${OPENSSL}" base64 -e | tr -d '\n\r' | _sed -e 's:=*$::g' -e 'y:+/:-_:' } # 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')" } # Get string value from json dictionary get_json_string_value() { local filter filter=$(printf 's/.*"%s": *"\([^"]*\)".*/\\1/p' "$1") sed -n "${filter}" } # Get array value from json dictionary get_json_array_value() { local filter filter=$(printf 's/.*"%s": *\\[\([^]]*\)\\].*/\\1/p' "$1") sed -n "${filter}" } # Get sub-dictionary from json get_json_dict_value() { local filter filter=$(printf 's/.*"%s": *{\([^}]*\)}.*/\\1/p' "$1") sed -n "${filter}" } # Get integer value from json get_json_int_value() { local filter filter=$(printf 's/.*"%s": *\([0-9]*\).*/\\1/p' "$1") sed -n "${filter}" } rm_json_arrays() { local filter filter='s/\[[^][]*\]/null/g' # remove three levels of nested arrays sed -e "${filter}" -e "${filter}" -e "${filter}" } # OpenSSL writes to stderr/stdout even when there are no errors. So just # display the output if the exit code was != 0 to simplify debugging. _openssl() { set +e out="$("${OPENSSL}" "${@}" 2>&1)" res=$? set -e if [[ ${res} -ne 0 ]]; then echo " + ERROR: failed to run $* (Exitcode: ${res})" >&2 echo >&2 echo "Details:" >&2 echo "${out}" >&2 echo >&2 exit ${res} fi } # Send http(s) request with specified method http_request() { tempcont="$(_mktemp)" tempheaders="$(_mktemp)" if [[ -n "${IP_VERSION:-}" ]]; then ip_version="-${IP_VERSION}" fi set +e if [[ "${1}" = "head" ]]; then statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)" touch "${tempheaders}" curlret="${?}" 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}")" 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}")" curlret="${?}" else set -e _exiterr "Unknown request method: ${1}" fi set -e if [[ ! "${curlret}" = "0" ]]; then _exiterr "Problem connecting to server (${1} for ${2}; curl returned with ${curlret})" fi if [[ ! "${statuscode:0:1}" = "2" ]]; then # check for existing registration warning if [[ "${API}" = "1" ]] && [[ -n "${CA_NEW_REG:-}" ]] && [[ "${2}" = "${CA_NEW_REG:-}" ]] && [[ "${statuscode}" = "409" ]] && grep -q "Registration key is already in use" "${tempcont}"; then # do nothing : # check for already-revoked warning elif [[ -n "${CA_REVOKE_CERT:-}" ]] && [[ "${2}" = "${CA_REVOKE_CERT:-}" ]] && [[ "${statuscode}" = "409" ]]; then grep -q "Certificate already revoked" "${tempcont}" && return else echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2 echo >&2 echo "Details:" >&2 cat "${tempheaders}" >&2 cat "${tempcont}" >&2 echo >&2 echo >&2 # 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})" "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" fi rm -f "${tempcont}" rm -f "${tempheaders}" # remove temporary domains.txt file if used [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}" exit 1 fi fi if { true >&4; } 2>/dev/null; then cat "${tempheaders}" >&4 fi cat "${tempcont}" rm -f "${tempcont}" rm -f "${tempheaders}" } # Send signed request 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 Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" else nonce="$(http_request head "${CA_NEW_NONCE}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" 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}"'"}' protected64="$(printf '%s' "${protected}" | urlbase64)" 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}"'"}' else protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "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 [[ ${API} -eq 1 ]]; then # Send header + extended header + payload + signature to the acme-server data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' else # Send extended header + payload + signature to the acme-server data='{"protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' fi http_request post "${1}" "${data}" } # Extracts all subject names from a CSR # Outputs either the CN, or the SANs, one per line extract_altnames() { csr="${1}" # the CSR itself (not a file) if ! <<<"${csr}" "${OPENSSL}" req -verify -noout 2>/dev/null; then _exiterr "Certificate signing request isn't valid" fi reqtext="$( <<<"${csr}" "${OPENSSL}" req -noout -text )" if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then # SANs used, extract these altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )" # 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" fi # strip away the DNS: prefix altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|othername:)//' )" printf "%s" "${altnames}" | tr '\n' ' ' else # No SANs, extract CN altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.* CN ?= ?([^ /,]*).*/\1/' )" printf "%s" "${altnames}" fi } # Create certificate for domain(s) and outputs it FD 3 sign_csr() { csr="${1}" # the CSR itself (not a file) if { true >&3; } 2>/dev/null; then : # fd 3 looks OK else _exiterr "sign_csr: FD 3 not open" fi shift 1 || true export altnames="${*}" if [[ ${API} -eq 1 ]]; then if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then _exiterr "Certificate authority doesn't allow certificate signing" fi elif [[ ${API} -eq 2 ]] && [[ -z "${CA_NEW_ORDER}" ]]; then _exiterr "Certificate authority doesn't allow certificate signing" fi if [[ -n "${ZSH_VERSION:-}" ]]; then local -A challenge_names challenge_uris challenge_tokens authorizations keyauths deploy_args else local -a challenge_names challenge_uris challenge_tokens authorizations keyauths deploy_args fi # Initial step: Find which authorizations we're dealing with if [[ ${API} -eq 2 ]]; then # Request new order and store authorization URIs local challenge_identifiers="" for altname in ${altnames}; do challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")" done challenge_identifiers="[${challenge_identifiers%, }]" echo " + Requesting new certificate order from CA..." result="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}')" order_authorizations="$(echo ${result} | get_json_array_value authorizations)" finalize="$(echo "${result}" | get_json_string_value finalize)" local idx=0 for uri in ${order_authorizations}; do authorizations[${idx}]="$(echo "${uri}" | _sed -e 's/\"(.*)".*/\1/')" idx=$((idx+1)) done echo " + Received ${idx} authorizations URLs from the CA" else # Copy $altnames to $authorizations (just doing this to reduce duplicate code later on) local idx=0 for altname in ${altnames}; do authorizations[${idx}]="${altname}" idx=$((idx+1)) done fi # Check if authorizations are valid and gather challenge information for pending authorizations local idx=0 for authorization in ${authorizations[*]}; do if [[ "${API}" -eq 2 ]]; then # Receive authorization ($authorization is authz uri) response="$(http_request get "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" | clean_json)" identifier="$(echo "${response}" | get_json_dict_value identifier | get_json_string_value value)" echo " + Handling authorization for ${identifier}" else # Request new authorization ($authorization is altname) identifier="${authorization}" echo " + Requesting authorization for ${identifier}..." response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${identifier}"'"}}' | clean_json)" 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 fi # Find challenge in authorization challenges="$(echo "${response}" | _sed 's/.*"challenges": \[(\{.*\})\].*/\1/')" challenge="$(<<<"${challenges}" _sed -e 's/^[^\[]+\[(.+)\]$/\1/' -e 's/\}(, (\{)|(\]))/}\'$'\n''\2/g' | grep \""${CHALLENGETYPE}"\" || true)" if [ -z "${challenge}" ]; then allowed_validations="$(grep -Eo '"type": "[^"]+"' <<< "${challenges}" | grep -Eo ' "[^"]+"' | _sed -e 's/"//g' -e 's/^ //g')" _exiterr "Validating this certificate is not possible using ${CHALLENGETYPE}. Possible validation methods are: ${allowed_validations}" fi # Gather challenge information challenge_names[${idx}]="${identifier}" challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)" if [[ ${API} -eq 2 ]]; then challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value url)" else challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value uri)" fi # Prepare challenge tokens and deployment parameters keyauth="${challenge_tokens[${idx}]}.${thumbprint}" case "${CHALLENGETYPE}" in "http-01") # Store challenge response in well-known location and make world-readable (so that a webserver can access it) printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_tokens[${idx}]}" chmod a+r "${WELLKNOWN}/${challenge_tokens[${idx}]}" keyauth_hook="${keyauth}" ;; "dns-01") # Generate DNS entry content for dns-01 validation keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)" ;; esac keyauths[${idx}]="${keyauth}" deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}" idx=$((idx+1)) done local num_pending_challenges=${idx} echo " + ${num_pending_challenges} pending challenge(s)" # Deploy challenge tokens if [[ ${num_pending_challenges} -ne 0 ]]; then echo " + Deploying challenge tokens..." if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then "${HOOK}" "deploy_challenge" ${deploy_args[@]} elif [[ -n "${HOOK}" ]]; then # Run hook script to deploy the challenge token local idx=0 while [ ${idx} -lt ${num_pending_challenges} ]; do "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} idx=$((idx+1)) done fi fi # Validate pending challenges local idx=0 while [ ${idx} -lt ${num_pending_challenges} ]; do echo " + Responding to challenge for ${challenge_names[${idx}]} authorization..." # Ask the acme-server to verify our challenge and wait until it is no longer pending if [[ ${API} -eq 1 ]]; then result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauths[${idx}]}"'"}' | clean_json)" else result="$(signed_request "${challenge_uris[${idx}]}" '{"keyAuthorization": "'"${keyauths[${idx}]}"'"}' | clean_json)" fi reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)" while [[ "${reqstatus}" = "pending" ]]; do sleep 1 result="$(http_request get "${challenge_uris[${idx}]}")" reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)" done [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" if [[ "${reqstatus}" = "valid" ]]; then echo " + Challenge is valid!" else [[ -n "${HOOK}" ]] && "${HOOK}" "invalid_challenge" "${altname}" "${result}" break fi idx=$((idx+1)) done if [[ ${num_pending_challenges} -ne 0 ]]; then echo " + Cleaning challenge tokens..." # Clean challenge tokens using chained hook [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[@]} # Clean remaining challenge tokens if validation has failed local idx=0 while [ ${idx} -lt ${num_pending_challenges} ]; do # Delete challenge file [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" # Clean challenge token using non-chained hook [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[${idx}]} idx=$((idx+1)) done if [[ "${reqstatus}" != "valid" ]]; then echo " + Challenge validation has failed :(" _exiterr "Challenge is invalid! (returned: ${reqstatus}) (result: ${result})" fi fi # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem echo " + Requesting certificate..." csr64="$( <<<"${csr}" "${OPENSSL}" req -config "${OPENSSL_CNF}" -outform DER | urlbase64)" if [[ ${API} -eq 1 ]]; then crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)" crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )" else result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | clean_json | get_json_string_value certificate)" crt="$(http_request get "${result}")" fi # Try to load the certificate to detect corruption echo " + Checking certificate..." _openssl x509 -text <<<"${crt}" echo "${crt}" >&3 unset challenge_token echo " + Done!" } # grep issuer cert uri from certificate get_issuer_cert_uri() { certificate="${1}" "${OPENSSL}" x509 -in "${certificate}" -noout -text | (grep 'CA Issuers - URI:' | cut -d':' -f2-) || true } get_issuer_hash() { certificate="${1}" "${OPENSSL}" x509 -in "${certificate}" -noout -issuer_hash } get_ocsp_url() { certificate="${1}" "${OPENSSL}" x509 -in "${certificate}" -noout -ocsp_uri } # walk certificate chain, retrieving all intermediate certificates walk_chain() { local certificate certificate="${1}" local issuer_cert_uri issuer_cert_uri="${2:-}" if [[ -z "${issuer_cert_uri}" ]]; then issuer_cert_uri="$(get_issuer_cert_uri "${certificate}")"; fi if [[ -n "${issuer_cert_uri}" ]]; then # create temporary files local tmpcert local tmpcert_raw tmpcert_raw="$(_mktemp)" tmpcert="$(_mktemp)" # download certificate http_request get "${issuer_cert_uri}" > "${tmpcert_raw}" # PEM if grep -q "BEGIN CERTIFICATE" "${tmpcert_raw}"; then mv "${tmpcert_raw}" "${tmpcert}" # DER elif "${OPENSSL}" x509 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM 2> /dev/null > /dev/null; then : # PKCS7 elif "${OPENSSL}" pkcs7 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM -print_certs 2> /dev/null > /dev/null; then : # Unknown certificate type else _exiterr "Unknown certificate type in chain" fi local next_issuer_cert_uri next_issuer_cert_uri="$(get_issuer_cert_uri "${tmpcert}")" if [[ -n "${next_issuer_cert_uri}" ]]; then printf "\n%s\n" "${issuer_cert_uri}" cat "${tmpcert}" walk_chain "${tmpcert}" "${next_issuer_cert_uri}" fi rm -f "${tmpcert}" "${tmpcert_raw}" fi } # Create certificate for domain(s) sign_domain() { local certdir="${1}" shift timestamp="${1}" shift domain="${1}" altnames="${*}" export altnames echo " + Signing domains..." if [[ ${API} -eq 1 ]]; then if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then _exiterr "Certificate authority doesn't allow certificate signing" fi elif [[ ${API} -eq 2 ]] && [[ -z "${CA_NEW_ORDER}" ]]; then _exiterr "Certificate authority doesn't allow certificate signing" fi local privkey="privkey.pem" if [[ ! -e "${certdir}/cert-${timestamp}.csr" ]]; then # generate a new private key if we need or want one if [[ ! -r "${certdir}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then echo " + Generating private key..." privkey="privkey-${timestamp}.pem" local 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}";; esac cat "${tmp_privkey}" > "${certdir}/privkey-${timestamp}.pem" rm "${tmp_privkey}" fi # move rolloverkey into position (if any) if [[ -r "${certdir}/privkey.pem" && -r "${certdir}/privkey.roll.pem" && "${PRIVATE_KEY_RENEW}" = "yes" && "${PRIVATE_KEY_ROLLOVER}" = "yes" ]]; then echo " + Moving Rolloverkey into position.... " mv "${certdir}/privkey.roll.pem" "${certdir}/privkey-tmp.pem" mv "${certdir}/privkey-${timestamp}.pem" "${certdir}/privkey.roll.pem" mv "${certdir}/privkey-tmp.pem" "${certdir}/privkey-${timestamp}.pem" fi # generate a new private rollover key if we need or want one if [[ ! -r "${certdir}/privkey.roll.pem" && "${PRIVATE_KEY_ROLLOVER}" = "yes" && "${PRIVATE_KEY_RENEW}" = "yes" ]]; then 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";; esac fi # delete rolloverkeys if disabled if [[ -r "${certdir}/privkey.roll.pem" && ! "${PRIVATE_KEY_ROLLOVER}" = "yes" ]]; then echo " + Removing Rolloverkey (feature disabled)..." rm -f "${certdir}/privkey.roll.pem" fi # Generate signing request config and the actual signing request echo " + Generating signing request..." SAN="" for altname in ${altnames}; do SAN="${SAN}DNS:${altname}, " done SAN="${SAN%%, }" local tmp_openssl_cnf tmp_openssl_cnf="$(_mktemp)" cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}" printf "[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: SUBJ="/${SUBJ}" fi "${OPENSSL}" req -new -sha256 -key "${certdir}/${privkey}" -out "${certdir}/cert-${timestamp}.csr" -subj "${SUBJ}" -reqexts SAN -config "${tmp_openssl_cnf}" rm -f "${tmp_openssl_cnf}" fi crt_path="${certdir}/cert-${timestamp}.pem" # shellcheck disable=SC2086 sign_csr "$(< "${certdir}/cert-${timestamp}.csr")" ${altnames} 3>"${crt_path}" # Create fullchain.pem echo " + Creating fullchain.pem..." if [[ ${API} -eq 1 ]]; then cat "${crt_path}" > "${certdir}/fullchain-${timestamp}.pem" local issuer_hash issuer_hash="$(get_issuer_hash "${crt_path}")" if [ -e "${CHAINCACHE}/${issuer_hash}.chain" ]; then echo " + Using cached chain!" cat "${CHAINCACHE}/${issuer_hash}.chain" > "${certdir}/chain-${timestamp}.pem" else echo " + Walking chain..." local issuer_cert_uri issuer_cert_uri="$(get_issuer_cert_uri "${crt_path}" || echo "unknown")" (walk_chain "${crt_path}" > "${certdir}/chain-${timestamp}.pem") || _exiterr "Walking chain has failed, your certificate has been created and can be found at ${crt_path}, the corresponding private key at ${privkey}. If you want you can manually continue on creating and linking all necessary files. If this error occurs again you should manually generate the certificate chain and place it under ${CHAINCACHE}/${issuer_hash}.chain (see ${issuer_cert_uri})" cat "${certdir}/chain-${timestamp}.pem" > "${CHAINCACHE}/${issuer_hash}.chain" fi cat "${certdir}/chain-${timestamp}.pem" >> "${certdir}/fullchain-${timestamp}.pem" else tmpcert="$(_mktemp)" tmpchain="$(_mktemp)" awk '{print >out}; /----END CERTIFICATE-----/{out=tmpchain}' out="${tmpcert}" tmpchain="${tmpchain}" "${certdir}/cert-${timestamp}.pem" mv "${certdir}/cert-${timestamp}.pem" "${certdir}/fullchain-${timestamp}.pem" cat "${tmpcert}" > "${certdir}/cert-${timestamp}.pem" cat "${tmpchain}" > "${certdir}/chain-${timestamp}.pem" rm "${tmpcert}" "${tmpchain}" fi # Update symlinks [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${certdir}/privkey.pem" ln -sf "chain-${timestamp}.pem" "${certdir}/chain.pem" ln -sf "fullchain-${timestamp}.pem" "${certdir}/fullchain.pem" ln -sf "cert-${timestamp}.csr" "${certdir}/cert.csr" ln -sf "cert-${timestamp}.pem" "${certdir}/cert.pem" # Wait for hook script to clean the challenge and to deploy cert if used [[ -n "${HOOK}" ]] && "${HOOK}" "deploy_cert" "${domain}" "${certdir}/privkey.pem" "${certdir}/cert.pem" "${certdir}/fullchain.pem" "${certdir}/chain.pem" "${timestamp}" unset challenge_token echo " + Done!" } # Usage: --version (-v) # Description: Print version information command_version() { load_config noverify echo "Dehydrated by Lukas Schauer" echo "https://dehydrated.io" echo "" echo "Dehydrated version: ${VERSION}" revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")" echo "GIT-Revision: ${revision}" echo "" if [[ "${OSTYPE}" = "FreeBSD" ]]; then echo "OS: $(uname -sr)" else echo "OS: $(cat /etc/issue | grep -v ^$ | 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 2>&1 | head -n1 | cut -d" " -f1-2)" if [[ "${OSTYPE}" = "FreeBSD" ]]; then echo " awk, sed, mktemp: FreeBSD base system versions" else echo " awk: $(awk -W version 2>&1 | head -n1)" echo " sed: $(sed --version 2>&1 | head -n1)" echo " mktemp: $(mktemp --version 2>&1 | head -n1)" fi echo " grep: $(grep --version 2>&1 | head -n1)" echo " diff: $(diff --version 2>&1 | head -n1)" echo " openssl: $("${OPENSSL}" version 2>&1)" exit 0 } # Usage: --register # Description: Register account key command_register() { init_system echo "+ Done!" exit 0 } # Usage: --account # Description: Update account contact information command_account() { init_system FAILED=false NEW_ACCOUNT_KEY_JSON="$(_mktemp)" # Check if we have the registration id if [[ -z "${ACCOUNT_ID}" ]]; then _exiterr "Error retrieving registration id." fi echo "+ Updating registration id: ${ACCOUNT_ID} contact information..." if [[ ${API} -eq 1 ]]; then # If an email for the contact has been provided then adding it to the registered account if [[ -n "${CONTACT_EMAIL}" ]]; then (signed_request "${ACCOUNT_URL}" '{"resource": "reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true else (signed_request "${ACCOUNT_URL}" '{"resource": "reg", "contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true fi else # If an email for the contact has been provided then adding it to the registered account if [[ -n "${CONTACT_EMAIL}" ]]; then (signed_request "${ACCOUNT_URL}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true else (signed_request "${ACCOUNT_URL}" '{"contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true fi fi if [[ "${FAILED}" = "true" ]]; then rm "${NEW_ACCOUNT_KEY_JSON}" _exiterr "Error updating account information. See message above for more information." fi if diff -q "${NEW_ACCOUNT_KEY_JSON}" "${ACCOUNT_KEY_JSON}" > /dev/null; then echo "+ Account information was the same after the update" rm "${NEW_ACCOUNT_KEY_JSON}" else ACCOUNT_KEY_JSON_BACKUP="${ACCOUNT_KEY_JSON%.*}-$(date +%s).json" echo "+ Backup ${ACCOUNT_KEY_JSON} as ${ACCOUNT_KEY_JSON_BACKUP}" cp -p "${ACCOUNT_KEY_JSON}" "${ACCOUNT_KEY_JSON_BACKUP}" echo "+ Populate ${ACCOUNT_KEY_JSON}" mv "${NEW_ACCOUNT_KEY_JSON}" "${ACCOUNT_KEY_JSON}" fi echo "+ Done!" exit 0 } # Usage: --cron (-c) # Description: Sign/renew non-existent/changed/expiring certificates. command_sign_domains() { init_system hookscript_bricker_hook # Call startup hook [[ -n "${HOOK}" ]] && "${HOOK}" "startup_hook" if [ ! -d "${CHAINCACHE}" ]; then echo " + Creating chain cache directory ${CHAINCACHE}" mkdir "${CHAINCACHE}" fi if [[ -n "${PARAM_DOMAIN:-}" ]]; then DOMAINS_TXT="$(_mktemp)" if [[ -n "${PARAM_ALIAS:-}" ]]; then printf -- "${PARAM_DOMAIN} > ${PARAM_ALIAS}" > "${DOMAINS_TXT}" else printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}" fi elif [[ -e "${DOMAINS_TXT}" ]]; then if [[ ! -r "${DOMAINS_TXT}" ]]; then _exiterr "domains.txt found but not readable" fi else _exiterr "domains.txt not found and --domain not given" fi # 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 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!" domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)" morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)" [ ${aliascount} -lt 1 ] && alias="${domain}" || alias="${alias#>}" export alias if [[ -z "${morenames}" ]];then echo "Processing ${domain}" else echo "Processing ${domain} with alternative names: ${morenames}" fi if [ "${alias:0:2}" = "*." ]; then _exiterr "Please define a valid alias for your ${domain} wildcard-certificate. See domains.txt-documentation for more details." fi local certdir="${CERTDIR}/${alias}" cert="${certdir}/cert.pem" chain="${certdir}/chain.pem" force_renew="${PARAM_FORCE:-no}" timestamp="$(date +%s)" # If there is no existing certificate directory => make it if [[ ! -e "${certdir}" ]]; then echo " + Creating new directory ${certdir} ..." mkdir -p "${certdir}" || _exiterr "Unable to create directory ${certdir}" fi # read cert config # for now this loads the certificate specific config in a subshell and parses a diff of set variables. # we could just source the config file but i decided to go this way to protect people from accidentally overriding # variables used internally by this script itself. if [[ -n "${DOMAINS_D}" ]]; then certconfig="${DOMAINS_D}/${alias}" else certconfig="${certdir}/config" fi if [ -f "${certconfig}" ]; then echo " + Using certificate specific config file!" ORIGIFS="${IFS}" IFS=$'\n' for cfgline in $( beforevars="$(_mktemp)" aftervars="$(_mktemp)" set > "${beforevars}" # shellcheck disable=SC1090 . "${certconfig}" set > "${aftervars}" diff -u "${beforevars}" "${aftervars}" | grep -E '^\+[^+]' rm "${beforevars}" rm "${aftervars}" ); do config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)" config_value="$(echo "${cfgline:1}" | cut -d'=' -f2-)" case "${config_var}" in KEY_ALGO|OCSP_MUST_STAPLE|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS) echo " + ${config_var} = ${config_value}" declare -- "${config_var}=${config_value}" ;; _) ;; *) echo " ! Setting ${config_var} on a per-certificate base is not (yet) supported" >&2 esac done IFS="${ORIGIFS}" fi verify_config hookscript_bricker_hook export WELLKNOWN CHALLENGETYPE KEY_ALGO PRIVATE_KEY_ROLLOVER skip="no" # Allow for external CSR generation local csr="" if [[ -n "${HOOK}" ]]; then csr="$("${HOOK}" "generate_csr" "${domain}" "${certdir}" "${domain} ${morenames}")" if grep -qE "\-----BEGIN (NEW )?CERTIFICATE REQUEST-----" <<< "${csr}"; then altnames="$(extract_altnames "${csr}")" domain="$(cut -d' ' -f1 <<< "${altnames}")" morenames="$(cut -s -d' ' -f2- <<< "${altnames}")" echo " + Using CSR from hook script (real names: ${altnames})" else csr="" fi fi # Check domain names of existing certificate if [[ -e "${cert}" ]]; 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/^ //')" if [[ "${certnames}" = "${givennames}" ]]; then echo " unchanged." else echo " changed!" echo " + Domain name(s) are not matching!" echo " + Names in old certificate: ${certnames}" echo " + Configured names: ${givennames}" echo " + Forcing renew." force_renew="yes" fi fi # Check expire date of existing certificate if [[ -e "${cert}" ]]; then echo " + Checking expire date of existing cert..." 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}"; then printf "(Longer than %d days). " "${RENEW_DAYS}" if [[ "${force_renew}" = "yes" ]]; then echo "Ignoring because renew was forced!" else # Certificate-Names unchanged and cert is still valid echo "Skipping renew!" [[ -n "${HOOK}" ]] && "${HOOK}" "unchanged_cert" "${domain}" "${certdir}/privkey.pem" "${certdir}/cert.pem" "${certdir}/fullchain.pem" "${certdir}/chain.pem" skip="yes" fi else echo "(Less than ${RENEW_DAYS} days). Renewing!" fi fi local update_ocsp update_ocsp="no" # Sign certificate for this domain if [[ ! "${skip}" = "yes" ]]; then update_ocsp="yes" [[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr" if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} & wait $! || true else sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} fi 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 432000 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 "^(0|(1\.0))\." <<< "$(${OPENSSL} version | awk '{print $2}')"; then ocsp_log="$("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respout "${certdir}/ocsp-${ocsp_timestamp}.der" -url "${ocsp_url}" -header "HOST" "$(echo "${ocsp_url}" | _sed -e 's/^http(s?):\/\///' -e 's/\/.*$//g')" 2>&1)" || _exiterr "Error while fetching OCSP information: ${ocsp_log}" else ocsp_log="$("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respout "${certdir}/ocsp-${ocsp_timestamp}.der" -url "${ocsp_url}" 2>&1)" || _exiterr "Error while fetching OCSP information: ${ocsp_log}" fi ln -sf "ocsp-${ocsp_timestamp}.der" "${certdir}/ocsp.der" [[ -n "${HOOK}" ]] && altnames="${domain} ${morenames}" "${HOOK}" "deploy_ocsp" "${domain}" "${certdir}/ocsp.der" "${ocsp_timestamp}" else echo " + OSCP stapling file is still valid (skipping update)" fi fi done reset_configvars # remove temporary domains.txt file if used [[ -n "${PARAM_DOMAIN:-}" ]] && rm -f "${DOMAINS_TXT}" [[ -n "${HOOK}" ]] && "${HOOK}" "exit_hook" if [[ "${AUTO_CLEANUP}" == "yes" ]]; then echo "+ Running automatic cleanup" command_cleanup noinit fi exit 0 } # Usage: --signcsr (-s) path/to/csr.pem # Description: Sign a given CSR, output CRT on stdout (advanced usage) command_sign_csr() { # 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 _exiterr "Could not read certificate signing request ${csrfile}" fi csr="$(cat "${csrfile}")" # extract names altnames="$(extract_altnames "${csr}")" # gen cert certfile="$(_mktemp)" sign_csr "${csr}" ${altnames} 3> "${certfile}" # print cert echo "# CERT #" >&3 cat "${certfile}" >&3 echo >&3 # print chain if [ -n "${PARAM_FULL_CHAIN:-}" ]; then # get and convert ca cert chainfile="$(_mktemp)" tmpchain="$(_mktemp)" http_request get "$("${OPENSSL}" x509 -in "${certfile}" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${tmpchain}" if grep -q "BEGIN CERTIFICATE" "${tmpchain}"; then mv "${tmpchain}" "${chainfile}" else "${OPENSSL}" x509 -in "${tmpchain}" -inform DER -out "${chainfile}" -outform PEM rm "${tmpchain}" fi echo "# CHAIN #" >&3 cat "${chainfile}" >&3 rm "${chainfile}" fi # cleanup rm "${certfile}" exit 0 } # Usage: --revoke (-r) path/to/cert.pem # Description: Revoke specified certificate command_revoke() { init_system [[ -n "${CA_REVOKE_CERT}" ]] || _exiterr "Certificate authority doesn't allow certificate revocation." cert="${1}" if [[ -L "${cert}" ]]; then # follow symlink and use real certificate name (so we move the real file and not the symlink at the end) local link_target link_target="$(readlink -n "${cert}")" if [[ "${link_target}" =~ ^/ ]]; then cert="${link_target}" else cert="$(dirname "${cert}")/${link_target}" fi fi [[ -f "${cert}" ]] || _exiterr "Could not find certificate ${cert}" echo "Revoking ${cert}" cert64="$("${OPENSSL}" x509 -in "${cert}" -inform PEM -outform DER | urlbase64)" if [[ ${API} -eq 1 ]]; then response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}' | clean_json)" else response="$(signed_request "${CA_REVOKE_CERT}" '{"certificate": "'"${cert64}"'"}' | clean_json)" fi # if there is a problem with our revoke request _request (via signed_request) will report this and "exit 1" out # so if we are here, it is safe to assume the request was successful echo " + Done." echo " + Renaming certificate to ${cert}-revoked" mv -f "${cert}" "${cert}-revoked" } # Usage: --cleanup (-gc) # Description: Move unused certificate files to archive directory command_cleanup() { if [ ! "${1:-}" = "noinit" ]; then load_config fi # Create global archive directory if not existent if [[ ! -e "${BASEDIR}/archive" ]]; then mkdir "${BASEDIR}/archive" fi # Allow globbing [[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f # Loop over all certificate directories for certdir in "${CERTDIR}/"*; do # Skip if entry is not a folder [[ -d "${certdir}" ]] || continue # Get certificate name certname="$(basename "${certdir}")" # Create certificates archive directory if not existent archivedir="${BASEDIR}/archive/${certname}" if [[ ! -e "${archivedir}" ]]; then mkdir "${archivedir}" fi # Loop over file-types (certificates, keys, signing-requests, ...) for filetype in cert.csr cert.pem chain.pem fullchain.pem privkey.pem ocsp.der; do # Skip if symlink is broken [[ -r "${certdir}/${filetype}" ]] || continue # Look up current file in use current="$(basename "$(readlink "${certdir}/${filetype}")")" # Split filetype into name and extension filebase="$(echo "${filetype}" | cut -d. -f1)" fileext="$(echo "${filetype}" | cut -d. -f2)" # Loop over all files of this type for file in "${certdir}/${filebase}-"*".${fileext}" "${certdir}/${filebase}-"*".${fileext}-revoked"; do # Check if current file is in use, if unused move to archive directory filename="$(basename "${file}")" if [[ ! "${filename}" = "${current}" ]]; then echo "Moving unused file to archive directory: ${certname}/${filename}" mv "${certdir}/${filename}" "${archivedir}/${filename}" fi done done done exit 0 } # Usage: --help (-h) # Description: Show help text command_help() { printf "Usage: %s [-h] [command [argument]] [parameter [argument]] [parameter [argument]] ...\n\n" "${0}" printf "Default command: help\n\n" echo "Commands:" grep -e '^[[:space:]]*# Usage:' -e '^[[:space:]]*# Description:' -e '^command_.*()[[:space:]]*{' "${0}" | while read -r usage; read -r description; read -r command; do if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]] || [[ ! "${command}" =~ ^command_ ]]; then _exiterr "Error generating help text." fi printf " %-32s %s\n" "${usage##"# Usage: "}" "${description##"# Description: "}" done printf -- "\nParameters:\n" grep -E -e '^[[:space:]]*# PARAM_Usage:' -e '^[[:space:]]*# PARAM_Description:' "${0}" | while read -r usage; read -r description; do if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]]; then _exiterr "Error generating help text." fi printf " %-32s %s\n" "${usage##"# PARAM_Usage: "}" "${description##"# PARAM_Description: "}" done } # Usage: --env (-e) # Description: Output configuration variables for use in other scripts command_env() { echo "# dehydrated configuration" load_config typeset -p CA CERTDIR CHALLENGETYPE DOMAINS_D DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE } # Main method (parses script arguments and calls command_* methods) main() { COMMAND="" set_command() { [[ -z "${COMMAND}" ]] || _exiterr "Only one command can be executed at a time. See help (-h) for more information." COMMAND="${1}" } check_parameters() { if [[ -z "${1:-}" ]]; then echo "The specified command requires additional parameters. See help:" >&2 echo >&2 command_help >&2 exit 1 elif [[ "${1:0:1}" = "-" ]]; then _exiterr "Invalid argument: ${1}" fi } # shellcheck disable=SC2199 [[ -z "${@}" ]] && eval set -- "--help" while (( ${#} )); do case "${1}" in --help|-h) command_help exit 0 ;; --env|-e) set_command env ;; --cron|-c) set_command sign_domains ;; --register) set_command register ;; --account) set_command account ;; # PARAM_Usage: --accept-terms # PARAM_Description: Accept CAs terms of service --accept-terms) PARAM_ACCEPT_TERMS="yes" ;; --signcsr|-s) shift 1 set_command sign_csr check_parameters "${1:-}" PARAM_CSR="${1}" ;; --revoke|-r) shift 1 set_command revoke check_parameters "${1:-}" PARAM_REVOKECERT="${1}" ;; --version|-v) set_command version ;; --cleanup|-gc) set_command cleanup ;; # PARAM_Usage: --full-chain (-fc) # PARAM_Description: Print full chain when using --signcsr --full-chain|-fc) PARAM_FULL_CHAIN="1" ;; # PARAM_Usage: --ipv4 (-4) # PARAM_Description: Resolve names to IPv4 addresses only --ipv4|-4) PARAM_IP_VERSION="4" ;; # PARAM_Usage: --ipv6 (-6) # PARAM_Description: Resolve names to IPv6 addresses only --ipv6|-6) PARAM_IP_VERSION="6" ;; # PARAM_Usage: --domain (-d) domain.tld # PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!) --domain|-d) shift 1 check_parameters "${1:-}" if [[ -z "${PARAM_DOMAIN:-}" ]]; then PARAM_DOMAIN="${1}" else PARAM_DOMAIN="${PARAM_DOMAIN} ${1}" fi ;; # PARAM_Usage: --alias certalias # PARAM_Description: Use specified name for certificate directory (and per-certificate config) instead of the primary domain (only used if --domain is specified) --alias) shift 1 check_parameters "${1:-}" [[ -n "${PARAM_ALIAS:-}" ]] && _exiterr "Alias can only be specified once!" PARAM_ALIAS="${1}" ;; # PARAM_Usage: --keep-going (-g) # PARAM_Description: Keep going after encountering an error while creating/renewing multiple certificates in cron mode --keep-going|-g) PARAM_KEEP_GOING="yes" ;; # PARAM_Usage: --force (-x) # PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS --force|-x) PARAM_FORCE="yes" ;; # PARAM_Usage: --no-lock (-n) # PARAM_Description: Don't use lockfile (potentially dangerous!) --no-lock|-n) PARAM_NO_LOCK="yes" ;; # PARAM_Usage: --lock-suffix example.com # PARAM_Description: Suffix lockfile name with a string (useful for with -d) --lock-suffix) shift 1 check_parameters "${1:-}" PARAM_LOCKFILE_SUFFIX="${1}" ;; # PARAM_Usage: --ocsp # PARAM_Description: Sets option in CSR indicating OCSP stapling to be mandatory --ocsp) PARAM_OCSP_MUST_STAPLE="yes" ;; # PARAM_Usage: --privkey (-p) path/to/key.pem # PARAM_Description: Use specified private key instead of account key (useful for revocation) --privkey|-p) shift 1 check_parameters "${1:-}" PARAM_ACCOUNT_KEY="${1}" ;; # PARAM_Usage: --config (-f) path/to/config # PARAM_Description: Use specified config file --config|-f) shift 1 check_parameters "${1:-}" CONFIG="${1}" ;; # PARAM_Usage: --hook (-k) path/to/hook.sh # PARAM_Description: Use specified script for hooks --hook|-k) shift 1 check_parameters "${1:-}" PARAM_HOOK="${1}" ;; # PARAM_Usage: --out (-o) certs/directory # PARAM_Description: Output certificates into the specified directory --out|-o) shift 1 check_parameters "${1:-}" PARAM_CERTDIR="${1}" ;; # PARAM_Usage: --challenge (-t) http-01|dns-01 # PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported --challenge|-t) shift 1 check_parameters "${1:-}" PARAM_CHALLENGETYPE="${1}" ;; # PARAM_Usage: --algo (-a) rsa|prime256v1|secp384r1 # PARAM_Description: Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 --algo|-a) shift 1 check_parameters "${1:-}" PARAM_KEY_ALGO="${1}" ;; *) echo "Unknown parameter detected: ${1}" >&2 echo >&2 command_help >&2 exit 1 ;; esac shift 1 done case "${COMMAND}" in env) command_env;; sign_domains) command_sign_domains;; register) command_register;; account) command_account;; sign_csr) command_sign_csr "${PARAM_CSR}";; revoke) command_revoke "${PARAM_REVOKECERT}";; cleanup) command_cleanup;; version) command_version;; *) command_help; exit 1;; esac } # Determine OS type OSTYPE="$(uname)" if [[ ! "${DEHYDRATED_NOOP:-}" = "NOOP" ]]; then # Run script main "${@:-}" fi dehydrated-0.6.2/docs/000077500000000000000000000000001327017064000145645ustar00rootroot00000000000000dehydrated-0.6.2/docs/dns-verification.md000066400000000000000000000035751327017064000203640ustar00rootroot00000000000000### dns-01 challenge This script also supports the new `dns-01`-type verification. This type of verification requires you to be able to create a specific `TXT` DNS record for each hostname included in the certificate. You need a hook script that deploys the challenge to your DNS server. The hook script (indicated in the config file or the `--hook/-k` command line argument) gets four arguments: $1 an operation name (`clean_challenge`, `deploy_challenge`, `deploy_cert`, `invalid_challenge` or `request_failure`) and some operands for that. For `deploy_challenge` $2 is the domain name for which the certificate is required, $3 is a "challenge token" (which is not needed for dns-01), and $4 is a token which needs to be inserted in a TXT record for the domain. Typically, you will need to split the subdomain name in two, the subdomain name and the domain name separately. For example, for "my.example.com", you'll need "my" and "example.com" separately. You then have to prefix "_acme-challenge." before the subdomain name, as in "_acme-challenge.my" and set a TXT record for that on the domain (e.g. "example.com") which has the value supplied in $4 ``` _acme-challenge IN TXT $4 _acme-challenge.my IN TXT $4 ``` That could be done manually (as most providers don't have a DNS API), by having your hook script echo $1, $2 and $4 and then wait (`read -s -r -e < /dev/tty`) - give it a little time to get into their DNS system. Usually providers give you a boxes to put "_acme-challenge.my" and the token value in, and a dropdown to choose the record type, TXT. Or when you do have a DNS API, pass the details accordingly to achieve the same thing. You can delete the TXT record when called with operation `clean_challenge`, when $2 is also the domain name. Here are some examples: [Examples for DNS-01 hooks](https://github.com/lukas2511/dehydrated/wiki/Examples-for-DNS-01-hooks) dehydrated-0.6.2/docs/domains_txt.md000066400000000000000000000046461327017064000174510ustar00rootroot00000000000000## domains.txt dehydrated uses the file `domains.txt` as configuration for which certificates should be requested. The file should have the following format: ```text example.org example.com www.example.com example.net www.example.net wiki.example.net ``` This states that there are the following certificates: * `example.org` without any *alternative names* * `example.com` with an *alternative name* of `www.example.com` * `example.net` with the *alternative names*: `www.example.net` and `wiki.example.net` ### Aliases You can define an *alias* for your certificate which will (instead of the primary domain) be used as the directory name under your `CERTDIR` and for a per-certificate lookup. This is done using the `>` character. This allows multiple certificates with identical sets of domains but different configuration to exist. Here is an example of using an *alias* called `certalias` for creating the certificate for `example.net` with *alternative names* `www.example.net` and `wiki.example.net`. The certificate will be stored in the directory `certalias` under your `CERTDIR`. ```text example.net www.example.net wiki.example.net > certalias ``` ### Wildcards Support for wildcards was added by the ACME v2 protocol. Certificates with a wildcard domain as the first (or only) name require an *alias* to be set. *Aliases* can't start with `*.`. For example to create the wildcard for `*.service.example.com` your `domains.txt` could use the *alias* method like this: ```text *.service.example.com > star_service_example_com ``` This creates a wildcard certificate for only `*.service.example.com` and will store it in the directory `star_service_example_com` under your `CERTDIR`. As a note this certificate will **NOT** be valid for `service.example.com` but only for `*.service.example.com`. So it would, for example, be valid for `foo.service.example.com`. Another way to create it is using *alternative names*. For example your `domains.txt` could do this: ```text service.example.com *.service.example.com eggs.example.com *.ham.example.com ``` This creates two certificates one for `service.example.com` with an *alternative name* of `*.service.example.com` and a second certificate for `eggs.example.com` with an *alternative name* of `*.ham.example.com`. **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. dehydrated-0.6.2/docs/ecc.md000066400000000000000000000002561327017064000156430ustar00rootroot00000000000000### Elliptic Curve Cryptography (ECC) This script also supports certificates with Elliptic Curve public keys! Simply set the `KEY_ALGO` variable in one of the config files. dehydrated-0.6.2/docs/examples/000077500000000000000000000000001327017064000164025ustar00rootroot00000000000000dehydrated-0.6.2/docs/examples/config000066400000000000000000000103421327017064000175720ustar00rootroot00000000000000######################################################## # This is the main config file for dehydrated # # # # This file is looked for in the following locations: # # $SCRIPTDIR/config (next to this script) # # /usr/local/etc/dehydrated/config # # /etc/dehydrated/config # # ${PWD}/config (in current working-directory) # # # # Default values of this config are in comments # ######################################################## # Which user should dehydrated run as? This will be implictly enforced when running as root #DEHYDRATED_USER= # Which group should dehydrated run as? This will be implictly enforced when running as root #DEHYDRATED_GROUP= # Resolve names to addresses of IP version only. (curl) # supported values: 4, 6 # default: #IP_VERSION= # Path to certificate authority (default: https://acme-v02.api.letsencrypt.org/directory) #CA="https://acme-v02.api.letsencrypt.org/directory" # Path to old certificate authority # Set this value to your old CA value when upgrading from ACMEv1 to ACMEv2 under a different endpoint. # If dehydrated detects an account-key for the old CA it will automatically reuse that key # instead of registering a new one. # default: https://acme-v01.api.letsencrypt.org/directory #OLDCA="https://acme-v01.api.letsencrypt.org/directory" # Which challenge should be used? Currently http-01 and dns-01 are supported #CHALLENGETYPE="http-01" # Path to a directory containing additional config files, allowing to override # the defaults found in the main configuration file. Additional config files # in this directory needs to be named with a '.sh' ending. # default: #CONFIG_D= # Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined) #BASEDIR=$SCRIPTDIR # File containing the list of domains to request certificates for (default: $BASEDIR/domains.txt) #DOMAINS_TXT="${BASEDIR}/domains.txt" # Output directory for generated certificates #CERTDIR="${BASEDIR}/certs" # Directory for account keys and registration information #ACCOUNTDIR="${BASEDIR}/accounts" # Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: /var/www/dehydrated) #WELLKNOWN="/var/www/dehydrated" # Default keysize for private keys (default: 4096) #KEYSIZE="4096" # Path to openssl config file (default: - tries to figure out system default) #OPENSSL_CNF= # Path to OpenSSL binary (default: "openssl") #OPENSSL="openssl" # Extra options passed to the curl binary (default: ) #CURL_OPTS= # Program or function called in certain situations # # After generating the challenge-response, or after failed challenge (in this case altname is empty) # Given arguments: clean_challenge|deploy_challenge altname token-filename token-content # # After successfully signing certificate # Given arguments: deploy_cert domain path/to/privkey.pem path/to/cert.pem path/to/fullchain.pem # # BASEDIR and WELLKNOWN variables are exported and can be used in an external program # default: #HOOK= # Chain clean_challenge|deploy_challenge arguments together into one hook call per certificate (default: no) #HOOK_CHAIN="no" # Minimum days before expiration to automatically renew certificate (default: 30) #RENEW_DAYS="30" # Regenerate private keys instead of just signing new certificates on renewal (default: yes) #PRIVATE_KEY_RENEW="yes" # Create an extra private key for rollover (default: no) #PRIVATE_KEY_ROLLOVER="no" # Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 #KEY_ALGO=rsa # E-mail to use during the registration (default: ) #CONTACT_EMAIL= # Lockfile location, to prevent concurrent access (default: $BASEDIR/lock) #LOCKFILE="${BASEDIR}/lock" # Option to add CSR-flag indicating OCSP stapling to be mandatory (default: no) #OCSP_MUST_STAPLE="no" # Fetch OCSP responses (default: no) #OCSP_FETCH="no" # Issuer chain cache directory (default: $BASEDIR/chains) #CHAINCACHE="${BASEDIR}/chains" # Automatic cleanup (default: no) #AUTO_CLEANUP="no" # ACME API version (default: auto) #API=auto dehydrated-0.6.2/docs/examples/domains.txt000066400000000000000000000026421327017064000206010ustar00rootroot00000000000000# Create certificate for 'example.org' with an alternative name of # 'www.example.org'. It will be stored in the directory ${CERT_DIR}/example.org example.org www.example.org # Create certificate for 'example.com' with alternative names of # 'www.example.com' & 'wiki.example.com'. It will be stored in the directory # ${CERT_DIR}/example.com example.com www.example.com wiki.example.com # Using the alias 'certalias' create certificate for 'example.net' with # alternate name 'www.example.net' and store it in the directory # ${CERTDIR}/certalias example.net www.example.net > certalias # Using the alias 'service_example_com' create a wildcard certificate for # '*.service.example.com' and store it in the directory # ${CERTDIR}/service_example_com # NOTE: It is NOT a certificate for 'service.example.com' *.service.example.com > service_example_com # Using the alias 'star_service_example_org' create a wildcard certificate for # '*.service.example.org' with an alternative name of `service.example.org' # and store it in the directory ${CERTDIR}/star_service_example_org # NOTE: It is a certificate for 'service.example.org' *.service.example.org service.example.org > star_service_example_org # 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 service.example.net *.service.example.net dehydrated-0.6.2/docs/examples/hook.sh000077500000000000000000000153061327017064000177060ustar00rootroot00000000000000#!/usr/bin/env bash deploy_challenge() { 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. # 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}" # 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 } deploy_cert() { 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. # 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}" # 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 } unchanged_cert() { 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). } invalid_challenge() { 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 # 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}" # 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 } generate_csr() { 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 cerificate 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 } startup_hook() { # This hook is called before the cron command to do some initial tasks # (e.g. starting a webserver). : } exit_hook() { # This hook is called at the end of the cron command and can be used to # do some final (cleanup or other) tasks. : } HANDLER="$1"; shift if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then "$HANDLER" "$@" fi dehydrated-0.6.2/docs/hook_chain.md000066400000000000000000000055141327017064000172150ustar00rootroot00000000000000# HOOK_CHAIN If you want to deploy (and clean) all challenges for a single certificate in one hook call you can use `HOOK_CHAIN=yes` in your config. Calls to your hook script change in a way that instead of having only X parameters on deploy_challenge and clean_challenge it will have Y*X parameters, where Y is the number of domains in your cert, and you'll have to iterate over a set of X parameters at a time in your hook script. See below for an example on how the calls change: ### HOOK_CHAIN="no" (default behaviour) ``` # INFO: Using main config file /etc/dehydrated/config Processing lukas.im with alternative names: www.lukas.im + Checking domain name(s) of existing cert... unchanged. + Checking expire date of existing cert... + Valid till Jul 7 20:54:00 2016 GMT (Longer than 30 days). Ignoring because renew was forced! + Signing domains... + Generating private key... + Generating signing request... + Requesting challenge for lukas.im... + Requesting challenge for www.lukas.im... HOOK: deploy_challenge lukas.im blablabla blablabla.supersecure + Responding to challenge for lukas.im... HOOK: clean_challenge lukas.im blablabla blablabla.supersecure + Challenge is valid! HOOK: deploy_challenge www.lukas.im blublublu blublublu.supersecure + Responding to challenge for www.lukas.im... HOOK: clean_challenge www.lukas.im blublublu blublublu.supersecure + Challenge is valid! + Requesting certificate... + Checking certificate... + Done! + Creating fullchain.pem... HOOK: deploy_cert lukas.im /etc/dehydrated/certs/lukas.im/privkey.pem /etc/dehydrated/certs/lukas.im/cert.pem /etc/dehydrated/certs/lukas.im/fullchain.pem /etc/dehydrated/certs/lukas.im/chain.pem 1460152442 + Done! ``` ### HOOK_CHAIN="yes" ``` # INFO: Using main config file /etc/dehydrated/config Processing lukas.im with alternative names: www.lukas.im + Checking domain name(s) of existing cert... unchanged. + Checking expire date of existing cert... + Valid till Jul 7 20:52:00 2016 GMT (Longer than 30 days). Ignoring because renew was forced! + Signing domains... + Generating private key... + Generating signing request... + Requesting challenge for lukas.im... + Requesting challenge for www.lukas.im... HOOK: deploy_challenge lukas.im blablabla blablabla.supersecure www.lukas.im blublublu blublublu.supersecure + Responding to challenge for lukas.im... + Challenge is valid! + Responding to challenge for www.lukas.im... + Challenge is valid! HOOK: clean_challenge lukas.im blablabla blablabla.supersecure www.lukas.im blublublu blublublu.supersecure + Requesting certificate... + Checking certificate... + Done! + Creating fullchain.pem... HOOK: deploy_cert lukas.im /etc/dehydrated/certs/lukas.im/privkey.pem /etc/dehydrated/certs/lukas.im/cert.pem /etc/dehydrated/certs/lukas.im/fullchain.pem /etc/dehydrated/certs/lukas.im/chain.pem 1460152408 + Done! ``` dehydrated-0.6.2/docs/import-from-official-client.md000066400000000000000000000003431327017064000224070ustar00rootroot00000000000000# Import If you want to import existing keys from the official letsencrypt client have a look at [Import from official letsencrypt client](https://github.com/lukas2511/dehydrated/wiki/Import-from-official-letsencrypt-client). dehydrated-0.6.2/docs/logo.jpg000066400000000000000000001244301327017064000162320ustar00rootroot00000000000000JFIFHHC     C    {W.:4RjçƘ(Y[6y}OwN!,1 0w.( (FR9@0y$VdĔY RAF<D%c)Y(x>yy$bd Dz{ 8dד8B̚-neB\+DCf0Pufey%HЌ2#y9(9DujˊF:?8;Y)ECu ,Й^ @FhR0EŇ"4f+œ-BMwsshJ#cZ]9cȚLZl]ngao( h$ Db4ms2ʏQjEo:Q:LlIz7`ٸEWWo6,oz%!:Zsu|.a)@05/ 728M8v. O^WI:$ǂ&w:~gr8.6cgs|So~?;7ry9L8]ٞ7nXaĐUPqetd1GHq= _$k2=}?szXqWc_w'Oi܉O>7sI]. Ggs%*X/<0J"ĊQȺzP0Y20dDR4##bCKT?3roc4{o3z,->kuM':WZ60e}(_]7Sžw.j.k;by AhnBHB3GP^?zN ,(k*Ƭy.ye,1Y*u`;MOu3jg_)2VCvZ 9dc@rʹ`fBh4(DڥST[I<YU,-Zn =;qL(Ϊ4ut;4u67o&[wg\8c k!(ڭJ1&E֜V\dc3~%'Ē]ufwE&t2wھgYڍIKw[I¬6Y[s]c꣛G0[_^tr|,{ǁʞ%E(O-I (D" 2J;VyLz37W1r]z `u M^8\^{jl99mWaݾ+8ӵO 'rա F,R0'-s¸e D=+A)(:_\TniRԮu6zedó9mby:)zkҍ {&┌I*7I$D A"$$r~W4֞gՌV ?b0q)yjD{<=9~{ގx:y,@EY럹il]5^O0(1 ITt4@2Y#2<%UbJ%/SlQVvCQmz\3Tbqi4MW>=nd_7Мfx[wt Ϩh!H f*oA@  7,ArbA3fy` 2ڸ{|Yr/:tY@|;zmk+=VS+gHfH$N9#|%'q愢1䪣 'ktY?Ok͵)n) ^psbgxc7q=G igO,Nyaꔶ۬B4y>EсRf +R)AseKqV0H "eZD$3 hiz\K3}n3z6񽟢_ʈәr8~(ޞQWX$"B1b^9["ya!ءҵך,+Մ5͆[{h'[}VcrEu?[ը{G/PiLIgR|g4v[EX$I‚*vI B# %sWw.gv[CdH]P8Gt G;<=Yy;5y;)[HY-Ա ŀ ,b1@R#2(s>Sت$PU01݌ZB~W5=W,5n@65k˭+P(ͶCnκl YOQU %VGp/*cl˪*~QW!p$!`fP0XȥؔEUkc \ӓ1{z%ǶWZ7{;X2$4!(&my=Wٖ+9uyZp)2c`v=TjMZyYT,d c3i'MFƣ2}|xm\✽ݿ;YC{3,a0tjCfuR-ᓨh܎S,2'yO0ʫ))ym:*ػi#hB%ĀQXF$`*67JXy;^3tF"Խ7f38bHeeT ruzJބN$ rRq x!fJ$U 94Y?M*/N%(0"H,R#9U>_e>{ԪN-+[I[e.uE4zm04VX4;N(款nO|/[yՃl\뮇*XD2!1A "2#%35$406BE81cdg0E:r ^r0p0&(GiDNv!ҕ6X.:q.|CxDC;sr_n༐RPGFFp;`m$pK޶f9le1iF%{`t l]K#9>Gf  "9@cFEk#d -G<3@WN>I E,LJJ^V$zދ}sLGLU-JT99""/#H .'isW&$\܆p2<\J,(3PDS! #4.*eܖ$؈zFʤACL.tbđ0vcOB9$ E5с(? ee]:G@1d YBy2 f"}eڊ*Z 3K%FK/PK]P@U~v'-P0fhhJl0:A; ػy890Ip@m!r ^>&A=L4Լt(x-\&a0bȔA%5Qk߾J N{g;g"Fp0<݌u NQaJL1:DE~CcGԬ4 i3U#Tm:hD]9(il%4~͌m z=]9؏c1`ve`w%igV؎FGpfO0N%6Cr:Ҕ!ܱbP )qm S8|"4&"#E]®L-0\!<o\mx9"rTcxiT7,GE߸rKfEK3t%W4(ERI;K1Q.`Y"ʈ6# X; Q\y?gq>b$!k3~"E@lGNT-6CK3Ol# iY֔/`"۰,4# 8lzh5(7aH_z5BBA+;m*$ -, ֬qpul7Pΐͽ۞)[cqw",y&fڝBM*%q=' ;5\qtpD8=Xݺ?Iwg" 9d!YC%$ gpc3ȑ5 'Qu'KJ\*tc"m-*}.Mw >R=6H1cr",\{7[Hsn gLځK"Bry HrgCCLy9V 6o:u:"MV{[xJJr*weMQ$:T ~l~Hr1:A ?9"\;: ʇՀ$#j!Xl_k08j}sc2U_L6Rl-lS:'Cݡ5Aծ@,` 4(-1^TYm i` gw'dm SyvԧO.LNufo)nc,BDt)M827ON9j 92\tFJ 'u3YƘgclr20;,?G`cd_o)Myk|׬!Lb%[H&\[.\)IO؆&z2vH}NUVMsCUyjd݌VJ#8#ۤxGn: J }-\(z 'Z,Ȍa$%0^WIj][Q1,Xk1EDZ;<&hzދmûՕjdT&$hغǶ fI6 |`w#ʗblՓXML`ii;Zj,WJ'vz/AjX~l܊k| DiZkYk FTz\3,$kOX>jqcYk &Y/Q7ZM<1 cֿjTlJ܊܅u&n|?L7k'X&Z:EВ4:vm ~JMɻQ UIdhVkISyn*|m-]*G#e]:yIູr= *ڡΧnbf54lN ڹzrqJS,VML$ܣJ}D֠Kx44ҶG[+B%lh(s=^y13 ^zOxݏrBH̺ c20#|YzOm$`@5+3m {`OFԌ!95]tjK7Ƀ>@ѵO |O{k]/I? [ZSP|֓IhwK#2oJ[=k) 1w!MN& #9Ӂ0!`x.Uݻiy:eǘrS.SFu6Vݖn W:Hý04Փ,hc:[f'Y׬5/ˉ)#/gҚ"Pv(X=ŪeN]tV$gu<{T3!`ې0]H)7b-X"C.&Nݥ]x[aV8tE0r6cWFrR郮vcdc D0~A:t+!f9#Emr=(' m|猃H"<{~[P I" }*+ ,#|S[tQոa'QrȡԈ;񮏯L]un|wQ[j1%*2$ 5B)P](7P31iO̦ϩ[:I VdL:JMRl lmJcޏۙe 쭻J?jVѤshtoDbQtda=88݆Dq3Q!2=0KڵYOo@\V8OP|=mnt0ւu4NaZP~lDzHv2YG9Z4EjZpJ񞑜cb< p:Fj;+ bD_qWAR]_3m}!N`!~&{8&$$`2:8?a7EQI6LZn(5jzRfT,OzH"-IGT V0Vi<RV%a(7/#$ۨ-ICۤz)`r pGO| e.QO O[ޡL~dj("76e2kT}@1bE8ʕCBu}u`6uR@$_hDC!]JKgPB[|x f2d Ei 3pcڟ )Zw>H9#'Ӟ#~U‹%b5V>ZRI¿ %= GSy0yQ1!1 A"02#3Q$B4Raq@?a[r@URQ:&U GW2V6|.{/J9VTX=ĞÑ@졪5KQ=:OEהԹږ$iZ WD@+"g`_YlJ.f#tZcj[K.p&U9 =[?|G#m˯cK4쎐zwzZ<(D+KN'`d{/V; }+_dUMavn3NZ["(DŽ#w"BNf'\rǭ4my6Wto)Ռ \@xRWpP"롕 ٫P_l:; 29iCw`D:v)m D#:r9 s yWQ9B-'N<'N9߿bAYxG핕Nq)Fz+Jr'CFVLq/R0--)a>M#X7N N+Se4HPҿw(0(.S!nv݇0߉lТ{+ {F7qEMG} dM:!* !!N()4S@+wlxQ,4(0id(06F=J(Mh+=1=S4NU4iM1tlxD pr Z O_kA#AO"kEޡ»GI[v%C;&j VUYk\wO /-yQT`;+'ڥTN$v >fzhhasl2muQ_'SC7t}(tPrfΧ~ÐWA9B%kdM9CQAR]ER|_O^X^H:w=rr|B Q‡kEޢ? SX!GU&ܹQQn$rTo*͡EdTTjP*Y*'lSJd+fBT-7SQ6I*?TwMv ,>i<*|eCpM5>@tBGhgr$>aV&',PiF`(^ TF+Tk۩& fW֙ / Q+5z*Y[V>Q|/ UzF'#Jn gYQ[^vVNXOes7A{Jw]fO=!KZl. xP` >!ӉTUj p:ÓiyNFYE@H7SJЀBw*ZF O)NbNn4fTQ)dwX{d<+SAA7V&=\¥mTtQ9{ *зSWRuªDiT#jt_%v 'I$BOG򠣎 U?LA>ZdnU2&50JXgH]5۬@]>眬et|?Ġ'INQ.Qd&nH j&H Y:il\x NkdPݪNУ)(*? t,x@ʶa5+^TH# ,E~U*`,uUWXFU84[*f=ӓ]zWMhAr JtTquR?f~!Kp p$4ҙXUKRYu%0) m}hvbRePnrzEc"VY-%Ofа*Z)-Clc3eiR0ZQ8 t@~3 a@ :f5͐)#k/a[v)Q >Mr-;&Ђ/ V)dő*j5$ƬTʮ,,*kbJU{kYPqDo*ZxXϊŧtȣ#nm첃&8HRRLQzP3y*57US~X5ָ졤#uԷT#rI6bI>E\V9F5<6ݷxE xT P(UԠ Q5䦣 ߲TFWŤp[V7ӽz7x $\l7KS 9KSeW(u"NYS=|\g%LmQͨ]Te-* .=Ѝ6-kxRUG (ҝN-j=u^u^SKa7T4eR7mngu~dV鱼Sh97_Bq:V/VGnQj!yȄAp j酤e\/U;Bl ]iPT"v!N? cVn̫VXE' w9ͼ^r|-KZ'.s#ɶ*"g]Yw6 :5*|WDL< hM']2w%}lsKj]ީhYѦ6!U 8m`x`lW+x@*\=AI¦rݪ`TqҵwMEɤ*R1;1: SɿfOBueee&͸_0!1 A"023Qa#Bq$@4?\fB-P*`:r\w+P؊QV bdpb£$I^˗.3W\q}*~ "4N2bcK$:s~GSLeI2C' R D\ r.zJNQu%_uI_$UݐKy&rF|͐ %"eQ45 Ybȸ.\slT¶G $\ RK %~~ȻiӕK؎Z%#aU*ksu E!XZƢ11d:y>^KJ74s/sP'QTBL_b_*ncȻ`RX-*v位G'-4V%7U!J2W{ȸQ<5<%-rTNĽqERdr!quGQR\VիVn4MѧVfS^Jmy-_Aˁܔ*WRdR{>"Y"ۖREGY [,p\)I\Rw*W+ZQV2d(м aVZ%䖥JS&8U,|*o/i7*.bHr'i4\hB\98nHO2Jj1lUJZ+bj]C\iXM+XhPt%bV> %s˗4FDE٩\ȹc&ꎠ~JZnztG<#K -5/?=i>Y㮔ѽX?Pq9FDQߓZס$l\cՏ%83촍J4/S(Z:EGRi/Y \JZh')R<7hQ+wY͞Qa9"U[e&XjJu&Q"c*$7Vnjbtޟ$i+@r٘8)PԜg'db*c ]S"i|F#uиŋ5Q q_i867Br'F3eOU)T1&(17! DQج]"?"NXbwը? uuH?$FOUWom{OŔ.-ùb1SpCW9~ jG3O%O?n&1T'^~J'K9\"r4P74<ؖc~ߞebKF'玿Gbk4SI>L5t3&j,CsEˑ@!S"6P؋\ >rv?o;|3TBb&'UjGDQ*pQ(km1]4Hė a/hc%jgmrG_4F8,̸̖Ķ?ȋ{>G4FFiQR:J\2{/Ub{ ǿMDP^EJ1:b㸸d8c߂)jV'+=HвfhdicK,F I\#'"Rm$JLy&)qblМ佻p?rĄr!] dhK"Dn>HEc>7]9\^vOK؍oȥMSfE#EB;2OXr*Tj(SIF%QZk,-sH5TܰzHPhN͢ZܟۉZV%Slc۽b]1v߷R6*םRLز!U"HXHϓ KTȲ>GG#i6ʒ؜ ˟n߶.2PVf5IВ%/rS&4:H{2.ܩ=NN2KleN_nRBDq5!{"8rJcM Hx$, \ł:PuT 'ȸ*lr^"< |rwh?e+J$d'r),D,d?#ScɝyS0 +C*eb>v-5|dŒˌAHm-aܗ9KEQ_;#hf+"{1hm;.˲˲.˲MعEIR$!'B"Ȳ4Ȳ4J4bDLn iEdKes}?G!1AQ "#Raq02Bbr3CSsc$@4Tt?tmmO2G7n솓e஢C읊 ˶͋N!zܴq_I޳nY'k7>8e3n{r!f@*1X31D?U cvՄyJ^ 6Kg~oA:^5i*D#m\^.:ǫMf;V,#E7M^WSXj G>e+mKrow麹[sq;ӥeܣV{EPr$WzsV? | ?`o-?Hg@k[ gawO!tEMh5w#xS?2`ֿ!UcW~Ʃ:2;u\͟T[E7 wi_؉cFQ#X뒇ٗވܛtex}O/ppF;s!bnanSS$qd?yQ=:-]Ժnn֝dK)9QݺjVR[yYNwV|؏UNƭ~ uԲ>h'&A%Q݇%;6noi5ʊ74mNnWcҙr%Daq,ҥø䉟\e>VM}Q29cogyWq8 ھdkD; Ŋ0oܣ1rS.{9[¯aӧ&pT)neқy= *S݌JPӫ Yp{p ǹI+A? }Ja׸?VƻeR2*Y:g2GeZrB7JW`ʼn8{lat]{l+Q57{hT>_5,c|⤥smd'LpȌ $`IN|Ov }v,X#8w,Ն|t>k[VZbj)g.4΋ CקC4Bzi-C-;sT\޾_hs>,CfoSc+ H:O5 pK*WH<:KT ly\>-;JWlpVVr͍ܩ)5cpqyFsAc xqܠʰ˕Ĺ@\?T"5_iLk;-eëk {e=3vIa;,ځd:;rfSݰ5QwKϛb 4?{yxX5g7>N(.NwٿR~k#E5MVm|)ca~DcsZvvXg*iMwɟ?R?O7[ar*eVhNm3[ȹXg|1`.M5Tz MS (mݣdVYst[¡|a:';.P|/_F 3nlL&23g*5 .M6iUpcGCw H)(lڒM߃{C e 0#Hd}S; "o-T28ۑ7ު"4Ѻ͉]QT}\Pes/db4aS7 wަ-%]%Ly 7#&~}OXwcorx%ʗ^y8*;5 N̯I#cIZT~upL6y`^5J3LTF: sGgJfN06crMaF}9zl³YY-hMÈ: KnKNkj}`ܠ& Ŋ3eNֆxk"'jљZI~+X,Us؏jZ2 -W@D$=)uM%-K-x5x.xu 63wQRFw"ʟ%gA 68Ƌ6l#l9k(;es\n5fm貺? 1nJcKa{_yBFGt#t$9qa ŵA1k$z;?<Aj\ hhx #feR~ukG>jGYSm{ _p us)ܭ gh͊t5%> szd12` ;،}yܭp,kUv=ˢAw0!RɸQkpsv K6f&uAwZ U7 'G(f7.DS2o%QA]F捽Wͫ14׉Tdɨb9ULfy֍ܱ'CWH?Н]nv)oGy( ##]ctnU#]l\o䵍yc8~(ʋ|-$2-Jgz5D`=Po:uLm kC@ϛK -̫DNm!MECZ 6T8tEe 6sCz,?)]Tw9t,V+cI+>gYB~(nL&2U7%cŧSNZ_Fk_(r^?:16푞 +ui'10jS0wygPcI^K^^.O.9Sٍ@܂d ]&t.+g3bD|م?}HaG[,7T핍{{ЖtIbYQYzWѷM(s-̣9fZZTyQжィ8,L;¼ܡ?#7SJ `b;@apb;Yͱ^ ߙAP*5|ςî_ âK@aus%`c R.q :tH{Hx UZ9:JX7/JUĖJio? F9xQR\t I. 2f8 1_Ž]EvU"a:' fD1 ڽ# ǤEseqgNH!E mzo'/ iYn8RtJlօ_N 5"]7[%z# Bs+\EMe~יgO761x9Q'ЉcJ8eKc>RQvQ&͓ٔ&%I|K 3T6UĪ,I00<ҥAH7CNJ٭G-f}5 T-=1%P%]fd@9U=XC\!`U_PzܵZir'GdgZ&Y! (0L!JĺVN 0 -CwCǰ*TYGSN,&1_pIm+LP@ cwa/h]~į(s^f`̺Ɉ~_Ef;ab4.]mLyJYJ:\eWe.%} x7;Á?+eWBlqn1l>qjs^AGf@L!y ͹hr 0jj,f2HɈw˶cp%Z3 '?E`)RtGBQ绀>\0*ESˎy'}Ϲ( 'YFe(AjFF ˄"˥_ptVͦ L?Dt 9*=\ Vط<կ{as{r=ƀT\2,2vrO\:*ʼnHOWԬxª4ƿ_  zs /2^4c=C4!M"`̻lĩko5=;tx~|$۞L|Ceʬ}T j[1AK|/ w5PV|RpqPn20&>ٍW sb!ヴŤ?uZ"ES//6}VÈR%,w\q?ܾ݂V5HcRݟDoA\MΗp8u7e Iwf3am0jjYWgSq &FeO0SVj rڱp .U4]:P/ )< yt,c Kɴ/UUFˋGdq~ L:[gCT([P+:3۹@y 9KYhapz.ո$g22 Ѭ|Y3p D V4[~<<&ۋvՃ9v:a%R ڷP7|>5q`U?uis5|rϾ%j BŜn,(*)J7_(ST|w 3 TFU7W5(H *c,Z$q0F!*×yE_د@+Na3:bf\.T'[ "`-f:oUK͟pp7m\߃cA296YQhx1}#KC5*%*v^yd ]bc؁WP )# q 5芯)a4JZ_iXR4=+S6a7ThVX/^!/zU|ݦcZ"@ ^[L7,Fg@u|Eʵ ^?0]5ބR>6(sU+僂~)IL'u[KxZz% t\ϟb2¬6 B3SvtP^D0.Hn)Cz\jKixтK% oTJq)pQtC()MKAMOĺ]̓ NjyQismX0G3wE q F[ {au|K7q1yE %s߈]&qY9_{xGDu44jU")$VNqs5o0Cqr1^%Qj5EE/+wyf(TP">‹;1PQŠ?,@_ pI6CDkT5^U%W~btP?q>iu*WoeDbRrqyn!#RNedWU3L˥T*lj)"2~jhq2yp| ٜNͪ-ۖF01+2IN, or%Q3*ǦZВ 4*YYXCV |ۚL E[|ʆ&}Ux- B(r0`Dm\PPb½ʧ ^״ .N%m8Xxw*TW Z}DS}D,Eݵ4-Sps{EJyKu`<8f?D|3ЮǖG@.\ˊBa˂+κy̳v]+*ܹhEv(6\o2;.cS0iԵ3R;RW.} S0PP! yE?3I9y[0`"3=ju~Ji!Dc{X^N$_"klY } 1m$==ũ+ Bu,l/]*7,jE _DO|X@s;M:x0]0aB;$<4{SGdJ+q3<95յ4١9  6Q1wNBBSFޚsve.gE O uJ;{(q/}t ^7Vuv0bs{#V8 D(' 5 `Y{>Ig*D{QfyR`gگUFV[Bn[ '#x$23[Pb,F1cMp$d HliZީ5kTZ+-m;}H J_RM.]n fn˥ח}̌AYZ6Ћ\;XyYnzK^oLB_ iڃfe}^"ẙ iѶ釶;@A[D8zi3/%Ƈ0zI-֡(Q48 !2'!1AQaq ?&<aqa^!٘mF9lX f6nŏV/Rd:;hI`C̏v;g9+ak{W̡Vf^GTٮsM@~h5:s8S}۵rπcpq.(9tCL`qeOIX2l Ën}_mu >zZ%2<1v"fgRٜڧ|SP<dAd,'y&BY :\ Ӹ.HaOvxdt HLunSA'/ ͹xxiJb[Ub$4e1?-#  3HrOs6h&ٜ>8ͺy%ı{wg X6Y:Qyp6:5[݌zoh}̃HrE7χ0-VGeSl9-yLJ`'/L%m-dPBՠ$< $2{Cy{!=XH-l/cX(L8XLPWM|YPO`L9М>]3pCrK|y8a.<>:".Q #}#Hqz( By'HۤiFXb//9||A@{ry>6㇂xhܝr\> npĕ[w?X=L';`$eqJF&noKM<ߖ׭AD"ߟ{LYsqr1PO7Ha$$Nl^#3-z`D32-C:zdvs`̴ sfSC^ ݗCn%<3pP[7r͘\*el02~ Ț0瘬 @Hن~Hwfma.[+əsIv$u[μ.]l}@Zo,~e,\9_6 'ʯ?wMMk1~[=gƾeyY S| 8>܊4Z7nڳcQq`ʸܖsʘ-Iwe3|hN&xRl%yYŷ"l9}L7>=Jl׿2=GP;&KF^|8 KȹGsoP~ rHc^md#XE\[9eŸ=N-ռgr /9<[i p #iiԽ|oCިncN&d셁<1#-uоN3-AX9|I^QCҕ"Q\ 8,h@l˖d7%.Su=$D޼2=~\"\C]JAbA;fs3q9{_An:/zM-LcUͼO ˢ~hKwG,˾_z25 ĸl- w6Bg_'rdž"nw#`]}M~Pq& um]O1~1ɃȻa9y`Z$ 6-&S'Dm miG]QW-˃y|so+F]}E:Yfxx`#=" [Uhp+Ē06ul=Eq1^l_mɟ!Dw9k xL2egVe@2  0 ##C c$h.L._qثAԾ``}+[}qf} ,۴MdS6l\&!1AQa q?o>>ed;3댙^2> :/q( Od%A&|s{fytp\@A/ s;K y+ɇnP<aboO;pux[rDå}ޡWOm8؊UqLˡ'Rnq;RZcӁn\>a^7w,l`qOٗ& 7 6Ru:ThE-܌ݼ=f/q7e!0 be蔷݇:#mQ.;<tÄ6~HO%Lpa L-4oPYyݏ8lk8D_n|pnYKqlǁv ^~I;7c uծˆڻdG"ٯ^xu mR/ޜ.Zf;pAݏm3.[m턴mF 6t] %|} 8'%:m -0C׫r<[Ύ|G[4I53go77a=bۍ[ "ݽIo}W_+3#;Df<#Oŝ0cGHz.+2ͣ3$laz((Y G}EG?/zHr>&}=z^Kx8'S=.ܱO4nv0퉿K?%N{3I~ٍmV^M:f+x00]܆;cj40^ x.C5\; Q̃fNu#ۅp eݿ]Zo7aOd<ڃg{̧7r3azl] &bդ,Cu?u_/qMpdm}:HUݰ׬K?P%q^8_?HY )/;y> "&bܶ"e!m< CvGqKZ_0ю.2/ww]͇gcC3k{.5rf~kWh[:-+[ -ޖu0yx:-!Dzkuwda-{fV7鰧~-y썌=:?cl;Մ-݋vtp?ξ8 N&Ad6ggo&jt~,@"/薷tQ<31GƟJ1kSc1`@!)(-ˏIKٔ ޕoWK~ak'f9wgr Ւ7d~ϟ# NV#ꝙ< oHP;$Rw> {&`Ƨ?oWzn\#OL2w| 27|: ^xxR˳gVh5;tdocS<17imO<^x[-O3 %LI 3grNHR<gwte< {g >_<ɺO`[ ,n?fka^ܓ2ݧ?0FG@[؆GOL8OWz3GtrA&c/ ПY{r${c3O~_"A?xIn;:m grl,Oȟl& vݖ". ݚM5E^ >u=^at߻ g<ckv "BrQߔ>^CܽG?.V\-f]fq 6!XHi?;:lo:['q cl쌒C"txt$'x`H vǑ ' 6Zdؿ$ {A:!;lyF@ 뫨6%=K.nGB%;uMG@yxm-Go./BNϰoQܮ-55X{uozș{f@ f7Cmw#6w~dӭї/cK< Ӑ=&xN,=aьM)p`FZ/`@xjYc`˻|Dr[O%v$ȧ./O,߲?$0Ȧٞ9l{kp?%!1AQaq?[YW" =hVLtpuPӛb͞rױޤ]!ĕ45(*k [8j(ÂYN&FLMǼ <#Wy$qte!C鈎y͖o PxÞ19ÇnI!G!f*@Nwp v\5VsTű[?YN?X:&uX\4.P(@'HvcՔٖ15YޱʍS)ͱ" !9#TUߌJmo(0V\L Lۘ[-vNrmeeLa>Fbh̆Hv<*qHYSÀQRˌ}Ìl&Hyo8*-wONL+? 0.H?x㗛JÌ6?8]gGf?h ibK }cè@ <+y^ZW4f^| :#xxTy3 BszaL7q!|yQ^1O-d9ʂK6x+% }*=>1Rb p^c/.Yj"q)۳1l.SnVbUقhnԘQ5"J-lu2pmBMY6OġwÍ`v6KA]'SN[k@sI?  :UL.0Aifr% +%م&X}d_& iL0x#`W$[{5K*KCBvc=ݥx4`em8b,6{0C8|)8 [񡲚*ڛ@hArá#0T Id9K2e,l.Y! 앸0<=bRO8ىrLd*({Q g`X uVF" <˗ < >#pZ`x7 $CPrEٶd 5ȼ7ZC(ٕ!a-4ciCM1?O<!2"6KYVt➁\{["6+S2H||%PnwΜ(LI>pZp$SJcak*ɜz_;&'rAzP {5v4FH/?Xbv63:0:y}@^n+W8ݬ$?&4b}8uμc`x3S;8oA}r]𮴘&W As0T&]*޲X5!47#7`vbxJy֬AWG ) |Li fHLYA*ɻԩ`YVN=O'MW!1WxU#j{ˈZ $!QI6l>rQ6kss|-x->J MtrbG:a~y<ȥx`Py; C[NuCׇ\e9#5^P`A;5Z۝ n "Ls|r8"zQWr\Z#GC%/y*$NɰM3χBPT`xq(j~Ё_~'z8ZɣWt|nD_\%"PjIZ3q0p?J 䕑ЪkrC%\ݭ~L gMU_Q;Ѽt%bsLҩhx f=DSBS }njƮcӂE_k,֬.B9}a[TmXM*E؜a 뼊ɟk<*CaFM`[׌+Ʀ%{i1s;1u s^~434 J9ӣ\C>]W͢q,8wo72E_#,,׃ȆA X֑S 9#%<ER'@] JBQU#/qM@f޿V#eK6 .'" \eX0wG:5OZq߫ss3PO\JJ<;UǢ2wRx /P0kO7ȁ )J@O\a<#÷ ]gKʺS+DιX+&_%# 3jhmrd5ŸpP'Rΰ `x_G+cI2i(RW ^2lԮymCA Bfp2Fsp _VRM,ٽtMj0b9>+(o4rxkA&LZwӒXA kh   F&YPr 3JA;y S/Ûzq$D8@_.$&B bNY8(\`kXVK5ho;Y esr~'+i\5x&l|=dP~\x}~p|Dk`$fQ!4gW%ne&hQnj;UYBX8ޕ9p~rʯ0xwj|N%iuï5N'>pLN>\c |3mjxN8 br]La-,0%ngw_YQ~_.~s^}$?8`C偡}6Ϳ2,Z> 3|*ђIoZ.A = C~p)*Ǽ7 fڦ4, L X(Og ºQ.|Z[tC8*F~ptMdsTBv@ːQz}Fo NŪ&N`fR 8Q62mAcf}cx4ebg!J@xaHPo] KsQ0t1ܿ x>܃_+د!ϳԧj0F3̋?9g ?fA 8hW#e K:boDFni1Ȟ)QOEv)0vVhd'_(nxnjs]xHjphԥ|S+3Eg-`Ќ[hﻀbG S$;0uf$$)= Po<`,C Kx== xt 4U8۵붮=k7al\w7@gQsb/:ӏ3(ɇzɖ#|=v+~p?FLE&@"@rO}ڍk(}/shW= "(9M(@4R*8V eB9W篌ޕi9ƹOYl?(pG&]/A`]=9ޑ # mCJ( G4Q^6 =v2ʅ"]ػ;l+p%PU>p7"ٶ)j0' s `0$9\ qYm$.;p&G59Fyx17\~u 4.pf(B/  C|9pQ_,/U_m>vcGĕs6 󍕃ӎbK,U.+b-bU ܭLvܮQNu~i \wc~È0DnK_r6)hۑa\GLd u$D$>q%œ:`^L%fR{50!'0t&p^l:^Xlq@#BaZ@zV`CC< FbxPb W"iBȼxʻ&&L\U._l5'9B:^LJsqDO =0 '9$}/< u"OPOiiXK%syi-+tqrn8[ÈDozɟ:b=bQH*#J]X54oz-mVxѝV&ȼR>I551`yT3q ‚oSx8]Oe;]Ore/gW=1 U2 ʒ "]pV&N*qË4@/'*MP^sa呹dyW YɁ.Dۛ8Pb :p !ރA0Ą,rc 4  9 sP96O gmb}y=8iNPDX^^?9 m:1cyP%mz2rx#F7 RrOOCoV#(3H+`:pkB;@)ֻ1Mw(єcx.&*nr<(02^ʳq7;Ϯr“񼃳 Àt΁\_y1uM ryXH>#{~*2T+ZqP|*jX^9JLa12%xGEAa1-0ĩ^|DRN]9 ypøAC8UI59xܒ=bؙ, \ D u >):2uawW^`eSmr ZOLNt&OHV#K+1x[q %]~qtQ2gb>p"e^D5PN$$M/x@ik<- ct^}e-q'9}q4IDG44`(t5t̰7͏6U/28E50G tNl*`s!&Zhlhƻ%eBy5 ^&74xr4raZ=o0޼C!#b&2]vLJEƉpZ/83pzI!h\pMAiNjcF['jN.m%E"rvY Ar|LuOl.inp>1ˑ*$!u ~1xc'7y2K9WޱqÁ}qʊƆb:xNsD,Rv*^#U󼁇LIA*@s[5Ů !1g j]y1R;r(GƜmLh)¦˰Ai>d.оn)PҶP6Zŏ/%)1j|5-/rvTwp:N28S&HGd;ɖkWØF NYLLMcE[8j"I>^' AU?([xiof%6e'"%_>2.z:2,ޝ`EƁ5 * ur2~0.ӀآnTaay8%1h.?3K8O(Wa$2o 7`IY+Μw0kji>JPMo7fڍ [VZc+178$c~xj)ȍ.t}AE "-ף c*GeTsr{fwѰn_%Ы^!N7NF:ێ2*1MAyikF.ck {8p9xR(NWew`VKBE:(.LEF6Bi!b;pT?8| Options None AllowOverride None # Apache 2.x Order allow,deny Allow from all # Apache 2.4 Require all granted ``` ### Lighttpd example config With Lighttpd just add this to your config and it should work in any VHost: ```lighttpd server.modules += ("alias") alias.url += ( "/.well-known/acme-challenge/" => "/var/www/dehydrated/", ) ``` ### Hiawatha example config With Hiawatha just add an alias to your config file for each VirtualHost and it should work: ```hiawatha VirtualHost { Hostname = example.tld subdomain.mywebsite.tld Alias = /.well-known/acme-challenge:/var/www/dehydrated } ```