Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Last active May 30, 2020 20:46
Show Gist options
  • Save QNimbus/08dd3a44cddfc7b8ffc22aad1f012645 to your computer and use it in GitHub Desktop.
Save QNimbus/08dd3a44cddfc7b8ffc22aad1f012645 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
##
## Title: deploy_freenas.sh
## Description: Script to deploy uploaded certificates for use with VMWare ESXi
## Author: B. van wetten
## Created date: 28-01-2020
## Updated date: 12-02-2020
## Version: 0.3
## GitHub Gist: https://gist.github.com/QNimbus/08dd3a44cddfc7b8ffc22aad1f012645
##
## Usage: deploy_freenas.sh
## Notes: For remote use (e.g. SSH) use the following command as example:
## cat fullchain.cer /var/etc/acme-client/home/lan.besqua.red/lan.besqua.red.key | ssh -oStrictHostKeyChecking=no -T -i <keyfile> data-admin@<freenas host>
##
## You can use this in your 'authorized_keys' file to run this command remotely:
## command="/bin/cat | /usr/bin/xargs -0 -r -J % /mnt/storage/scripts/certs/deploy_freenas.sh -u <username> -p '<password>' -n <certname> -c % --",no-pty,from="1.2.3.4/32" ssh-rsa <public-key-here>== data-admin
# Shell utilities
RM=$(which rm); [[ $? != 0 ]] && echo "Command 'rm' not found" >&2 && exit 1
CP=$(which cp); [[ $? != 0 ]] && echo "Command 'cp' not found" >&2 && exit 1
MV=$(which mv); [[ $? != 0 ]] && echo "Command 'mv' not found" >&2 && exit 1
JQ=$(which jq); [[ $? != 0 ]] && echo "Command 'jq' not found" >&2 && exit 1
FIND=$(which find); [[ $? != 0 ]] && echo "Command 'find' not found" >&2 && exit 1
CURL=$(which curl); [[ $? != 0 ]] && echo "Command 'curl' not found" >&2 && exit 1
XARGS=$(which xargs); [[ $? != 0 ]] && echo "Command 'xargs' not found" >&2 && exit 1
PYTHON=$(which python); [[ $? != 0 ]] && echo "Command 'python' not found" >&2 && exit 1
OPENSSL=$(which openssl); [[ $? != 0 ]] && echo "Command 'openssl' not found" >&2 && exit 1
GETOPT=$(which getopt); [[ $? != 0 ]] && echo "Command 'getopt' not found" >&2 && exit 1
# Initialize variables
_PWD="/mnt/storage/scripts/certs"
_ME=$(basename "${0}")
_TMPFILE="tmp-${$}-${RANDOM}"
# Option strings
SHORT=hu:p:n:
LONG=help,username:,password:,name:
##########################################################################
# 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$) && ! -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
###############################################################################
# Functions #
###############################################################################
# _print_usage()
#
# Usage:
# _print_usage
#
# Print the program usage information.
_print_usage() {
cat <<HEREDOC
___ ____ ___ _ ____ _ _ ____ ____ ____ ____ _ _ ____ ____
| \ |___ |__] | | | \_/ |___ |__/ |___ |___ |\ | |__| [__
|__/ |___ | |___ |__| | ___ | | \ |___ |___ | \| | | ___]
Usage:
${_ME} -u username -p password -n name -c file
${_ME} -h
Options:
-h Show this screen
-n Certificate name
-c Certificate file
-u FreeNAS API access username
-p FreeNAS API access password
HEREDOC
exit ${1:-0}
}
# _json_escape()
#
# Usage:
# _json_escape variable value
#
# Escape a JSON object (e.g. for use with 'curl')
_json_escape () {
printf '%s' "$1" | ${PYTHON} -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
}
# _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
-u|--username)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable USERNAME "${2}"
shift 2
;;
-p|--password)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable PASSWORD "${2}"
shift 2
;;
-n|--name)
[[ -z "${2}" || "${2}" == *[[:space:]]* || "${2}" == -* ]] && { echo "$_ME: $1 needs a value" >&2; _print_usage 1; }
_set_variable CERT_NAME "${2}"
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 if $USERNAME and $PASSWORD are set and not empty
[[ -z "${USERNAME:-}" || -z "${PASSWORD:-}" ]] && echo -e "$_ME: Username and password are required\n" && _print_usage 1;
# 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 $USERNAME and $PASSWORD are set and not empty
[[ -z "${CERT_NAME:-}" ]] && echo -e "$_ME: Certificate name is required\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
chain=$(<${_TMPFILE}.chain.pem)
key=$(<${_TMPFILE}.key.pem)
chain=$(_json_escape "${chain}")
key=$(_json_escape "${key}")
# Check if certificate already exists
existing_cert_id=$(curl -X GET "https://${HOST:-localhost}/api/v2.0/certificate" -H "accept: */*" --silent --insecure --user "${USERNAME}:${PASSWORD}" | jq ".[] | select(.name == \"${CERT_NAME}\") | .id")
if [[ -n $existing_cert_id ]]
then
# Rename existing certificate to prevent conflict
rename_job_id=$(curl -X PUT "https://${HOST:-localhost}/api/v2.0/certificate/id/${existing_cert_id}" -H "accept: */*" -H "Content-Type: application/json" --silent --insecure --user "root:tvAQCGWx7aBCAhp&" -d "{\"name\":\"${CERT_NAME}-old\"}")
# [START] Give FreeNAS time to complete job
success=false
for retry in {0..5}
do
# Wait for 1 second
sleep 1
# Get job result
result=$(curl -X GET "https://${HOST:-localhost}/api/v2.0/core/get_jobs" -H "accept: */*" --silent --insecure --user "${USERNAME}:${PASSWORD}" | jq ".[] | select(.id == ${rename_job_id}) | .state")
# Stop looping if job was completed successfully
[[ "${result}" =~ SUCCESS ]] && success=true && break
done
[[ "${success}" = false ]] && echo -e "Failed to rename old certificate\nResult: ${result}\n" && exit 1;
# [END] Give FreeNAS time to complete job
fi
# Upload new certificate
create_job_id=$(curl -X POST "https://${HOST:-localhost}/api/v2.0/certificate" -H "accept: */*" -H "Content-Type: application/json" --silent --insecure --user "root:tvAQCGWx7aBCAhp&" -d "{\"name\":\"${CERT_NAME}\",\"certificate\":${chain},\"privatekey\":${key},\"create_type\":\"CERTIFICATE_CREATE_IMPORTED\"}")
# [START] Give FreeNAS time to complete job
success=false
for retry in {0..5}
do
# Wait for 1 second
sleep 1
# Get job result
result=$(curl -X GET "https://${HOST:-localhost}/api/v2.0/core/get_jobs" -H "accept: */*" --silent --insecure --user "${USERNAME}:${PASSWORD}" | jq ".[] | select(.id == ${create_job_id}) | .state")
# Stop looping if job was completed successfully
[[ "${result}" =~ SUCCESS ]] && success=true && break
done
[[ "${success}" = false ]] && echo -e "Failed to create new certificate\nResult: ${result}\n" && exit 1;
# [END] Give FreeNAS time to complete job
# Get new certificate id
new_cert_id=$(curl -X GET "https://${HOST:-localhost}/api/v2.0/certificate" -H "accept: */*" --silent --insecure --user "${USERNAME}:${PASSWORD}" | jq ".[] | select(.name == \"${CERT_NAME}\") | .id")
# If certificate was not found, exit (should not happen)
if [[ -z $new_cert_id ]]
then
echo -e "Could not find newly created certificate '${CERT_NAME}'\n" && exit 1;
fi
# Set new certificate as active
result=$(curl -X PUT "https://${HOST:-localhost}/api/v2.0/system/general" -H "accept: */*" -H "Content-Type: application/json" --write-out "%{http_code}" --output /dev/null --silent --insecure --user "root:tvAQCGWx7aBCAhp&" -d "{\"ui_certificate\":\"${new_cert_id}\"}")
[[ ! "${result}" =~ ^20[0-9]$ ]] && echo -e "Failed to activate new certificate\nResult: ${result}\n" && exit 1;
# If we found an existing certificate earlier - remove it
if [[ -n $existing_cert_id ]]
then
delete_cert_job_id=$(curl -X DELETE "https://${HOST:-localhost}/api/v2.0/certificate/id/${existing_cert_id}" -H "accept: */*" -H "Content-Type: application/json" --silent --insecure --user "root:tvAQCGWx7aBCAhp&")
# [START] Give FreeNAS time to complete job
success=false
for retry in {0..5}
do
# Wait for 1 second
sleep 1
# Get job result
result=$(curl -X GET "https://${HOST:-localhost}/api/v2.0/core/get_jobs" -H "accept: */*" --silent --insecure --user "${USERNAME}:${PASSWORD}" | jq ".[] | select(.id == ${delete_cert_job_id}) | .state")
# Stop looping if job was completed successfully
[[ "${result}" =~ SUCCESS ]] && success=true && break
done
[[ "${success}" = false ]] && echo -e "Failed to remove old certificate\nResult: ${result}\n" && exit 1;
# [END] Give FreeNAS time to complete job
fi
# Restart web ui (does not return HTTP 200 because web server restarts)
result=$(curl -X GET "https://${HOST:-localhost}/api/v2.0/system/general/ui_restart" -H "accept: */*" -H "Content-Type: application/json" --write-out "%{http_code}" --output /dev/null --silent --insecure --user "root:tvAQCGWx7aBCAhp&")
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