diff --git a/README.md b/README.md index cad2775..cb226e3 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,23 @@ -# getssl +# getssl ![Run all tests](https://github.com/srvrco/getssl/workflows/Run%20all%20tests/badge.svg) ![shellcheck](https://github.com/srvrco/getssl/workflows/shellcheck/badge.svg) Obtain SSL certificates from the letsencrypt.org ACME server. Suitable for automating the process on remote servers. -## Table of Contents - +## Table of Contents - [Features](#features) - [Installation](#installation) - [Overview](#overview) - [Getting started](#getting-started) +- [Wildcard certificates](#wildcard-certificates) - [Automating updates](#automating-updates) - [Structure](#structure) - [Server-Types](#server-types) - [Revoke a certificate](#revoke-a-certificate) - [Elliptic curve keys](#elliptic-curve-keys) +- [Preferred Chain](#preferred-chain) +- [Full chain](#full-chain) - [Issues / problems / help](#issues--problems--help) ## Features @@ -163,9 +165,35 @@ Change the server in your config file to get a fully valid certificate. dns. The certificate can be used (and checked with getssl) on alternate ports. +## Wildcard certificates + +`getssl` supports creating wildcard certificates, i.e. _*.example.com_ which allows a single certificate to be used for any domain under *example.com*, e.g. *www.example.com*, *mail.example.com*. These must be validated using the dns-01 method. + +A *partial* example `getssl.cfg` file is: + +```sh +VALIDATE_VIA_DNS=true +export CPANEL_USERNAME='' +export CPANEL_URL='https://www.cpanel.host:2083' +export CPANEL_APITOKEN='1ABC2DEF3GHI4JKL5MNO6PQR7STU8VWX9YZA' +DNS_ADD_COMMAND=/home/root/getssl/dns_scripts/dns_add_cpanel +DNS_DEL_COMMAND=/home/root/getssl/dns_scripts/dns_del_cpanel +``` + +Create the wildcard certificate (need to use quotes to prevent globbing): + +```sh +getssl "*.example.domain" +``` + +You can renew the certificate using `getssl -a` to renew all configured certificates. + +You can also specify additional domains in the `SANS` line, e.g. `SANS="www.test.example.com"`. +This cannot contain any of the domains which would be covered by the wildcard certificate. + ## Automating updates -I use the following cron +I use the following **cron** job ```cron 23 5 * * * /root/scripts/getssl -u -a -q @@ -353,6 +381,34 @@ key (different of course, don't use the same key for both). prime256v1 secp521r1 (NIST P-521) is included in the code, but not currently supported by Let's Encrypt). +## Preferred Chain + +If a CA offers multiple chains then it is possible to select which chain +is used by using the `PREFERRED_CHAIN` variable in `getssl.cfg` or specifying + `--preferred-chain` in the call to `getssl` + +This uses wildcard matching so requesting "X1" returns the first certificate +returned by the CA which contains the text "X1", Note you may need to escape +any characters which special characters, e.g. +` PREFERRED_CHAIN="\(STAGING\) Doctored Durian Root CA X3"` + +* Staging options are: "(STAGING) Doctored Durian Root CA X3" and "(STAGING) Pretend Pear X1" +* Production options are: "ISRG Root X1" and "ISRG Root X2" + +## Full chain + +Some servers, including those that use Java keystores, will not accept a server certificate if it cannot valid the full chain of signers. + +Specifically, Nutanix Prism (Element and Central) will not accept the `fullchain.crt` until the root CA's certificate has been appended to it manually. + +If your application requires the full chain, i.e. including the +root certificate of the CA, then this can be included in the `fullchain.crt` file by +adding the following line to `getssl.cfg` + +```sh +FULL_CHAIN_INCLUDE_ROOT="true" +``` + ## Issues / problems / help If you have any issues, please log them at diff --git a/dns_scripts/dns_add_cpanel b/dns_scripts/dns_add_cpanel new file mode 100644 index 0000000..24a1ca8 --- /dev/null +++ b/dns_scripts/dns_add_cpanel @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Need to add your email address and API key to cpanel below or set as env variables +user=${CPANEL_USERNAME:-''} +password=${CPANEL_PASSWORD:-''} +url=${CPANEL_URL:-''} # e.g. https://www.cpanel-host.test:2083 +apitoken=${CPANEL_APITOKEN:-''} + +fulldomain="${1}" +token="${2}" + +# Check initial parameters +if [[ -z "$fulldomain" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi +if [[ -z "$token" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi +if [[ -z "$user" ]]; then + echo "CPANEL_USERNAME (username) parameter not set" + exit 1 +fi +if [[ -z "$apitoken" ]] && [[ -z "$password" ]]; then + echo "Must set either CPANEL_APITOKEN or CPANEL_PASSWORD in dns script, environment variable or getssl.cfg" + exit 1 +fi +if [[ -z "$url" ]]; then + echo "CPANEL_URL (url) parameter not set" + exit 1 +fi + +# Setup +request_func="${url}/json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit" +if [[ -n $apitoken ]]; then + curl_params=( -H "Authorization: cpanel $user:$apitoken" ) +else + auth_string=$(echo -ne "$user:$password" | base64 --wrap 0) + curl_params=( -H "Authorization: Basic $auth_string" ) +fi + +# Check if domain is a CNAME +res=$(dig CNAME "$fulldomain") +domain=$(echo "$res"| awk '$4 ~ "CNAME" {print $5}' |sed 's/\.$//g') +if [[ -n "$domain" ]]; then + name=".${fulldomain%.$domain}" +else + domain=$fulldomain + name="" +fi + +# Check to see if challenge dns entry already exists (update or delete?) +request_params="&cpanel_jsonapi_func=fetchzone_records&domain=${domain}&type=TXT&name=_acme-challenge.${fulldomain}." +resp=$(curl --silent "${curl_params[@]}" "$request_func$request_params") +if [[ "$resp" = *\"error\":* ]]; then + echo -n "cpanel fetchzone records failed: " + echo "$resp" | awk -F"error" '{ print $2 }' | awk -F\" '{ print $3 }' + exit 1 +fi + +# If no existing record, create a new TXT record, otherwise edit the existing record +if [[ "$resp" == *\"data\":[]* ]]; then + request_params="&cpanel_jsonapi_func=add_zone_record&domain=$domain&type=TXT&name=_acme-challenge$name&txtdata=$token" +else + # shellcheck disable=SC2001 + line=$(echo "$resp" | sed -e 's/.*line":\([0-9]*\),.*/\1/') + request_params="&cpanel_jsonapi_func=edit_zone_record&domain=$domain&type=TXT&name=_acme-challenge$name&txtdata=${token}&line=${line}" +fi +resp=$(curl --silent "${curl_params[@]}" "$request_func$request_params") + +if [[ "$resp" = *\"status\":0* ]]; then + echo -n "cpanel edit zone record failed: " + echo "$resp" | awk -F"statusmsg" '{ print $2 }' | awk -F\" '{ print $3 }' + exit 1 +fi diff --git a/dns_scripts/dns_del_cpanel b/dns_scripts/dns_del_cpanel new file mode 100644 index 0000000..922151a --- /dev/null +++ b/dns_scripts/dns_del_cpanel @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Need to add your email address and API key to cpanel below or set as env variables +user=${CPANEL_USERNAME:-''} +password=${CPANEL_PASSWORD:-''} +url=${CPANEL_URL:-''} # e.g. https://www.cpanel-host.test:2083 +apitoken=${CPANEL_APITOKEN:-''} + +fulldomain="${1}" + +# Check initial parameters +if [[ -z "$fulldomain" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi +if [[ -z "$user" ]]; then + echo "CPANEL_USERNAME (username) parameter not set" + exit 1 +fi +if [[ -z "$apitoken" ]] && [[ -z "$password" ]]; then + echo "Must set either CPANEL_APITOKEN or CPANEL_PASSWORD in dns script, environment variable or getssl.cfg" + exit 1 +fi +if [[ -z "$url" ]]; then + echo "CPANEL_URL (url) parameter not set" + exit 1 +fi + +# Setup +request_func="${url}/json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit" +if [[ -n $apitoken ]]; then + curl_params=( -H "Authorization: cpanel $user:$apitoken" ) +else + auth_string=$(echo -ne "$user:$password" | base64 --wrap 0) + curl_params=( -H "Authorization: Basic $auth_string" ) +fi + +# Check if domain is a CNAME +res=$(dig CNAME "$fulldomain") +domain=$(echo "$res"| awk '$4 ~ "CNAME" {print $5}' |sed 's/\.$//g') +if [[ -n "$domain" ]]; then + name=".${fulldomain%.$domain}" +else + domain=$fulldomain + name="" +fi + +# Find line number of existing record +request_params="&cpanel_jsonapi_func=fetchzone_records&domain=${domain}&type=TXT&name=_acme-challenge.${fulldomain}." +resp=$(curl --silent "${curl_params[@]}" "$request_func$request_params") +if [[ "$resp" = *\"error\":* ]]; then + echo -n "cpanel fetchzone records failed: " + echo "$resp" | awk -F"error" '{ print $2 }' | awk -F\" '{ print $3 }' + exit 1 +fi + +# shellcheck disable=SC2001 +line=$(echo "$resp" | sed -e 's/.*line":\([0-9]*\),.*/\1/') +if [[ "$line" != "" ]]; then + # Delete the challenge token + request_params="&cpanel_jsonapi_func=remove_zone_record&domain=$domain&type=TXT&name=_acme-challenge$name&line=$line" + resp=$(curl --silent "${curl_params[@]}" "$request_func$request_params") +fi + +if [[ "$resp" = *\"status\":0* ]]; then + echo -n "cpanel remove zone record failed: " + echo "$resp" | awk -F"statusmsg" '{ print $2 }' | awk -F\" '{ print $3 }' + exit 1 +fi diff --git a/getssl b/getssl index d73faea..1f1ca7c 100755 --- a/getssl +++ b/getssl @@ -2073,16 +2073,18 @@ revoke_certificate() { # revoke a certificate } requires() { # check if required function is available + args=("${@}") + lastarg=${args[${#args[@]}-1]} if [[ "$#" -gt 1 ]]; then # if more than 1 value, check list for i in "$@"; do - if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found + if [[ "$i" == "$lastarg" ]]; then # if on last variable then exit as not found error_exit "this script requires one of: ${*:1:$(($#-1))}" fi res=$(command -v "$i" 2>/dev/null) debug "checking for $i ... $res" if [[ -n "$res" ]]; then # if function found, then set variable to function and return - debug "function $i found at $res - setting ${!#} to $i" - eval "${!#}=\$i" + debug "function $i found at $res - setting ${lastarg} to $i" + eval "${lastarg}=\$i" return fi done