Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Last active February 6, 2024 10:15
Show Gist options
  • Save QNimbus/1d27f8f44912545f047d3254cb51de70 to your computer and use it in GitHub Desktop.
Save QNimbus/1d27f8f44912545f047d3254cb51de70 to your computer and use it in GitHub Desktop.
#!/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.8
## GitHub Gist: https://gist.github.com/QNimbus/1d27f8f44912545f047d3254cb51de70
##
## Usage: deploy_unifi.sh -c certificates [-r] [-d destination] [-n container]
## deploy_unifi.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 % /opt/certs/scripts/deploy_unifi.sh -r -n unifi -d /opt/certs/unifi -c --%
##
## You can use this in your 'authorized_keys' file to run this command remotely:
## command="/bin/cat | /bin/xargs -0 -I % /opt/certs/scripts/deploy_unifi.sh -r -n unifi -d /opt/certs/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
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
DOCKER=$(which docker); [[ $? != 0 ]] && echo "Command 'docker' 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}"
_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 file [-r] [-d destination] [-n container]
${_ME} -h
Options:
-h, --help Show this screen
-r, --restart Restart UniFi docker container
-c, --certificate Certificate file
-n, --container Unifi docker container name
-d, --destination Destination folder
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"
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} /bin/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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment