Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Last active July 18, 2024 16:06
Show Gist options
  • Save QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0 to your computer and use it in GitHub Desktop.
Save QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0 to your computer and use it in GitHub Desktop.
SSL Certificates #ssl_certificates

Deploy UniFi Script

Overview

The deploy_unifi.sh script automates the deployment of Let's Encrypt certificates for use with a UniFi controller container. It ensures that the UniFi controller utilizes the latest SSL certificates for secure communications. This script is ideal for administrators looking to automate the management of SSL certificates for their UniFi controllers, especially in environments where certificates are frequently updated.

Prerequisites

  • Docker installed on the host machine.
  • A running UniFi controller container.
  • The curl command available for downloading scripts.
  • OPNsense firewall with ACME plugin installed and configured to issue certificates.

Usage

Download and Prepare the Script

Download the script using the following command:

sudo mkdir -p /opt/ssl/scripts/ && sudo curl -o /opt/ssl/scripts/deploy_unifi.sh https://gist.githubusercontent.com/QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0/raw/scripts---deploy_unifi.sh && sudo chmod 0750 /opt/ssl/scripts/deploy_unifi.sh

Command Structure

./deploy_unifi.sh -c <certificate_file> [-r] [-d <destination_directory>] [-n <container_name>]
  • -c, --certificate: Specify the Let's Encrypt certificate file.
  • -r, --restart: (Optional) Restart the UniFi Docker container after updating the certificates.
  • -d, --destination: (Optional) Destination directory for the certificates. Defaults to the current directory.
  • -n, --container: (Optional) Name of the UniFi Docker container. Defaults to unifi.

Automating Certificate Deployment

ACME Plugin Automation on OPNsense

Configure the ACME plugin to automate the deployment of certificates to the UniFi controller using the following automation snippet:

[upload-unifi]
command: cat /var/etc/acme-client/home/lan.besqua.red/fullchain.cer /var/etc/acme-client/home/lan.besqua.red/lan.besqua.red.key | ssh -4 -oLogLevel=ERROR -oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no -T -i /root/.ssh/id_deploy_unifi certupload@ubuntu.lan.besqua.red
parameters:
type:script
message:deploying certificates to unifi
description:Deploy to UniFi [besquared]

This automation concatenates the certificate and private key, then securely transmits them to the UniFi controller through SSH for deployment.

Remote Execution via SSH

Setting up a Dedicated SSH Command

  1. Generate SSH Key Pair on the OPNsense firewall.
  2. Install the Public Key on the remote host where the UniFi controller is running.
  3. Configure the authorized_keys Entry to execute the deploy_unifi.sh script upon SSH login, optionally restricting execution to specific client IP addresses for added security.

Security Considerations

  • Regularly update the deploy_unifi.sh script to incorporate security enhancements and bug fixes.
  • Protect the SSH key pair used for remote execution with a strong passphrase.
  • Restrict permissions for the script and destination directories to prevent unauthorized access.

License

This script and documentation are provided 'as is' without any warranty of any kind, either expressed or implied. You are free to use and modify them for your purposes at your own risk.

#!/usr/bin/env bash
##
## Title: deploy_homeassistant.sh
## Description: Script to deploy uploaded certificates for use with Home Assistant
## Author: B. van wetten
## Created date: 28-12-2020
## Updated date: 18-07-2024
## Version: 0.2
## GitHub Gist: https://gist.githubusercontent.com/QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0/raw/scripts---deploy_homeassistant.sh
##
## Usage: deploy_homeassistant.sh -c certificates [-r] [-d destination]
## deploy_homeassistant.sh --help
## To do: Also allow for a filename to be passed as a positional parameter to the '-c' argument
## Notes: For remote use (e.g. SSH) use the following command as example:
## /bin/cat | /bin/xargs -0 -I % /home/dsmr/deploy_homeassistant.sh -c --%
##
## You can use this in your 'authorized_keys' file to run this command remotely:
## command="/bin/cat | /usr/bin/xargs -0 -I % /config/scripts/deploy_homeassistant.sh -r -d /home/pi/certs -c % --",no-user-rc,no-port-forwarding,no-x11-forwarding,no-pty,from="<client-ip>" ssh-rsa AAAAB3********
set -o nounset
# http://www.dwheeler.com/essays/filenames-in-shell.html
IFS=$'\n\t'
# Shell utilities
CP=$(which cp); [[ $? != 0 ]] && echo "Command 'cp' not found" >&2 && exit 1
MV=$(which mv); [[ $? != 0 ]] && echo "Command 'mv' not found" >&2 && exit 1
RM=$(which rm); [[ $? != 0 ]] && echo "Command 'rm' not found" >&2 && exit 1
FIND=$(which find); [[ $? != 0 ]] && echo "Command 'find' not found" >&2 && exit 1
CHMOD=$(which chmod); [[ $? != 0 ]] && echo "Command 'chmod' not found" >&2 && exit 1
XARGS=$(which xargs); [[ $? != 0 ]] && echo "Command 'xargs' not found" >&2 && exit 1
GETOPT=$(which getopt); [[ $? != 0 ]] && echo "Command 'getopt' not found" >&2 && exit 1
OPENSSL=$(which openssl); [[ $? != 0 ]] && echo "Command 'openssl' not found" >&2 && exit 1
# Initialize variables
_PWD=$(pwd)
_UID=$(id -u)
_GID=$(id -g)
_ME=$(basename "${0}")
_TMPFILE="tmp-${$}-${RANDOM}"
_CERT_FILE="cert.pem"
_KEY_FILE="privkey.pem"
_CHAIN_FILE="fullchain.pem"
_CERT_PERMS="0400"
_RFLAG=false
_DFLAG=false
# Option strings
SHORT=hrd:
LONG=help,restart,destination:
##########################################################################
# Init #
##########################################################################
# Cleanup any temp files after script execution (traps signals: 0, 1, 2, 15)
trap '${FIND} ${_PWD} -maxdepth 1 -name "${_TMPFILE}*" -type f -print0 | ${XARGS} -I % -0 ${RM} %' INT TERM HUP EXIT
# Read certificate from command line and remove from positional arguments
for((i=1; i<=$#; i++))
do
_cur=$i
_next=$(expr $i + 1)
_param=${!_cur}
_next_param=${!_next:-}
if [[ "${_param}" =~ (^-c$|^--certificate$) && ! -z "${_next_param}" ]]
then
echo -en "${_next_param}" > "${_TMPFILE}.pem"
set -- "${@:1:_cur-1}" "${@:_cur+2}"
fi
done
# Get command line arguments
if [[ "${#}" -gt 0 ]]
then
getopt -T > /dev/null
if [ $? -eq 4 ]; then
# GNU enhanced getopt is available
_ARGS=$(${GETOPT} --name "$_ME" --long ${LONG} --options ${SHORT} -- "$@")
else
# Original getopt is available (no long option names, no whitespace, no sorting)
_ARGS=$(${GETOPT} ${SHORT} "$@")
fi
if [ $? -ne 0 ]; then
echo "$_ME: usage error (use -h for help)" >&2
exit 2
fi
fi
eval set -- "${_ARGS:-}"
###############################################################################
# Functions #
###############################################################################
# _print_usage()
#
# Usage:
# _print_usage
#
# Print the program usage information.
_print_usage() {
cat <<HEREDOC
___ ____ ___ _ ____ _ _ _ _ ____ _ _ ____ ____ ____ ____ _ ____ ___ ____ _ _ ___
| \ |___ |__] | | | \_/ |__| | | |\/| |___ |__| [__ [__ | [__ | |__| |\ | |
|__/ |___ | |___ |__| | ___ | | |__| | | |___ | | ___] ___] | ___] | | | | \| |
Usage:
${_ME} -c file [-r] [-d destination]
${_ME} -h
Options:
-h, --help Show this screen
-r, --restart Restart Home Assistant
-c, --certificate Certificate file
-d, --destination Destination folder
HEREDOC
exit ${1:-0}
}
# _set_variable()
#
# Usage:
# _set_variable variable value
#
# Sets the variable to a value if not already set. Otherwise exits with an error message
_set_variable()
{
local varname="$1"
shift
if [ -z "${!varname:-}" ]; then
# eval="${varname}=${@@Q}"
eval "$varname=\"$@\""
else
echo "Error: $varname already set"
usage
fi
}
# _parse_commandline_arguments()
#
# Usage:
# _parse_commandline_arguments
#
# Parses and validates commandline arguments and populates appropriate variables.
_parse_commandline_arguments()
{
while true;
do
case "${1:-}" in
-r|--restart)
_RFLAG=true
shift
;;
-d|--destination)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable DST_DIR "${2}"
_DFLAG=true
shift 2
;;
\?) echo "$_ME: Unknown option -$1" >&2;
exit 1
;;
:) echo "$_ME: -$1 needs a value" >&2;
exit 1
;;
*) shift
break
;;
esac
done
}
# _validate_parameters()
#
# Usage:
# _validate_parameters
#
# Performs several checks on the supplied command line arguments
_validate_parameters() {
# Check certificates
[[ ! -f "${_TMPFILE}.pem" || ! $(${OPENSSL} rsa -in "${_TMPFILE}.pem" -check 2> /dev/null) ]] && echo -e "$_ME: Invalid or no certificate supplied\n" && _print_usage 1;
# Check if destination was given as argument otherwise set destination to source
[[ ]! "$_DFLAG" ]] && DST_DIR="$_PWD"
# Remove trailing slash unless root
case $DST_DIR in
*[!/]*/) DST_DIR=${DST_DIR%"${DST_DIR##*[!/]}"};;
*[/]) DST_DIR="/";;
esac
# Check if destination dir argument exists as a directory
[[ ! -d "$DST_DIR" ]] && echo -e "Error: '${DST_DIR}' does not exist or current user does not have writing permissions\n" && _print_usage 1;
}
###############################################################################
# Main #
###############################################################################
# _main()
#
# Usage:
# _main [<options>] [<arguments>]
#
# Description:
# Entry point for the program, handling basic option parsing and dispatching.
_main() {
# Avoid complex option parsing when only one program option is expected.
if [[ "${@:-}" =~ -h|--help ]]
then
_print_usage
else
_parse_commandline_arguments "$@"
_validate_parameters
# Extract certificate, key and certificate authority chain
${OPENSSL} x509 -in "${_TMPFILE}.pem" -outform PEM -out "${_TMPFILE}.cert.pem" || exit 1
${OPENSSL} pkey -out "${_TMPFILE}.key.pem" -in "${_TMPFILE}.pem" || exit 1
${OPENSSL} crl2pkcs7 -nocrl -certfile "${_TMPFILE}.pem" > "${_TMPFILE}.pkcs7" || exit 1
${OPENSSL} pkcs7 -print_certs -out "${_TMPFILE}.chain.pem" -in "${_TMPFILE}.pkcs7" || exit 1
# Move certificate files
sudo ${MV} "${_TMPFILE}.cert.pem" ${DST_DIR}/${_CERT_FILE} || exit 1
sudo ${MV} "${_TMPFILE}.key.pem" ${DST_DIR}/${_KEY_FILE} || exit 1
sudo ${MV} "${_TMPFILE}.chain.pem" ${DST_DIR}/${_CHAIN_FILE} || exit 1
# Set the correct permissions
sudo ${CHMOD} ${_CERT_PERMS} ${DST_DIR}/${_CERT_FILE} || exit 1
sudo ${CHMOD} ${_CERT_PERMS} ${DST_DIR}/${_KEY_FILE} || exit 1
sudo ${CHMOD} ${_CERT_PERMS} ${DST_DIR}/${_CHAIN_FILE} || exit 1
# Reload Home Assistant
# Work around: using 'sudo docker container restart homeassistant' since I have not found a way to restart HA yet)
[[ "$_RFLAG" ]] && sudo /usr/local/bin/docker container restart addon_core_nginx_proxy
fi
}
# Call `_main` after everything has been defined.
_main "$@"
exit 0;
#!/usr/bin/env bash
##
## Title: deploy_unifi.sh
## Description: Script to deploy Let's Encrypt certificates for use with unifi controller container
## Author: B. van wetten
## Created date: 23-01-2020
## Updated date: 06-02-2024
## Version: 0.9.3
## GitHub Gist: https://gist.github.com/QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0#file-scripts-deploy_unifi-sh
## GitHub Readme: https://gist.github.com/QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0#file-deploy_unify-readme-md
## Download: curl -o /opt/ssl/scripts/deploy_unifi.sh https://gist.githubusercontent.com/QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0/raw/scripts---deploy_unifi.sh
##
## Usage: deploy_unifi.sh -c certificates [-r] [-d destination] [-n container]
## deploy_unifi.sh --help
##
## Notes: For remote use (e.g. SSH) use the following command as example:
## /bin/cat | /bin/xargs -0 -I % /opt/ssl/scripts/deploy_unifi.sh -r -n unifi -d /opt/ssl/unifi -c --%
##
## You can use this in your 'authorized_keys' file to run this command remotely:
## command="/bin/cat | /bin/xargs -0 -I % /opt/ssl/scripts/deploy_unifi.sh -r -n unifi -d /opt/ssl/unifi -c % --",no-pty,from="<client-ip>" ssh-rsa AAAAB3********
set -o nounset
# http://www.dwheeler.com/essays/filenames-in-shell.html
IFS=$'\n\t'
# Prevents environment variables from corrupting script
unset DST_DIR CONTAINER_NAME
# Shell utilities
for cmd in echo cp mv rm find chmod xargs docker getopt openssl; do
if ! command -v "$cmd" &> /dev/null; then
echo "Command '$cmd' not found" >&2
exit 1
fi
done
# Initialize variables
_PWD=$(pwd)
_UID=$(id -u)
_GID=$(id -g)
_ME=$(basename "${0}")
_TMPFILE=$(mktemp "tmp-${_ME}-XXXXXX.pem")
_CONTAINER_NAME_DEFAULT="unifi"
_UNIFI_CERTS_FILE="cert.pem"
_UNIFI_KEY_FILE="privkey.pem"
_UNIFI_CA_FILE="chain.pem"
_CERT_PERMS="0400"
_RFLAG=false
_DFLAG=false
# Option strings
SHORT=rhd:n:
LONG=restart,help,destination:,container:
##########################################################################
# Init #
##########################################################################
# Cleanup any temp files after script execution (traps signals: 0, 1, 2, 15)
trap 'find . -maxdepth 1 -name "${_TMPFILE}*" -type f -print0 | xargs -I % -0 rm %' INT TERM HUP EXIT
# Read certificate from command line and remove from positional arguments
for((i=1; i<=$#; i++))
do
_cur=$i
_next=$(expr $i + 1)
_param=${!_cur}
_next_param=${!_next:-}
if [[ "${_param}" =~ (^-c$|^--certificate$) && ! -z "${_next_param}" ]]
then
echo -en "${_next_param}" > "${_TMPFILE}.pem"
set -- "${@:1:_cur-1}" "${@:_cur+2}"
fi
done
# Get command line arguments
if [[ "${#}" -gt 0 ]]
then
getopt -T > /dev/null
if [ $? -eq 4 ]; then
# GNU enhanced getopt is available
_ARGS=$(getopt --name "$_ME" --long ${LONG} --options ${SHORT} -- "$@")
else
# Original getopt is available (no long option names, no whitespace, no sorting)
_ARGS=$(getopt ${SHORT} "$@")
fi
if [ $? -ne 0 ]; then
echo "$_ME: usage error (use -h for help)" >&2
exit 2
fi
fi
eval set -- "${_ARGS:-}"
###############################################################################
# Functions #
###############################################################################
# _print_usage()
#
# Usage:
# _print_usage
#
# Print the program usage information.
_print_usage() {
cat <<HEREDOC
___ ____ ___ _ ____ _ _ _ _ _ _ _ ____ _
| \ |___ |__] | | | \_/ | | |\ | | |___ |
|__/ |___ | |___ |__| | ___ |__| | \| | | |
Usage:
${_ME} -c <certificate_file> [-r] [-d <destination_directory>] [-n <container_name>]
${_ME} --help
Options:
-h, --help Show this help message and exit.
-r, --restart Restart the specified UniFi Docker container after updating certificates.
-c, --certificate Path to the Let's Encrypt certificate file. This option is required.
-n, --container Specify the name of the UniFi Docker container to which certificates are deployed (default: "${_CONTAINER_NAME_DEFAULT}").
-d, --destination Specify the destination directory where certificates should be deployed. If not specified, the current directory is used.
Description:
This script deploys Let's Encrypt certificates for use with a UniFi controller container. It supports updating certificates, setting permissions, and optionally restarting the UniFi container to apply changes.
Notes:
- Ensure the Docker container name provided with -n matches your UniFi controller container's name.
- The destination directory must exist and be writable by the script.
- For automated deployments or remote execution, consider securely handling the certificate file path and ensuring appropriate permissions.
Examples:
Deploy certificates to a specific directory and UniFi container, then restart the container:
${_ME} -c /path/to/cert.pem -d /opt/ssl/unifi -n unifi -r
Show help information:
${_ME} --help
HEREDOC
exit ${1:-0}
}
# _openssl()
#
# Usage:
# _openssl variable value
#
# Runs the OpenSSL utility as a docker container
_openssl() {
docker run --rm -v "$_PWD:$_PWD" --user "${_UID}:${_GID}" --workdir="$_PWD" frapsoft/openssl "$@"
}
# _set_variable()
#
# Usage:
# _set_variable variable value
#
# Sets the variable to a value if not already set. Otherwise exits with an error message
_set_variable()
{
local varname="$1"
shift
if [ -z "${!varname:-}" ]; then
# eval="${varname}=${@@Q}"
eval "$varname=\"$@\""
else
echo "Error: $varname already set"
_print_usage
fi
}
# _parse_commandline_arguments()
#
# Usage:
# _parse_commandline_arguments
#
# Parses and validates commandline arguments and populates appropriate variables.
_parse_commandline_arguments()
{
while true;
do
case "${1:-}" in
-r|--restart)
_RFLAG=true
shift
;;
-n|--container)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable CONTAINER_NAME "${2}"
shift 2
;;
-d|--destination)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable DST_DIR "${2}"
_DFLAG=true
shift 2
;;
\?) echo "$_ME: Unknown option -$1" >&2;
exit 1
;;
:) echo "$_ME: -$1 needs a value" >&2;
exit 1
;;
*) shift
break
;;
esac
done
}
# _validate_parameters()
#
# Usage:
# _validate_parameters
#
# Performs several checks on the supplied command line arguments
_validate_parameters() {
# Check certificates
[[ ! -f "${_TMPFILE}.pem" || ! $(openssl rsa -in "${_TMPFILE}.pem" -check 2> /dev/null) ]] && echo -e "$_ME: Invalid or no certificate supplied\n" && _print_usage 1;
# Check if container is running
CONTAINER_NAME=${CONTAINER_NAME:-$_CONTAINER_NAME_DEFAULT}
[[ -z "$(docker ps -q -f name=${CONTAINER_NAME})" ]] && echo -e "$_ME: Docker container ${CONTAINER_NAME} is not running\n" && _print_usage 1;
# Check if destination was given as argument otherwise set destination to source
! "$_DFLAG" && DST_DIR="$_PWD"
# Remove trailing slash unless root
case $DST_DIR in
*[!/]*/) DST_DIR=${DST_DIR%"${DST_DIR##*[!/]}"};;
*[/]) DST_DIR="/";;
esac
# Check if destination dir argument exists as a directory
[[ ! -d "$DST_DIR" || ! -w "$DST_DIR" ]] && echo -e "Error: '${DST_DIR}' does not exist or current user does not have writing permissions\n" && _print_usage 1;
}
###############################################################################
# Main #
###############################################################################
# _main()
#
# Usage:
# _main [<options>] [<arguments>]
#
# Description:
# Entry point for the program, handling basic option parsing and dispatching.
_main() {
# Avoid complex option parsing when only one program option is expected.
if [[ "${@:-}" =~ -h|--help ]]
then
_print_usage
else
_parse_commandline_arguments "$@"
_validate_parameters
# Extract certificate, key and certificate authority chain
_openssl x509 -in "${_TMPFILE}.pem" -outform PEM -out "${_TMPFILE}.cert.pem" || exit 1
_openssl pkey -out "${_TMPFILE}.key.pem" -in "${_TMPFILE}.pem" || exit 1
_openssl crl2pkcs7 -nocrl -certfile "${_TMPFILE}.pem" > "${_TMPFILE}.pkcs7" || exit 1
_openssl pkcs7 -print_certs -out "${_TMPFILE}.ca.pem" -in "${_TMPFILE}.pkcs7" || exit 1
# Copy certificate files
cp -af "${_TMPFILE}.cert.pem" ${DST_DIR}/${_UNIFI_CERTS_FILE} || exit 1
cp -af "${_TMPFILE}.key.pem" ${DST_DIR}/${_UNIFI_KEY_FILE} || exit 1
cp -af "${_TMPFILE}.ca.pem" ${DST_DIR}/${_UNIFI_CA_FILE} || exit 1
# Set the correct permissions
chmod ${_CERT_PERMS} ${DST_DIR}/${_UNIFI_CERTS_FILE} || exit 1
chmod ${_CERT_PERMS} ${DST_DIR}/${_UNIFI_KEY_FILE} || exit 1
chmod ${_CERT_PERMS} ${DST_DIR}/${_UNIFI_CA_FILE} || exit 1
# Remove UniFi keystore and the MD5 hash
docker exec ${CONTAINER_NAME} rm -rf /unifi/cert/${_UNIFI_CERTS_FILE}.md5 2>&1 >/dev/null || exit 1
# Restart docker container
"$_RFLAG" && docker restart ${CONTAINER_NAME} 2>&1 >/dev/null || exit 1
fi
}
# Call `_main` after everything has been defined.
_main "$@"
exit 0;
#!/usr/bin/env bash
##
## Title: deploy_zwavejs.sh
## Description: Script to deploy Let's Encrypt certificates for use with Z-Wave JS container
## Author: B. van wetten
## Created date: 04-06-2024
## Version: 0.1.0
##
## Usage: deploy_zwavejs.sh -c certificates [-r] [-d destination] [-n container]
## deploy_zwavejs.sh --help
##
## Notes: For remote use (e.g. SSH) use the following command as example:
## /bin/cat | /bin/xargs -0 -I % /opt/ssl/scripts/deploy_zwavejs.sh -r -n zwavejs -d /home/pi/zwavejs-ui/store -c --%
##
## You can use this in your 'authorized_keys' file to run this command remotely:
## command="/bin/cat | /bin/xargs -0 -I % /opt/ssl/scripts/deploy_zwavejs.sh -r -n zwavejs -d /home/pi/zwavejs-ui/store -c % --",no-pty,from="<client-ip>" ssh-rsa AAAAB3********
set -o nounset
# http://www.dwheeler.com/essays/filenames-in-shell.html
IFS=$'\n\t'
# Prevents environment variables from corrupting script
unset DST_DIR CONTAINER_NAME
# Shell utilities
for cmd in echo cp mv rm find chmod xargs docker getopt openssl; do
if ! command -v "$cmd" &> /dev/null; then
echo "Command '$cmd' not found" >&2
exit 1
fi
done
# Initialize variables
_PWD=$(pwd)
_UID=$(id -u)
_GID=$(id -g)
_ME=$(basename "${0}")
_TMPFILE=$(mktemp "tmp-${_ME}-XXXXXX.pem")
_CONTAINER_NAME_DEFAULT="zwavejs"
_ZWAVEJS_CERTS_FILE="cert.pem"
_ZWAVEJS_KEY_FILE="privkey.pem"
_ZWAVEJS_CA_FILE="chain.pem"
_CERT_PERMS="0400"
_RFLAG=false
_DFLAG=false
# Option strings
SHORT=rhd:n:
LONG=restart,help,destination:,container:
##########################################################################
# Init #
##########################################################################
# Cleanup any temp files after script execution (traps signals: 0, 1, 2, 15)
trap 'find . -maxdepth 1 -name "${_TMPFILE}*" -type f -print0 | xargs -I % -0 rm %' INT TERM HUP EXIT
# Read certificate from command line and remove from positional arguments
for((i=1; i<=$#; i++))
do
_cur=$i
_next=$(expr $i + 1)
_param=${!_cur}
_next_param=${!_next:-}
if [[ "${_param}" =~ (^-c$|^--certificate$) && ! -z "${_next_param}" ]]
then
echo -en "${_next_param}" > "${_TMPFILE}.pem"
set -- "${@:1:_cur-1}" "${@:_cur+2}"
fi
done
# Get command line arguments
if [[ "${#}" -gt 0 ]]
then
getopt -T > /dev/null
if [ $? -eq 4 ]; then
# GNU enhanced getopt is available
_ARGS=$(getopt --name "$_ME" --long ${LONG} --options ${SHORT} -- "$@")
else
# Original getopt is available (no long option names, no whitespace, no sorting)
_ARGS=$(getopt ${SHORT} "$@")
fi
if [ $? -ne 0 ]; then
echo "$_ME: usage error (use -h for help)" >&2
exit 2
fi
fi
eval set -- "${_ARGS:-}"
###############################################################################
# Functions #
###############################################################################
# _print_usage()
#
# Usage:
# _print_usage
#
# Print the program usage information.
_print_usage() {
cat <<HEREDOC
___ ____ ___ _ ____ _ _ ___ _ _ _ ____ _ _ ____ _ ____
| \ |___ |__] | | | \_/ / | | | |__| | | |___ | [__
|__/ |___ | |___ |__| | ___ /__ |_|_| | | \/ |___ _| ___]
Usage:
${_ME} -c <certificate_file> [-r] [-d <destination_directory>] [-n <container_name>]
${_ME} --help
Options:
-h, --help Show this help message and exit.
-r, --restart Restart the specified Z-Wave JS Docker container after updating certificates.
-c, --certificate Path to the Let's Encrypt certificate file. This option is required.
-n, --container Specify the name of the Z-Wave JS Docker container to which certificates are deployed (default: "${_CONTAINER_NAME_DEFAULT}").
-d, --destination Specify the destination directory where certificates should be deployed (default: "/home/pi/zwavejs-ui/store").
Description:
This script deploys Let's Encrypt certificates for use with a Z-Wave JS container. It supports updating certificates, setting permissions, and optionally restarting the Z-Wave JS container to apply changes.
Notes:
- Ensure the Docker container name provided with -n matches your Z-Wave JS container's name.
- The destination directory must exist and be writable by the script.
- For automated deployments or remote execution, consider securely handling the certificate file path and ensuring appropriate permissions.
Examples:
Deploy certificates to a specific directory and Z-Wave JS container, then restart the container:
${_ME} -c /path/to/cert.pem -d /home/pi/zwavejs-ui/store -n zwavejs -r
Show help information:
${_ME} --help
HEREDOC
exit ${1:-0}
}
# _openssl()
#
# Usage:
# _openssl variable value
#
# Runs the OpenSSL utility as a docker container
_openssl() {
openssl "$@"
}
# _set_variable()
#
# Usage:
# _set_variable variable value
#
# Sets the variable to a value if not already set. Otherwise exits with an error message
_set_variable()
{
local varname="$1"
shift
if [ -z "${!varname:-}" ]; then
eval "$varname=\"$@\""
else
echo "Error: $varname already set"
_print_usage
fi
}
# _parse_commandline_arguments()
#
# Usage:
# _parse_commandline_arguments
#
# Parses and validates commandline arguments and populates appropriate variables.
_parse_commandline_arguments()
{
while true;
do
case "${1:-}" in
-r|--restart)
_RFLAG=true
shift
;;
-n|--container)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable CONTAINER_NAME "${2}"
shift 2
;;
-d|--destination)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable DST_DIR "${2}"
_DFLAG=true
shift 2
;;
\?) echo "$_ME: Unknown option -$1" >&2;
exit 1
;;
:) echo "$_ME: -$1 needs a value" >&2;
exit 1
;;
*) shift
break
;;
esac
done
}
# _validate_parameters()
#
# Usage:
# _validate_parameters
#
# Performs several checks on the supplied command line arguments
_validate_parameters() {
# Check certificates
[[ ! -f "${_TMPFILE}.pem" || ! $(openssl rsa -in "${_TMPFILE}.pem" -check 2> /dev/null) ]] && echo -e "$_ME: Invalid or no certificate supplied\n" && _print_usage 1;
# Check if container is running
CONTAINER_NAME=${CONTAINER_NAME:-$_CONTAINER_NAME_DEFAULT}
[[ -z "$(docker ps -q -f name=${CONTAINER_NAME})" ]] && echo -e "$_ME: Docker container ${CONTAINER_NAME} is not running\n" && _print_usage 1;
# Check if destination was given as argument otherwise set destination to default
! "$_DFLAG" && DST_DIR="/home/pi/zwavejs-ui/store"
# Remove trailing slash unless root
case $DST_DIR in
*[!/]*/) DST_DIR=${DST_DIR%"${DST_DIR##*[!/]}"};;
*[/]) DST_DIR="/";;
esac
# Check if destination dir argument exists as a directory
[[ ! -d "$DST_DIR" || ! -w "$DST_DIR" ]] && echo -e "Error: '${DST_DIR}' does not exist or current user does not have writing permissions\n" && _print_usage 1;
}
###############################################################################
# Main #
###############################################################################
# _main()
#
# Usage:
# _main [<options>] [<arguments>]
#
# Description:
# Entry point for the program, handling basic option parsing and dispatching.
_main() {
# Avoid complex option parsing when only one program option is expected.
if [[ "${@:-}" =~ -h|--help ]]
then
_print_usage
else
_parse_commandline_arguments "$@"
_validate_parameters
# Extract certificate, key and certificate authority chain
_openssl x509 -in "${_TMPFILE}.pem" -outform PEM -out "${_TMPFILE}.cert.pem" || exit 1
_openssl pkey -out "${_TMPFILE}.key.pem" -in "${_TMPFILE}.pem" || exit 1
_openssl crl2pkcs7 -nocrl -certfile "${_TMPFILE}.pem" > "${_TMPFILE}.pkcs7" || exit 1
_openssl pkcs7 -print_certs -out "${_TMPFILE}.ca.pem" -in "${_TMPFILE}.pkcs7" || exit 1
# Copy certificate files
cp -af "${_TMPFILE}.cert.pem" ${DST_DIR}/${_ZWAVEJS_CERTS_FILE} || exit 1
cp -af "${_TMPFILE}.key.pem" ${DST_DIR}/${_ZWAVEJS_KEY_FILE} || exit 1
cp -af "${_TMPFILE}.ca.pem" ${DST_DIR}/${_ZWAVEJS_CA_FILE} || exit 1
# Set the correct permissions
chmod ${_CERT_PERMS} ${DST_DIR}/${_ZWAVEJS_CERTS_FILE} || exit 1
chmod ${_CERT_PERMS} ${DST_DIR}/${_ZWAVEJS_KEY_FILE} || exit 1
chmod ${_CERT_PERMS} ${DST_DIR}/${_ZWAVEJS_CA_FILE} || exit 1
# Restart docker container
"$_RFLAG" && docker restart ${CONTAINER_NAME} 2>&1 >/dev/null || exit 1
fi
}
# Call `_main` after everything has been defined.
_main "$@"
exit 0;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment