Scripts
Copy/paste ready helpers for Debian administration
A growing collection of scripts used on Debian servers. These are written for safe operation: strict error handling, predictable output, and correct permissions.
Installing scripts
Quick and safe method
Recommended approach: install into /usr/local/sbin, owned by root, executable by admins.
sudo install -d -m 0750 /usr/local/sbin
sudo install -m 0750 -o root -g root /dev/stdin /usr/local/sbin/SCRIPTNAME.sh <<'EOF'
#!/bin/bash
set -euo pipefail
echo "Replace me"
EOF
sudo hash -r
Tip: keep script files owned by root:root. Avoid world-writable locations.
backup-dirs.sh
Backup directories and files into a single archive
This script creates a timestamped .tar.gz archive and a matching SHA256 checksum file.
It supports both directories and individual files, for example:
backup-dirs.sh /etc/dovecot /etc/postfix /etc/nginx
backup-dirs.sh /etc/fstab /etc/aliases /etc/hosts
backup-dirs.sh /etc/postfix/main.cf /etc/postfix/master.cf
#!/bin/bash
set -euo pipefail
umask 077
DESTDIR="/root/bck"
TS="$(date +%F-%H%M%S)"
HOST="$(hostname -s 2>/dev/null || echo host)"
ARCHIVE_BASENAME="backup-${HOST}-${TS}.tar.gz"
ARCHIVE="${DESTDIR}/${ARCHIVE_BASENAME}"
SHAFILE="${ARCHIVE}.sha256"
usage() {
cat <<'USAGE'
Usage:
backup-dirs.sh /abs/path1 [/abs/path2 ...]
Examples:
backup-dirs.sh /etc/dovecot /etc/postfix /etc/nginx
backup-dirs.sh /etc/dovecot /etc/aliases /etc/fstab
backup-dirs.sh /etc/postfix/main.cf /etc/postfix/master.cf
USAGE
}
install -d -m 0700 "$DESTDIR"
DEFAULT_PATHS=(
# "/etc/dovecot"
# "/etc/postfix"
# "/etc/aliases"
)
PATHS=()
if (( $# > 0 )); then
PATHS=("$@")
else
if (( ${#DEFAULT_PATHS[@]} == 0 )); then
usage >&2
echo "ERROR: No paths specified and DEFAULT_PATHS is empty." >&2
exit 2
fi
PATHS=("${DEFAULT_PATHS[@]}")
fi
for p in "${PATHS[@]}"; do
if [[ "$p" != /* ]]; then
echo "ERROR: Path must be absolute: $p" >&2
exit 2
fi
if [[ ! -e "$p" && ! -L "$p" ]]; then
echo "ERROR: Path does not exist: $p" >&2
exit 2
fi
if [[ -b "$p" || -c "$p" || -p "$p" || -S "$p" ]]; then
echo "ERROR: Refusing special file (block/char/fifo/socket): $p" >&2
exit 2
fi
done
REL=()
for p in "${PATHS[@]}"; do
REL+=("${p#/}")
done
tmp="${ARCHIVE}.tmp"
tar --create \
--gzip \
--file "$tmp" \
--preserve-permissions \
--acls \
--xattrs \
--numeric-owner \
--warning=no-file-changed \
-C / \
"${REL[@]}"
mv -f -- "$tmp" "$ARCHIVE"
tar -tzf "$ARCHIVE" >/dev/null
(
cd "$DESTDIR"
sha256sum "$ARCHIVE_BASENAME" > "${ARCHIVE_BASENAME}.sha256"
)
echo "OK: $ARCHIVE"
echo "OK: $SHAFILE"
mailctl
Unified mail administration wrapper
mailctl is the single entrypoint for mail administration actions.
It dispatches to helper scripts under /usr/local/sbin and logs actions to
/var/log/mailctl.log.
mailctl help
mailctl list
mailctl domain add
mailctl domain delete
mailctl user add
mailctl user delete
mailctl user passwd
mailctl alias add
mailctl alias delete
#!/bin/bash
# mailctl - unified mail administration wrapper
set -euo pipefail
umask 027
PATH="/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/sbin:/usr/local/bin"
export PATH
BASE="/usr/local/sbin"
LOGFILE="/var/log/mailctl.log"
die() {
echo "ERROR: $*" >&2
exit 1
}
if [[ "${EUID}" -ne 0 ]]; then
die "mailctl must be run as root"
fi
if [[ ! -e "$LOGFILE" ]]; then
install -m 0640 -o root -g adm /dev/null "$LOGFILE"
fi
log() {
local username="${SUDO_USER:-root}"
echo "$(date '+%Y-%m-%d %H:%M:%S') [mailctl] user=${username} cmd=\"$*\"" >> "$LOGFILE"
}
main_help() {
cat <<'EOF'
mailctl - Mail server administration
Usage:
mailctl <object> <action>
mailctl <action>
Objects & actions:
domain add Add a mail domain
domain delete Delete a mail domain (and all users/aliases)
user add Add a mailbox
user delete Delete a mailbox
user passwd Change mailbox password
alias add Add a mail alias
alias delete Delete a mail alias
Single-word commands:
list Show complete mail setup
Help:
mailctl help
mailctl help domain
mailctl help user
mailctl help alias
EOF
}
domain_help() {
cat <<'EOF'
mailctl domain
Commands:
mailctl domain add
mailctl domain delete
Deleting a domain removes all users and aliases via ON DELETE CASCADE.
EOF
}
user_help() {
cat <<'EOF'
mailctl user
Commands:
mailctl user add
mailctl user delete
mailctl user passwd
EOF
}
alias_help() {
cat <<'EOF'
mailctl alias
Commands:
mailctl alias add
mailctl alias delete
EOF
}
usage_error() {
echo "Invalid command" >&2
echo >&2
main_help >&2
exit 1
}
require_script() {
local f="$1"
[[ -x "$f" ]] || die "Missing or not executable: $f"
}
dispatch() {
local script="$1"
shift || true
require_script "$script"
log "$(basename "$script") $*"
exec "$script" "$@"
}
CMD="${1:-}"
OBJ="${2:-}"
if [[ "$CMD" == "help" || -z "$CMD" ]]; then
case "$OBJ" in
"" ) main_help ;;
domain ) domain_help ;;
user ) user_help ;;
alias ) alias_help ;;
* ) usage_error ;;
esac
exit 0
fi
if [[ "$CMD" == "list" ]]; then
dispatch "$BASE/list-mailsetup.sh"
fi
case "$CMD $OBJ" in
"domain add") dispatch "$BASE/add-domain.sh" ;;
"domain delete") dispatch "$BASE/delete-domain.sh" ;;
"user add") dispatch "$BASE/add-mailuser.sh" ;;
"user delete") dispatch "$BASE/delete-mailuser.sh" ;;
"user passwd") dispatch "$BASE/set-mailpassword.sh" ;;
"alias add") dispatch "$BASE/add-alias.sh" ;;
"alias delete") dispatch "$BASE/delete-alias.sh" ;;
*)
usage_error
;;
esac
Mail database helper scripts
Domain / user / alias management
These scripts manage the tables: virtual_domains, virtual_users, and virtual_aliases.
They are designed to be used via mailctl.
add-domain.sh
#!/bin/bash
# add-domain.sh - create virtual mail domain
set -euo pipefail
DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"
read -rp "Domain name (e.g. example.nl): " DOMAIN
if ! [[ "$DOMAIN" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "ERROR: Invalid domain name"
exit 1
fi
EXISTS=$($MYSQL "$DB" -e \
"SELECT 1 FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")
if [[ -n "$EXISTS" ]]; then
echo "ERROR: Domain already exists: $DOMAIN"
exit 1
fi
$MYSQL "$DB" <<EOF
INSERT INTO virtual_domains (name)
VALUES ('${DOMAIN}');
EOF
echo "OK: Domain created: $DOMAIN"
delete-domain.sh
#!/bin/bash
# delete-domain.sh - remove domain and all related data
set -euo pipefail
DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"
read -rp "Domain to delete: " DOMAIN
if ! [[ "$DOMAIN" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "ERROR: Invalid domain name"
exit 1
fi
DOMAIN_ID=$($MYSQL "$DB" -e \
"SELECT id FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")
if [[ -z "$DOMAIN_ID" ]]; then
echo "ERROR: Domain not found: $DOMAIN"
exit 1
fi
echo "WARNING: This will delete:"
echo " - Domain: $DOMAIN"
echo " - All users"
echo " - All aliases"
echo
read -rp "Type the domain name to confirm: " CONFIRM
[[ "$CONFIRM" == "$DOMAIN" ]] || exit 1
$MYSQL "$DB" <<EOF
DELETE FROM virtual_domains WHERE id=${DOMAIN_ID};
EOF
echo "OK: Domain deleted: $DOMAIN"
add-mailuser.sh
#!/bin/bash
# add-mailuser.sh - add virtual mailbox user
set -euo pipefail
DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"
read -rp "Email address: " EMAIL
if ! [[ "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "ERROR: Invalid email address"
exit 1
fi
DOMAIN="${EMAIL#*@}"
DOMAIN_ID=$($MYSQL "$DB" -e \
"SELECT id FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")
if [[ -z "$DOMAIN_ID" ]]; then
echo "ERROR: Domain not found in database: $DOMAIN"
exit 1
fi
EXISTS=$($MYSQL "$DB" -e \
"SELECT 1 FROM virtual_users WHERE email='${EMAIL}' LIMIT 1;")
if [[ -n "$EXISTS" ]]; then
echo "ERROR: User already exists: $EMAIL"
exit 1
fi
echo "Enter password for $EMAIL"
HASH=$(doveadm pw -s SHA512-CRYPT)
$MYSQL "$DB" <<EOF
INSERT INTO virtual_users (domain_id, email, password)
VALUES (${DOMAIN_ID}, '${EMAIL}', '${HASH}');
EOF
echo "OK: User created: $EMAIL"
echo "Testing authentication (doveadm auth test)"
read -rsp "Re-enter password for test: " TESTPW
echo
doveadm auth test "$EMAIL" "$TESTPW"
echo "OK: Done."
delete-mailuser.sh
#!/bin/bash
# delete-mailuser.sh - remove virtual mail user
set -euo pipefail
DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"
read -rp "Email address to delete: " EMAIL
if ! [[ "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "ERROR: Invalid email address"
exit 1
fi
EXISTS=$($MYSQL "$DB" -e \
"SELECT 1 FROM virtual_users WHERE email='${EMAIL}' LIMIT 1;")
if [[ -z "$EXISTS" ]]; then
echo "ERROR: User not found: $EMAIL"
exit 1
fi
read -rp "Really delete user $EMAIL? [y/N]: " CONFIRM
[[ "$CONFIRM" =~ ^[Yy]$ ]] || exit 0
$MYSQL "$DB" <<EOF
DELETE FROM virtual_users WHERE email='${EMAIL}';
EOF
echo "OK: User deleted: $EMAIL"
set-mailpassword.sh
#!/bin/bash
# set-mailpassword.sh - change virtual mailbox password
set -euo pipefail
DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"
read -rp "Email address: " EMAIL
if ! [[ "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "ERROR: Invalid email address"
exit 1
fi
EXISTS=$($MYSQL "$DB" -e \
"SELECT 1 FROM virtual_users WHERE email='${EMAIL}' LIMIT 1;")
if [[ -z "$EXISTS" ]]; then
echo "ERROR: User not found: $EMAIL"
exit 1
fi
echo "Enter new password for $EMAIL"
HASH=$(doveadm pw -s SHA512-CRYPT)
$MYSQL "$DB" <<EOF
UPDATE virtual_users
SET password='${HASH}'
WHERE email='${EMAIL}';
EOF
echo "OK: Password updated for: $EMAIL"
echo "Testing authentication (doveadm auth test)"
read -rsp "Re-enter password for test: " TESTPW
echo
doveadm auth test "$EMAIL" "$TESTPW"
echo "OK: Done."
add-alias.sh
#!/bin/bash
# add-alias.sh - create virtual mail alias
set -euo pipefail
DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"
read -rp "Alias address (source): " SOURCE
read -rp "Destination address: " DEST
EMAIL_RE='^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
if ! [[ "$SOURCE" =~ $EMAIL_RE ]]; then
echo "ERROR: Invalid source address"
exit 1
fi
if ! [[ "$DEST" =~ $EMAIL_RE ]]; then
echo "ERROR: Invalid destination address"
exit 1
fi
DOMAIN="${SOURCE#*@}"
DOMAIN_ID=$($MYSQL "$DB" -e \
"SELECT id FROM virtual_domains WHERE name='${DOMAIN}' LIMIT 1;")
if [[ -z "$DOMAIN_ID" ]]; then
echo "ERROR: Domain not found: $DOMAIN"
exit 1
fi
EXISTS=$($MYSQL "$DB" -e \
"SELECT 1 FROM virtual_aliases WHERE source='${SOURCE}' LIMIT 1;")
if [[ -n "$EXISTS" ]]; then
echo "ERROR: Alias already exists: $SOURCE"
exit 1
fi
$MYSQL "$DB" <<EOF
INSERT INTO virtual_aliases (domain_id, source, destination)
VALUES (${DOMAIN_ID}, '${SOURCE}', '${DEST}');
EOF
echo "OK: Alias created: $SOURCE -> $DEST"
delete-alias.sh
#!/bin/bash
# delete-alias.sh - remove virtual alias
set -euo pipefail
DB="mailserver"
MYSQL="/usr/bin/mysql -N -B"
read -rp "Alias address to delete: " SOURCE
if ! [[ "$SOURCE" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "ERROR: Invalid email address"
exit 1
fi
EXISTS=$($MYSQL "$DB" -e \
"SELECT 1 FROM virtual_aliases WHERE source='${SOURCE}' LIMIT 1;")
if [[ -z "$EXISTS" ]]; then
echo "ERROR: Alias not found: $SOURCE"
exit 1
fi
$MYSQL "$DB" <<EOF
DELETE FROM virtual_aliases WHERE source='${SOURCE}';
EOF
echo "OK: Alias deleted: $SOURCE"