#!/bin/sh # ### backup openLDAP configuration and frontend database(s) # version 3.0 # ### 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, it's 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}--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}--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 "${outputLocation%/}/restore" -t | head -n1 ) # remove TLS configuration entries logInfo 'task' "Removing TLS configuration $cnConfigFile... " sed -i -e '/^olcTLS/d' "$cnConfigFile" # check our work if ! grep -q '^olcTLS' "$cnConfigFile"; 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