From 5744c868355e8c1bebee2f7900c57817d3e39529 Mon Sep 17 00:00:00 2001 From: Thomas Lange Date: Sat, 22 Oct 2022 16:39:43 +0200 Subject: Put script into package directory Put the script into the package directory which reflects the directory structure of /usr/local. This makes it easily possible to install the script to /usr/local/sbin with a tool like *GNU Stow*. --- package/sbin/painless-le | 122 +++++++++++++++++++++++++++++++++++++++++++++++ painless-le.sh | 122 ----------------------------------------------- readme.md | 15 +++++- 3 files changed, 135 insertions(+), 124 deletions(-) create mode 100755 package/sbin/painless-le delete mode 100755 painless-le.sh diff --git a/package/sbin/painless-le b/package/sbin/painless-le new file mode 100755 index 0000000..19743c0 --- /dev/null +++ b/package/sbin/painless-le @@ -0,0 +1,122 @@ +#!/bin/bash +#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%# +# Painless Let's Encrypt Certificate Issuing [Thomas Lange ] # +#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%# +# # +# Easily get an X.509 certificate from the Let's Encrypt Certificate Authority # +# for a bunch of hostnames without having an HTTP server installed. The script # +# assumes that you have an existing private key stored within your desired # +# install directory (with the filename which is defined in "${CONFIDENTIAL}"). # +# # +# OPTION [-i]: Full path to the install directory for the certificates. # +# OPTION [-h]: List of hostnames for the certificate: example.org[:...] # +# OPTION [-K]: Filename for the existing private key relative to [-i] # +# OPTION [-I]: Target filename for the intermediate cert relative to [-i] # +# OPTION [-C]: Target filename for the certificate only file relative to [-i] # +# OPTION [-F]: Target filename for the certificate full file relative to [-i] # +# # +#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%# + +#=============================================================================== +# Parsing command-line arguments with the getopts shell builtin +#=============================================================================== +while getopts :i:h:K:I:C:F: option +do + case $option in + i) ARGUMENT_DIRECTORY="$OPTARG" ;; + h) ARGUMENT_HOSTNAMES="$OPTARG" ;; + K) ARGUMENT_CONFIDENTIAL="$OPTARG" ;; + I) ARGUMENT_INTERMEDIATE="$OPTARG" ;; + C) ARGUMENT_CERTIFICATE_ONLY="$OPTARG" ;; + F) ARGUMENT_CERTIFICATE_FULL="$OPTARG" ;; + esac +done; shift $((OPTIND-1)) + +#=============================================================================== +# Checking if all required command-line arguments are provided +#=============================================================================== +[ -z "${ARGUMENT_DIRECTORY}" ] && echo "$0: Missing argument: [-i directory]" >&2 +[ -z "${ARGUMENT_HOSTNAMES}" ] && echo "$0: Missing argument: [-h hostnames]" >&2 + +#=============================================================================== +# Abort execution if required command-line argument is missing +#=============================================================================== +[ -z "${ARGUMENT_DIRECTORY}" ] || [ -z "${ARGUMENT_HOSTNAMES}" ] && exit 1 + +#=============================================================================== +# Define the ACME endpoint address +#=============================================================================== +LETSENCRYPT_ENDPOINT="https://acme-v01.api.letsencrypt.org/directory" +#LETSENCRYPT_ENDPOINT="https://acme-staging-v02.api.letsencrypt.org/directory" + +#=============================================================================== +# Define commands who are executed BEFORE and AFTER the ACME challenge +#=============================================================================== +#LETSENCRYPT_COMMAND_BEFORE="systemctl stop apache2" +#LETSENCRYPT_COMMAND_AFTER="systemctl start apache2" + +#=============================================================================== +# Define required paths +#=============================================================================== + OPENSSLCONF="/etc/ssl/openssl.cnf" + REQUESTFILE=`mktemp /tmp/painless-le.XXXXXXXXXX.csr` + CONFIDENTIAL="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_CONFIDENTIAL:-confidential.pem}" + INTERMEDIATE="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_INTERMEDIATE:-intermediate.pem}" +CERTIFICATE_ONLY="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_CERTIFICATE_ONLY:-certificate_only.pem}" +CERTIFICATE_FULL="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_CERTIFICATE_FULL:-certificate_full.pem}" + +#=============================================================================== +# Delete Certificate-Signing-Request (CSR) file on exit +#=============================================================================== +trap 'rm ${REQUESTFILE}' EXIT + +#=============================================================================== +# Generate Certificate-Signing-Request (CSR) +#=============================================================================== +openssl req -config <(cat "${OPENSSLCONF}" <(printf "[SAN]\nsubjectAltName=DNS:`echo ${ARGUMENT_HOSTNAMES} | sed "s/:/,DNS:/g"`")) \ +-new -sha256 -key "${CONFIDENTIAL}" -out "${REQUESTFILE}" -outform der -reqexts SAN -subj "/" + +#=============================================================================== +# Checking if Certificate-Signing-Request (CSR) was successfully created +#=============================================================================== +if [ $? != 0 ]; then + echo "$0: Certificate-Signing-Request (CSR) could not be created!" >&2 + exit 1 +fi + +#=============================================================================== +# Execute defined command BEFORE the ACME challenge is started +#=============================================================================== +[ ! -z "${LETSENCRYPT_COMMAND_BEFORE}" ] && eval $LETSENCRYPT_COMMAND_BEFORE + +#=============================================================================== +# Execute Let's Encrypt and accomplish the ACME challenge to get the certificate +#=============================================================================== +certbot certonly --authenticator standalone --server "${LETSENCRYPT_ENDPOINT}" --csr "${REQUESTFILE}" \ +--cert-path "${CERTIFICATE_ONLY}.$$" --fullchain-path "${CERTIFICATE_FULL}.$$" --chain-path "${INTERMEDIATE}.$$" + +#=============================================================================== +# Checking if Certbot has successfully accomplished the ACME challenge +#=============================================================================== +if [ $? != 0 ]; then + echo "$0: Certbot could not successfully accomplish the ACME challenge." >&2 + exit 1 +fi + +#=============================================================================== +# Replace previous certificates with the new obtained certificate files +#=============================================================================== +[ -f "${INTERMEDIATE}.$$" ] && mv "${INTERMEDIATE}.$$" "${INTERMEDIATE}" +[ -f "${CERTIFICATE_ONLY}.$$" ] && mv "${CERTIFICATE_ONLY}.$$" "${CERTIFICATE_ONLY}" +[ -f "${CERTIFICATE_FULL}.$$" ] && mv "${CERTIFICATE_FULL}.$$" "${CERTIFICATE_FULL}" + +#=============================================================================== +# Adjust the UNIX permissions with owner and group for the new created files +#=============================================================================== +chmod --reference "${CONFIDENTIAL}" "${INTERMEDIATE}" "${CERTIFICATE_ONLY}" "${CERTIFICATE_FULL}" +chown --reference "${CONFIDENTIAL}" "${INTERMEDIATE}" "${CERTIFICATE_ONLY}" "${CERTIFICATE_FULL}" + +#=============================================================================== +# Execute defined command AFTER the ACME challenge is completed +#=============================================================================== +[ ! -z "${LETSENCRYPT_COMMAND_AFTER}" ] && eval $LETSENCRYPT_COMMAND_AFTER diff --git a/painless-le.sh b/painless-le.sh deleted file mode 100755 index 19743c0..0000000 --- a/painless-le.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash -#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%# -# Painless Let's Encrypt Certificate Issuing [Thomas Lange ] # -#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%# -# # -# Easily get an X.509 certificate from the Let's Encrypt Certificate Authority # -# for a bunch of hostnames without having an HTTP server installed. The script # -# assumes that you have an existing private key stored within your desired # -# install directory (with the filename which is defined in "${CONFIDENTIAL}"). # -# # -# OPTION [-i]: Full path to the install directory for the certificates. # -# OPTION [-h]: List of hostnames for the certificate: example.org[:...] # -# OPTION [-K]: Filename for the existing private key relative to [-i] # -# OPTION [-I]: Target filename for the intermediate cert relative to [-i] # -# OPTION [-C]: Target filename for the certificate only file relative to [-i] # -# OPTION [-F]: Target filename for the certificate full file relative to [-i] # -# # -#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%# - -#=============================================================================== -# Parsing command-line arguments with the getopts shell builtin -#=============================================================================== -while getopts :i:h:K:I:C:F: option -do - case $option in - i) ARGUMENT_DIRECTORY="$OPTARG" ;; - h) ARGUMENT_HOSTNAMES="$OPTARG" ;; - K) ARGUMENT_CONFIDENTIAL="$OPTARG" ;; - I) ARGUMENT_INTERMEDIATE="$OPTARG" ;; - C) ARGUMENT_CERTIFICATE_ONLY="$OPTARG" ;; - F) ARGUMENT_CERTIFICATE_FULL="$OPTARG" ;; - esac -done; shift $((OPTIND-1)) - -#=============================================================================== -# Checking if all required command-line arguments are provided -#=============================================================================== -[ -z "${ARGUMENT_DIRECTORY}" ] && echo "$0: Missing argument: [-i directory]" >&2 -[ -z "${ARGUMENT_HOSTNAMES}" ] && echo "$0: Missing argument: [-h hostnames]" >&2 - -#=============================================================================== -# Abort execution if required command-line argument is missing -#=============================================================================== -[ -z "${ARGUMENT_DIRECTORY}" ] || [ -z "${ARGUMENT_HOSTNAMES}" ] && exit 1 - -#=============================================================================== -# Define the ACME endpoint address -#=============================================================================== -LETSENCRYPT_ENDPOINT="https://acme-v01.api.letsencrypt.org/directory" -#LETSENCRYPT_ENDPOINT="https://acme-staging-v02.api.letsencrypt.org/directory" - -#=============================================================================== -# Define commands who are executed BEFORE and AFTER the ACME challenge -#=============================================================================== -#LETSENCRYPT_COMMAND_BEFORE="systemctl stop apache2" -#LETSENCRYPT_COMMAND_AFTER="systemctl start apache2" - -#=============================================================================== -# Define required paths -#=============================================================================== - OPENSSLCONF="/etc/ssl/openssl.cnf" - REQUESTFILE=`mktemp /tmp/painless-le.XXXXXXXXXX.csr` - CONFIDENTIAL="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_CONFIDENTIAL:-confidential.pem}" - INTERMEDIATE="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_INTERMEDIATE:-intermediate.pem}" -CERTIFICATE_ONLY="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_CERTIFICATE_ONLY:-certificate_only.pem}" -CERTIFICATE_FULL="${ARGUMENT_DIRECTORY%/}/${ARGUMENT_CERTIFICATE_FULL:-certificate_full.pem}" - -#=============================================================================== -# Delete Certificate-Signing-Request (CSR) file on exit -#=============================================================================== -trap 'rm ${REQUESTFILE}' EXIT - -#=============================================================================== -# Generate Certificate-Signing-Request (CSR) -#=============================================================================== -openssl req -config <(cat "${OPENSSLCONF}" <(printf "[SAN]\nsubjectAltName=DNS:`echo ${ARGUMENT_HOSTNAMES} | sed "s/:/,DNS:/g"`")) \ --new -sha256 -key "${CONFIDENTIAL}" -out "${REQUESTFILE}" -outform der -reqexts SAN -subj "/" - -#=============================================================================== -# Checking if Certificate-Signing-Request (CSR) was successfully created -#=============================================================================== -if [ $? != 0 ]; then - echo "$0: Certificate-Signing-Request (CSR) could not be created!" >&2 - exit 1 -fi - -#=============================================================================== -# Execute defined command BEFORE the ACME challenge is started -#=============================================================================== -[ ! -z "${LETSENCRYPT_COMMAND_BEFORE}" ] && eval $LETSENCRYPT_COMMAND_BEFORE - -#=============================================================================== -# Execute Let's Encrypt and accomplish the ACME challenge to get the certificate -#=============================================================================== -certbot certonly --authenticator standalone --server "${LETSENCRYPT_ENDPOINT}" --csr "${REQUESTFILE}" \ ---cert-path "${CERTIFICATE_ONLY}.$$" --fullchain-path "${CERTIFICATE_FULL}.$$" --chain-path "${INTERMEDIATE}.$$" - -#=============================================================================== -# Checking if Certbot has successfully accomplished the ACME challenge -#=============================================================================== -if [ $? != 0 ]; then - echo "$0: Certbot could not successfully accomplish the ACME challenge." >&2 - exit 1 -fi - -#=============================================================================== -# Replace previous certificates with the new obtained certificate files -#=============================================================================== -[ -f "${INTERMEDIATE}.$$" ] && mv "${INTERMEDIATE}.$$" "${INTERMEDIATE}" -[ -f "${CERTIFICATE_ONLY}.$$" ] && mv "${CERTIFICATE_ONLY}.$$" "${CERTIFICATE_ONLY}" -[ -f "${CERTIFICATE_FULL}.$$" ] && mv "${CERTIFICATE_FULL}.$$" "${CERTIFICATE_FULL}" - -#=============================================================================== -# Adjust the UNIX permissions with owner and group for the new created files -#=============================================================================== -chmod --reference "${CONFIDENTIAL}" "${INTERMEDIATE}" "${CERTIFICATE_ONLY}" "${CERTIFICATE_FULL}" -chown --reference "${CONFIDENTIAL}" "${INTERMEDIATE}" "${CERTIFICATE_ONLY}" "${CERTIFICATE_FULL}" - -#=============================================================================== -# Execute defined command AFTER the ACME challenge is completed -#=============================================================================== -[ ! -z "${LETSENCRYPT_COMMAND_AFTER}" ] && eval $LETSENCRYPT_COMMAND_AFTER diff --git a/readme.md b/readme.md index 8df6fd1..f9657d5 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,17 @@ Painless issuing a single [X.509 certificate](https://tools.ietf.org/html/rfc528 ## Requirements The [Certbot client](https://certbot.eff.org/) must be installed on your machine because PainlessLE uses this piece of software to communicate over the [ACME protocol](https://tools.ietf.org/html/draft-ietf-acme-acme-01) with the ACME endpoint of Let's Encrypt and runs the ACME challenge. There are no known further requirements for using PainlessLE on Debian GNU/Linux at this time. +## Installation +Beside the possibility to manually place the script in some directory, you can use the more elegant way with [*GNU Stow*](https://www.gnu.org/software/stow/) to map the content of the `package` directory via symbolic links properly to `/usr/local`: + +~~~bash +cd /usr/local/src +git clone $REPO && cd $REPO +stow -t /usr/local package +~~~ + +Make sure that no unprivileged user has write permissions on `/usr/local/sbin`, the symlink targets (in case you've choosen `stow`) and/or the `painless-le` script, because PainlessLE is usually executed with `root` privileges. + ## Configuration Change the `LETSENCRYPT_ENDPOINT` to the address of the ACME staging API for testing purposes. You also can define a command within `LETSENCRYPT_COMMAND_BEFORE` to shut down a running webserver to release the HTTP(S) port for the standalone webserver before Certbot runs the ACME challenge. You can restart your webserver after the ACME challenge is completed within `LETSENCRYPT_COMMAND_AFTER`. @@ -26,9 +37,9 @@ Lets assume you want to get a single X.509 certificate from the Let's Encrypt CA /etc/painless-le/example.org/ └── [-rw-r----- user group ] confidential.pem -The next step is to execute `painless-le.sh` and providing the `-i` and `-h` options which are described above. In this example, the complete command-line string with the desired install directory `/etc/painless-le/example.org` and the desired hostnames `example.org`, `blog.example.org` and `shop.example.org` looks as follows: +The next step is to execute `painless-le` and providing the `-i` and `-h` options which are described above. In this example, the complete command-line string with the desired install directory `/etc/painless-le/example.org` and the desired hostnames `example.org`, `blog.example.org` and `shop.example.org` looks as follows: - painless-le.sh -i /etc/painless-le/example.org/ -h example.org:blog.example.org:shop.example.org + painless-le -i /etc/painless-le/example.org/ -h example.org:blog.example.org:shop.example.org The Certbot client will now contact the ACME challenge servers and runs a temporary standalone webserver on your machine to accomplish the ACME challenge. If all works fine, you have nothing to intervene. After the command was successfully executed, you will see your certificates within your desired install directory (the certificate files will inherit the UNIX permissions of the `confidential.pem` file) and you're done: -- cgit v1.2.3