-
-
Save QNimbus/ee05d9cfe7edeca2a20481f1bc09c5d0 to your computer and use it in GitHub Desktop.
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.
- 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.
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
./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 tounifi
.
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 [email protected]
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.
- Generate SSH Key Pair on the OPNsense firewall.
- Install the Public Key on the remote host where the UniFi controller is running.
- Configure the
authorized_keys
Entry to execute thedeploy_unifi.sh
script upon SSH login, optionally restricting execution to specific client IP addresses for added security.
- 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.
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; |