Files
SysAdminAutomation/zimbra_backup.sh
T
jcoffey-dev f8a86736f9 Harden user_manage, update_system, and the Zimbra pair
user_manage.sh ran useradd/userdel/usermod with no privilege check at
all, so an ordinary user got "Failed to create user." with no hint that
root was the missing piece. Mutating subcommands now require root while
listusers/listgroups stay open, and the check runs *after* the
subcommand is recognised so a bare invocation still prints usage instead
of complaining about privileges. Account names are validated before
reaching useradd, and `deluser` -- which removes the home directory
irrecoverably -- prints what it will delete and confirms first.

zimbra_backup.sh reported success on failed backups. getRestURL can
write an HTTP error body and still exit zero, so a " Backup completed"
could sit over a file containing an error page. Size and gzip -t checks
now gate that, and a suspect file is renamed .suspect rather than
deleted, so it can be looked at. zimbra_restore.sh likewise validates
the archive before starting a restore from it.

Both Zimbra scripts had `cmd` followed by `if [ $? -eq 0 ]`. Adding
set -e to those would have made the error branches unreachable -- set -e
exits before the check -- so the tests are inline instead. That would
have been a silent regression rather than a visible one.

update_system.sh gained strict mode, and a note on the pacman branch:
Arch has no supported partial-upgrade path, and --noconfirm answers away
the prompts that would otherwise warn.

Integrity checks verified against an error page, a truncated archive, a
non-gzip file and a real one.
2026-08-22 22:22:05 -07:00

96 lines
3.9 KiB
Bash

#!/bin/bash
#
# Copyright (C) 2025 LINUXexpert.org
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.
set -euo pipefail
# Ensure script is run as root or sudo
if [ "$EUID" -ne 0 ]; then
echo "❌ This script must be run as root or with sudo."
exit 1
fi
# Prompt for email and backup directory
read -p "Enter Zimbra username (email address): " EMAIL
read -p "Enter backup directory (absolute path) [/opt/zimbra/backups]: " BACKUP_DIR
# Reject anything that is not a plain address. $EMAIL is used both as a
# command argument and as part of the backup filename, so a "/" here
# would write outside $BACKUP_DIR entirely.
if ! [[ "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "❌ Not a valid email address: $EMAIL"
exit 1
fi
# Use default if none provided
BACKUP_DIR=${BACKUP_DIR:-/opt/zimbra/backups}
# Ensure directory exists
mkdir -p "$BACKUP_DIR"
chown zimbra:zimbra "$BACKUP_DIR"
# Generate timestamped filename
TIMESTAMP=$(date +%F_%H-%M-%S)
BACKUP_FILE="${BACKUP_DIR}/${EMAIL}_${TIMESTAMP}.tgz"
# Confirm action
echo "Backing up mailbox for $EMAIL to $BACKUP_FILE"
read -p "Proceed? (y/n): " CONFIRM
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
echo "❌ Backup cancelled."
exit 0
fi
# Run zmmailbox command as zimbra user
echo "📦 Starting backup..."
# The script body is single-quoted so nothing is interpolated into it;
# $EMAIL arrives as a positional argument instead. Interpolating it (as
# this line previously did) let any shell metacharacter in the address
# run commands as the zimbra user, which owns the whole mail store.
# The redirect is performed by this shell, which is root -- not by the
# sudo'd zimbra process. That is intentional (root can always write
# here, and a pipe into `tee` would put tee's exit status in $? and mask
# a zmmailbox failure), but it means the file lands root-owned inside a
# directory chowned to zimbra, so ownership is handed over below.
# shellcheck disable=SC2024
# Tested inline rather than via `$?` on the next line: under set -e a
# failure would exit before any check ran, making the error branch below
# dead code.
if sudo -u zimbra bash -c '/opt/zimbra/bin/zmmailbox -z -m "$1" getRestURL "//?fmt=tgz"' _ "$EMAIL" > "$BACKUP_FILE"; then
# A zero exit is not sufficient: getRestURL can write an HTTP error
# body and still succeed, which previously produced a cheerful "✅"
# over a file containing an error page. Require a plausible size and
# a readable gzip container.
size="$(stat -c %s -- "$BACKUP_FILE" 2>/dev/null || echo 0)"
if [ "$size" -lt 1024 ]; then
echo "❌ Backup is only ${size} bytes -- almost certainly an error response, not a mailbox."
echo " Leaving it at ${BACKUP_FILE}.suspect for inspection."
mv -- "$BACKUP_FILE" "${BACKUP_FILE}.suspect"
exit 1
fi
if ! gzip -t -- "$BACKUP_FILE" 2>/dev/null; then
echo "❌ Backup is not a valid gzip stream -- treating as failed."
echo " Leaving it at ${BACKUP_FILE}.suspect for inspection."
mv -- "$BACKUP_FILE" "${BACKUP_FILE}.suspect"
exit 1
fi
chown zimbra:zimbra "$BACKUP_FILE" 2>/dev/null || true
echo "✅ Backup completed: $BACKUP_FILE ($(numfmt --to=iec "$size" 2>/dev/null || echo "$size bytes"))"
else
echo "❌ Backup failed. Check if the user exists or zmmailbox is working."
rm -f -- "$BACKUP_FILE"
exit 1
fi