scripts/ab-openldap/backup.sh
2020-09-14 16:25:05 -06:00

600 lines
24 KiB
Bash
Executable File

#!/bin/sh
#
### backup openLDAP configuration and frontend database(s)
# version 3.2
#
### text formatting presets
# set colours for various log message types
bold=$(tput bold)
err=$(tput bold)$(tput setaf 1)
info=$(tput sgr0)
norm=$(tput sgr0)
ok=$(tput setaf 2)
# define other colours used
cyan=$(tput bold)$(tput setaf 6)
magenta=$(tput sgr0)$(tput setaf 5)
yellow=$(tput sgr0)$(tput setaf 3)
### parameter defaults
width=$(tput cols)
scriptPath="$( CDPATH='' cd -- "$( dirname -- "$0" )" && pwd -P )"
scriptName="$( basename "$0" )"
# logfile default: same location and name as this script, with '.log' extension
logfile="$scriptPath/${scriptName%.*}.log"
# encryption parameters file default: same location and name as this script,
# with '.params' extension
encParams="$scriptPath/${scriptName%.*}.params"
# backup mode by default
unset removeTLS
unset backupFile
extract=false
decrypt=false
# encryption enabled by default
noEncryption=0
tempDir="$(date '+%s')"
fileDate="$(date '+%F_%T')"
outputLocation="$scriptPath/"
# frontend starts at '1', so this number should always be the number of
# frontend databases + 1 -- default to only 1 frontend db
numFrontEnd=2
### functions
cleanup () {
logInfo 'task' 'Cleaning up'
if ! docker exec "$container" rm -rf "$tempDir" \
>> "$logfile" 2>&1; then
logInfo 'err'
exitError 3 'Unable to remove temporary files in container.' 'nc'
fi
logInfo 'done'
}
consoleError () {
printf "%s\n%s\n" "$err" "$2"
printf "Exiting.\n\n%s" "$norm"
exit "$1"
}
decryptionNote () {
printf "\n"
textblock "${bold}${magenta}Decrypting your backup archive:${norm}"
printf "\n"
textblock 'To decrypt and extract your backup file, you need the following information:'
textblock '1. The password used to encrypt the file'
textblock '2. The encryption cipher used (default: AES-256-CBC)'
textblock '3. The number of iterations used to create the private key (default: 25000)'
printf "\n"
textblock "Run the following command, replacing the ${cyan}[cyan stuff in square brackets]${norm} with values appropriate to your environment."
printf "\n"
textblock "openssl enc -d -${cyan}[cipher-here]${norm} -iter +${cyan}[iterations]${norm} -k ${cyan}[password]${norm} -in ${cyan}[backup_archive.tar.gz]${norm} | tar -xz -C ${cyan}[destinationDirectory]${norm}"
printf "\n"
textblock 'NOTE: The output directory must already exist or tar will throw an error.'
printf "\n"
textblock "${magenta}Example:${norm}"
textblock "openssl enc -d -${cyan}aes-256-cbc${norm} -iter +${cyan}25000${norm} -k ${cyan}mypassword${norm} -in ${cyan}~/ldap-2020-03-10_17:31:34.tar.gz${norm} | tar -xz -C ${cyan}~/ldapRestore${norm}"
printf "\n"
textblock 'Note: Using this script with the --decrypt option handles this for you and can create target directories automatically.'
printf "\n\n"
exit 0
}
exitError () {
# cleanup temp directory unless 'nc' passed
if [ ! "$3" = "nc" ]; then cleanup; fi
# log error
printf "%s[%s] -- [ERROR] code %s: %s --%s\n" \
"$err" "$(stamp)" "$1" "$2" "$norm" >> "$logfile"
printf "%s[%s] --- %s terminated with errors ---\n%s" \
"$err" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
# exit with proper error code
exit "$1"
}
logInfo () {
if [ "$1" = 'task' ]; then
printf "%s[%s] -- [INFO] %s... " \
"$info" "$(stamp)" "$2" >> "$logfile"
elif [ "$1" = 'done' ]; then
if [ -z "$2" ]; then
printf "%sdone%s --%s\n" \
"$ok" "$info" "$norm" >> "$logfile"
else
printf "%s%s%s --%s\n" \
"$ok" "$2" "$info" "$norm" >> "$logfile"
fi
elif [ "$1" = 'err' ]; then
printf "%serror%s --%s\n" \
"$err" "$info" "$norm" >> "$logfile"
else
printf "%s[%s] -- [INFO] %s --%s\n" \
"$info" "$(stamp)" "$1" "$norm" >> "$logfile"
fi
}
logSuccess () {
printf "%s[%s] -- [SUCCESS] %s --%s\n" \
"$ok" "$(stamp)" "$1" "$norm" >> "$logfile"
}
scriptHelp () {
printf "\n"
textblock "${bold}${magenta}Usage: ${scriptName} [parameters]${norm}"
printf "\n"
textblock "${cyan}Parameters ${yellow}(default value):${norm}"
textblock "There are NO mandatory parameters. By default the script will run in 'backup' mode and save an encrypted backup archive to the current directory. If a parameter is not supplied, its default value will be used. In the case of a switch parameter, it will remain deactivated if not specified."
printf "\n"
textblock "${bold}*** Common parameters ***${norm}"
printf "\n"
textblock "${cyan}-l, --log ${yellow}(scriptPath/scriptName.log)${norm}"
textblock "FULL path to write log file. If you supply a path ending with a slash ('/') it will be assumed you mean a directory and the log file will be written to that directory using the format 'path/scriptname.log'. If you supply only a filename (no slashes anywhere), it will assumed you want to save the log using that name in the script directory. The script will attempt to create any provided paths/directories if they do not exist."
printf "\n"
textblock "${cyan}-o, --output ${yellow}(scriptPath/)${norm}"
textblock "Location where the output files should be saved on this machine. You should only specify a *directory* here (trailing slash optional). File names are automatic and cannot be changed via this script. All restore operations will create a 'restore' subdirectory in this specified directory."
printf "\n"
textblock "${cyan}-p, --params ${yellow}(scriptPath/scriptName.params)${norm}"
textblock "Location of the encryption parameters file. This file contains the password and encryption options that should be used. By default, the script looks in the script directory for a file named the same as the script but with a '.params' extension."
printf "\n"
textblock "${cyan}-h, -? | -??, --help${norm}"
textblock "Quick-help screen. | This (detailed) help screen."
printf "\n"
textblock "${bold}*** Encryption parameters ***${norm}"
printf "\n"
textblock "${cyan}-n, --num, --frontend ${yellow}(1)${norm}"
textblock "Number of frontend databases to backup. If you specify a number here greater than actually exist, openLDAP will generate an error and your backup will fail."
printf "\n"
textblock "${cyan}--no-encryption${norm}"
textblock "Switch parameter. Specify this if you DO NOT want the backup archive tar.gz file encrypted. Careful!"
printf "\n"
textblock "${bold}*** Decryption/Extraction parameters ***${norm}"
printf "\n"
textblock "${cyan}-b, --backupfile${norm}"
textblock "FULL path of the backup file you want to decrypt/extract. MUST be supplied if using the --decrypt or --extract switches."
printf "\n"
textblock "${cyan}-d, --decrypt ${yellow}(implies --extract)${norm}"
textblock "Switch parameter. Decrypt and extract your backup file (MUST be supplied using the --backupfile parameter) to the 'restore' subdirectory of the current directory or as specified using the '--output' parameter. This switch will log error code 8 if you try to decrypt an unencrypted backup archive."
printf "\n"
textblock "${cyan}-e, --extract${norm}"
textblock "Switch parameter. Extract your backup file (MUST be supplied using the --backupfile parameter) to the 'restore' subdirectory of the current directory or as specified using the '--output' parameter. This switch will log error code 8 if you try to extract an encrypted backup archive. In that case, use --decrypt instead."
printf "\n"
textblock "${cyan}--removeTLS${norm}"
textblock "Switch parameter. Remove olcTLS configuration parameters from your config DIT LDIF file. This is the only approved way to disable/change your TLS settings. Consult the openLDAP documentation or the repo wiki for more information."
printf "\n"
textblock "${cyan}--manualdecryption${norm}"
textblock "Display instructions on how to manually decrypt and extract your backup archive."
printf "\n\n"
textblock "More details and examples of script usage can be found in the repo wiki at ${magenta}https://git.asifbacchus.app/ab-docker/openldap/wiki${norm}"
printf "\n"
}
stamp () {
(date +%F' '%T)
}
textblock () {
printf "%s\n" "$1" | fold -w "$width" -s
}
quickHelp () {
textblock "${bold}${magenta}Usage: ${scriptName} [parameters]${norm}"
printf "\n"
textblock "${cyan}Parameters ${yellow}(default value):${norm}"
printf "\n"
textblock "${bold}*** Common parameters ***${norm}"
textblock "${cyan}-l, --log ${yellow}(scriptPath/scriptName.log)${norm}"
textblock "FULL path to write log file. Path ending with '/' will be treated as a directory. Filename only will be saved in script directory."
textblock "${cyan}-o, --output ${yellow}(scriptPath/)${norm}"
textblock "Directory to save output files on this machine."
textblock "${cyan}-p, --params ${yellow}(scriptPath/scriptName.params)${norm}"
textblock "Location of the encryption parameters file."
textblock "${cyan}-h, -? | -??, --help${norm}"
textblock "This help screen. | Detailed help."
printf "\n"
textblock "${bold}*** Encryption parameters ***${norm}"
textblock "${cyan}-n, --num, --frontend ${yellow}(1)${norm}"
textblock "Number of frontend databases to backup."
textblock "${cyan}--no-encryption${norm}"
textblock "Switch: DO NOT encrypt backup tar.gz."
printf "\n"
textblock "${bold}*** Decryption/Extraction parameters ***${norm}"
textblock "${cyan}-b, --backupfile${norm}"
textblock "FULL path of the backup file to be decrypted/extracted."
textblock "${cyan}-d, --decrypt ${yellow}(implies --extract)${norm}"
textblock "Decrypt and extract specified backup file to 'restore' subdirectory of current path/specified path (--output parameter)."
textblock "${cyan}-e, --extract${norm}"
textblock "Extract specified backup file to 'restore' subdirectory of current path/specified path (--output parameter)."
textblock "${cyan}--removeTLS${norm}"
textblock "Remove all olcTLS configuration settings from your config DIT LDIF restore file."
textblock "${cyan}--manualdecryption${norm}"
textblock "Display instructions on manually decrypting and extracting your backup archive."
printf "\n"
textblock "Run script with '-?? or --help' for detailed help. More details and examples of script usage can be found in the repo wiki at:"
printf "%s\thttps://git.asifbacchus.app/ab-docker/openldap/wiki%s\n\n" \
"$magenta" "$norm"
}
### pre-requisite checks
# is user root or in the docker group?
if [ ! "$( id -u )" -eq 0 ]; then
if ! id -Gn | grep docker > /dev/null; then
consoleError '1' "You must either be root or in the 'docker' group to run this script since you must be able to run 'docker exec' commands against the container!"
fi
fi
# is tar installed?
if ! command -v tar > /dev/null; then
consoleError '99' 'It appears that tar is not installed. This script requires tar in order to package and process backup files.'
fi
# is gzip installed?
if ! command -v gzip > /dev/null ; then
consoleError '99' 'It appears that gzip is not installed. This script requires gzip in order to compress/decompress backup tarball files.'
fi
# process startup parameters
while [ $# -gt 0 ]; do
case "$1" in
-b|--backupfile)
# name of backup file to decrypt/extract
if [ -z "$2" ]; then
consoleError '1' 'Backup file name cannot be null.'
elif [ ! -f "$2" ]; then
consoleError '1' "Specified backup file does not exist ($2)."
fi
backupFile="$2"
shift
;;
-d|--decrypt)
# decrypt and extract backup file (implies '-e')
decrypt=true
extract=true
;;
-e|--extract)
# extract gzipped tarball
extract=true
;;
-h|-\?)
# display brief help
quickHelp
exit 0
;;
-\?\?|--help)
# display full help
scriptHelp
exit 0
;;
-l|--log)
# location of log file
if [ -z "$2" ]; then
consoleError '1' 'Log file path cannot be null. Leave unspecified to save log in the same directory as this script.'
fi
logfile="$2"
shift
;;
--manualdecryption)
decryptionNote
exit 0
;;
-n|--num|--frontend)
# number of frontend databases
if [ -z "$2" ]; then
consoleError '1' 'Number of frontend databases cannot be null.'
fi
numFrontEnd=$(($2+1))
shift
;;
--no-encryption)
# disable encryption, switch parameter
noEncryption=1
# switches do not require an internal 'shift'
;;
-o|--output)
# directory on host to save output files
if [ -z "$2" ]; then
consoleError '1' 'Output destination location cannot be null.'
fi
outputLocation="$2"
shift
;;
-p|--params)
# path to encryption parameters file
if [ -z "$2" ]; then
consoleError '1' 'Path to encryption parameters file cannot be null.'
fi
encParams="$2"
shift
;;
--removeTLS)
# remove olcTLS entries from cn=config
removeTLS=true
;;
*)
printf "%s\nUnknown option: %s\n" "$err" "$1"
printf "Use '--help' for valid options.\n\n%s" "$norm"
exit 1
;;
esac
shift
done
### start logging
# logfile checks
if ! printf "%s" "$logfile" | grep -o / > /dev/null; then
# filename provided, save in scriptdir
logfile="$scriptPath/$logfile"
elif [ "$( printf "%s" "$logfile" | tail -c 1 )" = '/' ]; then
# directory provided, does it exist?
if [ -d "$logfile" ]; then
logfile="${logfile}${scriptName%.*}.log"
else
if ! mkdir -p "$logfile" > /dev/null 2>&1; then
consoleError '1' 'Unable to make specified log file directory.'
fi
logfile="${logfile}${scriptName%.*}.log"
fi
else
# full path provided, does the parent directory exist?
if [ ! -d "${logfile%/*}" ]; then
# make parent path
if ! mkdir -p "${logfile%/*}" > /dev/null 2>&1; then
consoleError '1' 'Unable to make specified log file path.'
fi
fi
fi
# write initial log entries for script run
if ! printf "%s[%s] --- Start %s execution ---%s\n" \
"$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logfile"; then
consoleError '1' "Unable to write to log file ($logfile)"
fi
logInfo "Log located at $logfile"
### process output directory
if [ -d "$outputLocation" ]; then
# touch test to ensure we can write here
if ! touch "${outputLocation%/}/test.touch" > /dev/null 2>&1; then
exitError '1' "Unable to write to output location: $outputLocation" 'nc'
else
logInfo "Writing output to: $outputLocation"
rm -f "${outputLocation%/}/test.touch" > /dev/null 2>&1
fi
else
# create directory
if ! mkdir -p "${outputLocation%/}" > /dev/null 2>&1; then
exitError '1' "Unable to create output path: $outputLocation" 'nc'
else
logInfo "Writing output to: $outputLocation"
fi
fi
### source encryption parameters file
# check if file exists (unless encryption disabled), otherwise exit
if [ $noEncryption = 1 ]; then
logInfo 'Backup archive will NOT be encrypted!'
elif [ $noEncryption = 0 ] && [ ! -f "$encParams" ]; then
exitError 1 'Encryption parameters file does not exist.' 'nc'
else
# import encryption parameters file
case "${encParams}" in
/*)
# absolute path, no need to rewrite
. "${encParams}"
;;
*)
# relative path, need to rewrite assuming current directory
. "./${encParams}"
;;
esac
logInfo "Imported: '$encParams'"
# verify import
logInfo 'task' 'Verify encryption password'
if [ -z "$password" ]; then
logInfo 'err'
exitError 1 "Imported null value for encryption 'password'." 'nc'
else
logInfo 'done'
fi
logInfo 'task' 'Verify encryption cipher'
if [ -z "$encryptionCipher" ]; then
logInfo 'err'
exitError 1 "Imported null value for 'encryptionCipher'." 'nc'
else
logInfo 'done' "$encryptionCipher"
fi
logInfo 'task' 'Verify encryption key iterations'
if [ -z "$encryptionIterations" ]; then
logInfo 'err'
exitError 1 "Imported null value for 'encryptionIterations'." 'nc'
else
logInfo 'done' "$encryptionIterations"
fi
fi
if [ $extract = 'true' ]; then
# ensure backupFile has been specifed
if [ -z "$backupFile" ]; then
exitError '1' 'backupFile (-b|--backupFile) must be specified when using script in Extract mode.' 'nc'
fi
# extract backupFile to outputLocation
logInfo "Extracting backup file ($backupFile)"
# create extraction target directory
if [ ! -d "${outputLocation%/}/restore" ]; then
# create subdirectory for restored files
if ! mkdir "${outputLocation%/}/restore" > /dev/null 2>&1; then
exitError '7' 'Could not create target subdirectory in output location.' 'nc'
else
logInfo "Extracted files will be written to '${outputLocation%/}/restore/'"
fi
else
# ensure we can write to subdirectory for restored files
if ! touch "${outputLocation%/}/restore/test.touch" > /dev/null 2>&1; then
exitError '1' "Unable to write to output location: ${outputLocation%/}/restore/" 'nc'
else
rm -f "${outputLocation%/}/restore/test.touch" > /dev/null 2>&1
fi
fi
# extract/decrypt backup file
if [ "$decrypt" = 'true' ]; then
logInfo 'Decrypting backup file before extraction'
if ! openssl enc -d -"${encryptionCipher}" -iter +"${encryptionIterations}" -k "${password}" -in "${backupFile}" 2>> "$logfile" | tar --overwrite -xz -C "${outputLocation%/}/restore/" 2>> "$logfile"; then
exitError '8' 'There was a problem extracting the backup file. Perhaps the file is NOT encrypted? If so, please run with the --extract flag instead of the --decrypt flag.' 'nc'
else
logSuccess "Backup extracted to '${outputLocation%/}/restore/'"
fi
else
if ! tar --overwrite -xzf "$backupFile" -C "${outputLocation%/}/restore/" 2>> "$logfile"; then
exitError '8' 'There was a problem extracting the backup file. Perhaps it is encrypted? If so, please run with --decrypt flag.' 'nc'
else
logSuccess "Backup extracted to '${outputLocation%/}/restore/'"
fi
fi
# remove TLS configuration
if [ "$removeTLS" = 'true' ]; then
# get newest config-? file in outputLocation/restore directory
cnConfigFile=$( ls -Adt ${outputLocation%/}/restore/config-* | head -n1 )
# remove TLS configuration entries
logInfo 'task' "Removing TLS configuration from '$cnConfigFile'"
sed -i -e '/^olcTLS/d' "$cnConfigFile" 2>> "$logfile"
# check our work
if ! grep -q '^olcTLS' "$cnConfigFile" 2>> "$logfile"; then
logInfo 'done'
else
logInfo 'err'
exitError '20' 'olcTLS entries could not be removed from restored config DIT LDIF file. You will have to do it manually.' 'nc'
fi
fi
elif [ $extract = 'false' ]; then
### process backup operations
## find ab-openldap container
container=$(docker ps -a --no-trunc --filter "label=org.label-schema.name=ab-openldap" --format "{{ .Names }}")
# check for null value -- cannot find container
if [ -z "$container" ]; then
exitError 2 'Cannot find ab-openldap container. Exiting.' 'nc'
fi
# check for multiple containers, exit if that's the case
# N.B. do NOT quote $container or this loop will NOT work!!!
set -- dummy $container
shift
containerCount=0
for c; do
containerCount=$((containerCount+1))
logInfo "Found container($containerCount): $c"
done
if [ "$containerCount" -gt 1 ]; then
exitError 2 'Multiple containers found. Exiting.' 'nc'
fi
## backup databases to ldif
# create temp working directory
if ! docker exec "$container" mkdir "/$tempDir" >> "$logfile" 2>&1; then
exitError 3 'Could not create temporary working directory.' 'nc'
fi
# backend to ldif
logInfo 'task' 'Backup configuration database'
if ! docker exec "$container" sh -c \
"slapcat -F /etc/openldap/ldif -n 0 -l ${tempDir}/config-${fileDate}.ldif" \
>> "$logfile" 2>&1; then
logInfo 'err'
exitError 4 'Could not backup configuration database.'
fi
logInfo 'done'
# iterate frontend databases and export to ldif
i=1
while [ "$i" -ne "$numFrontEnd" ]; do
logInfo 'task' "Backup frontend database $i"
if ! docker exec "$container" sh -c \
"slapcat -F /etc/openldap/ldif -n ${i} -l ${tempDir}/mdb${i}-${fileDate}.ldif" \
>> "$logfile" 2>&1; then
logInfo 'err'
exitError 4 "Could not backup frontend database $i."
else
logInfo 'done'
fi
i=$((i+1))
done
# compress and encrypt exported ldif files
if [ $noEncryption = 0 ]; then
logInfo 'task' 'Compressing and encrypting backup'
if ! docker exec -w "/$tempDir" "$container" sh -c \
"tar -czf - * | openssl enc -e -${encryptionCipher} -iter +${encryptionIterations} -k ${password} -out ./ldap-${fileDate}.tar.gz" \
>> "$logfile" 2>&1; then
logInfo 'err'
exitError 5 'Could not securely archive backup files.'
fi
logInfo 'done'
elif [ $noEncryption = 1 ]; then
logInfo 'task' 'Compressing backup'
if ! docker exec -w "/$tempDir" "$container" sh -c \
"tar -czf ldap-${fileDate}.tar.gz *" \
>> "$logfile" 2>&1; then
logInfo 'err'
exitError 5 'Could not archive backup files.'
fi
logInfo 'done'
fi
## copy file to output location on host
logInfo 'task' 'Copying archive from docker container to host'
if ! docker cp "$container:/$tempDir/ldap-${fileDate}.tar.gz" "$outputLocation/" \
>> "$logfile" 2>&1; then
logInfo 'err'
exitError 6 'Unable to copy backup archive from container to host.'
fi
logInfo 'done'
logInfo "Backup file: ${outputLocation%/}/ldap-${fileDate}.tar.gz"
## cleanup and log success
cleanup
logSuccess 'Backup completed successfully.'
fi
### exit gracefully
logSuccess 'All processes completed'
printf "%s[%s] --- %s execution completed ---\n%s" \
"$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
exit 0
# error code reference:
# 0: exited normally, no errors
# 1: parameter or permissions error
# 2: unsupported number of containers (0 or more than 1)
# 3: could not create/remove temporary directory/files
# 4: problem exporting database(s)
# 5: problem compressing/encrypting tar.gz archive
# 6: problem copying encrypted archive to host
# 7: problem creating target subdirectory in outputLocation
# 8: problem extracting gzipped tarball file
# 20: problem removing TLS entries
# 99: dependencies not installed (tar, gzip)
#EOF