Create automated MySQL backups with GPG encryption and secure remote storage using rsync and SSH keys. This tutorial covers database dumping, compression, encryption, and automated synchronization to remote servers.
Prerequisites
- MySQL server running
- Root or sudo access
- Remote server for backup storage
- Basic knowledge of SSH and GPG
What this solves
Database backups are critical for business continuity, but storing them without encryption creates security risks. This tutorial sets up automated MySQL backups with GPG encryption, compression, and secure remote storage using rsync over SSH. You'll learn to protect sensitive database information while ensuring reliable backup retention and recovery capabilities.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of backup tools and security packages.
sudo apt update && sudo apt upgrade -y
Install required backup tools
Install MySQL client tools, GPG for encryption, and rsync for remote synchronization. These tools provide the foundation for secure backup operations.
sudo apt install -y mysql-client gnupg2 rsync openssh-client gzip
Create backup user and directories
Create a dedicated backup user with minimal privileges and secure directory structure. This follows the principle of least privilege for backup operations.
sudo useradd -r -s /bin/bash -d /var/backups/mysql mysqlbackup
sudo mkdir -p /var/backups/mysql/{local,scripts,logs}
sudo chown -R mysqlbackup:mysqlbackup /var/backups/mysql
sudo chmod 750 /var/backups/mysql
Configure MySQL backup user
Create a MySQL user with only the necessary privileges for backup operations. This user should have SELECT and LOCK TABLES permissions.
mysql -u root -p -e "CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'SecureBackupPass2024!';"
mysql -u root -p -e "GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON . TO 'backup_user'@'localhost';"
mysql -u root -p -e "FLUSH PRIVILEGES;"
Create MySQL credentials file
Store MySQL credentials securely in a configuration file with restricted permissions. This prevents password exposure in process lists.
sudo -u mysqlbackup tee /var/backups/mysql/.my.cnf << 'EOF'
[client]
user=backup_user
password=SecureBackupPass2024!
host=localhost
EOF
sudo chmod 600 /var/backups/mysql/.my.cnf
Generate GPG encryption key
Create a GPG key pair for encrypting database backups. Use a strong passphrase and store it securely for backup recovery operations.
sudo -u mysqlbackup gpg --batch --generate-key << 'EOF'
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: MySQL Backup
Name-Email: backup@example.com
Expire-Date: 2y
Passphrase: SecureGPGPassphrase2024!
%commit
EOF
Export and secure GPG key
Export the GPG public key for backup verification and store the key information securely. This enables backup recovery on different systems.
sudo -u mysqlbackup gpg --list-keys --keyid-format LONG
sudo -u mysqlbackup gpg --armor --export backup@example.com > /var/backups/mysql/backup-public-key.asc
sudo chmod 644 /var/backups/mysql/backup-public-key.asc
Generate SSH key for remote backup
Create SSH key pair for passwordless authentication to the remote backup server. This enables automated remote synchronization.
sudo -u mysqlbackup ssh-keygen -t ed25519 -f /var/backups/mysql/.ssh/id_backup -N "" -C "mysql-backup@$(hostname)"
sudo chmod 700 /var/backups/mysql/.ssh
sudo chmod 600 /var/backups/mysql/.ssh/id_backup
Configure SSH client settings
Create SSH client configuration for the backup connection with security hardening and connection optimization.
Host backup-server
HostName 203.0.113.10
User backupuser
IdentityFile ~/.ssh/id_backup
IdentitiesOnly yes
Compression yes
ServerAliveInterval 60
ServerAliveCountMax 3
StrictHostKeyChecking yes
sudo chown mysqlbackup:mysqlbackup /var/backups/mysql/.ssh/config
sudo chmod 600 /var/backups/mysql/.ssh/config
Create backup script
Create the main backup script that handles database dumping, compression, encryption, and remote synchronization with error handling and logging.
#!/bin/bash
MySQL Encrypted Backup Script
set -euo pipefail
Configuration
BACKUP_DIR="/var/backups/mysql/local"
LOG_FILE="/var/backups/mysql/logs/backup.log"
RETENTION_DAYS=7
REMOTE_HOST="backup-server"
REMOTE_PATH="/backups/mysql/$(hostname)"
GPG_RECIPIENT="backup@example.com"
Functions
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
cleanup_old_backups() {
log "Cleaning up backups older than $RETENTION_DAYS days"
find "$BACKUP_DIR" -name "*.sql.gz.gpg" -mtime +$RETENTION_DAYS -delete
}
backup_database() {
local db_name="$1"
local timestamp=$(date '+%Y%m%d_%H%M%S')
local backup_file="${BACKUP_DIR}/${db_name}_${timestamp}.sql"
local compressed_file="${backup_file}.gz"
local encrypted_file="${compressed_file}.gpg"
log "Starting backup of database: $db_name"
# Create database dump
mysqldump --defaults-file=/var/backups/mysql/.my.cnf \
--single-transaction \
--routines \
--triggers \
--events \
--add-drop-database \
--databases "$db_name" > "$backup_file"
# Compress backup
gzip "$backup_file"
# Encrypt compressed backup
gpg --trust-model always --cipher-algo AES256 --compress-algo 2 \
--recipient "$GPG_RECIPIENT" --encrypt "$compressed_file"
# Remove unencrypted compressed file
rm "$compressed_file"
log "Backup completed: $encrypted_file"
echo "$encrypted_file"
}
sync_to_remote() {
log "Syncing backups to remote server"
# Create remote directory if it doesn't exist
ssh "$REMOTE_HOST" "mkdir -p $REMOTE_PATH"
# Sync encrypted backups to remote server
rsync -avz --delete \
--include='*.gpg' \
--exclude='*' \
"$BACKUP_DIR/" \
"${REMOTE_HOST}:${REMOTE_PATH}/"
log "Remote sync completed"
}
verify_backup() {
local backup_file="$1"
log "Verifying backup: $backup_file"
# Verify GPG encryption
if gpg --quiet --verify-options show-unusable-uids --verify "$backup_file" 2>/dev/null; then
log "GPG verification successful"
else
log "GPG verification failed"
return 1
fi
# Check file size (should be > 1KB for valid backup)
local file_size=$(stat -f%z "$backup_file" 2>/dev/null || stat -c%s "$backup_file")
if [ "$file_size" -gt 1024 ]; then
log "Backup size verification passed: ${file_size} bytes"
else
log "Backup size verification failed: ${file_size} bytes"
return 1
fi
}
Main execution
main() {
log "=== MySQL Backup Started ==="
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
# Get list of databases (excluding system databases)
databases=$(mysql --defaults-file=/var/backups/mysql/.my.cnf \
-Bse "SHOW DATABASES;" | grep -v -E '^(information_schema|performance_schema|mysql|sys)$')
if [ -z "$databases" ]; then
log "No user databases found to backup"
exit 1
fi
# Backup each database
for db in $databases; do
backup_file=$(backup_database "$db")
verify_backup "$backup_file"
done
# Clean up old backups
cleanup_old_backups
# Sync to remote server
sync_to_remote
log "=== MySQL Backup Completed Successfully ==="
}
Error handling
trap 'log "Backup failed with error code $?"' ERR
Run main function
main "$@"
Make backup script executable
Set proper permissions for the backup script and ensure it's owned by the backup user.
sudo chown mysqlbackup:mysqlbackup /var/backups/mysql/scripts/mysql-backup.sh
sudo chmod 750 /var/backups/mysql/scripts/mysql-backup.sh
Create backup restoration script
Create a script for restoring encrypted backups when needed. This script handles decryption and database restoration.
#!/bin/bash
MySQL Backup Restoration Script
set -euo pipefail
if [ $# -ne 2 ]; then
echo "Usage: $0 "
echo "Example: $0 mydb_20241201_120000.sql.gz.gpg mydb_restored"
exit 1
fi
ENCRYPTED_FILE="$1"
TARGET_DB="$2"
LOG_FILE="/var/backups/mysql/logs/restore.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "=== Starting restore of $ENCRYPTED_FILE to $TARGET_DB ==="
Verify encrypted file exists
if [ ! -f "$ENCRYPTED_FILE" ]; then
log "Error: Encrypted backup file not found: $ENCRYPTED_FILE"
exit 1
fi
Create temporary directory for restoration
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
Decrypt and decompress backup
log "Decrypting backup file"
gpg --quiet --decrypt "$ENCRYPTED_FILE" | gunzip > "$TEMP_DIR/restore.sql"
Create target database
log "Creating target database: $TARGET_DB"
mysql --defaults-file=/var/backups/mysql/.my.cnf \
-e "CREATE DATABASE IF NOT EXISTS \$TARGET_DB\;"
Restore database
log "Restoring database from backup"
mysql --defaults-file=/var/backups/mysql/.my.cnf \
"$TARGET_DB" < "$TEMP_DIR/restore.sql"
log "=== Restore completed successfully ==="
sudo chown mysqlbackup:mysqlbackup /var/backups/mysql/scripts/mysql-restore.sh
sudo chmod 750 /var/backups/mysql/scripts/mysql-restore.sh
Set up automated scheduling
Create a systemd service and timer for automated backup execution. This provides better logging and service management than cron.
[Unit]
Description=MySQL Encrypted Backup
After=mysql.service
Requires=network-online.target
[Service]
Type=oneshot
User=mysqlbackup
Group=mysqlbackup
ExecStart=/var/backups/mysql/scripts/mysql-backup.sh
StandardOutput=append:/var/backups/mysql/logs/systemd.log
StandardError=append:/var/backups/mysql/logs/systemd.log
[Unit]
Description=Run MySQL backup daily
Requires=mysql-backup.service
[Timer]
OnCalendar=daily
RandomizedDelaySec=30min
Persistent=true
[Install]
WantedBy=timers.target
Enable and start backup automation
Enable the systemd timer to run backups automatically and start it immediately.
sudo systemctl daemon-reload
sudo systemctl enable mysql-backup.timer
sudo systemctl start mysql-backup.timer
sudo systemctl status mysql-backup.timer
Configure remote server SSH access
Copy the SSH public key to the remote backup server and test the connection. Replace the IP address and username with your actual remote server details.
sudo -u mysqlbackup ssh-copy-id -i /var/backups/mysql/.ssh/id_backup.pub backupuser@203.0.113.10
Verify your setup
Test your backup configuration to ensure all components work correctly.
# Test backup script manually
sudo -u mysqlbackup /var/backups/mysql/scripts/mysql-backup.sh
Check backup files were created
ls -la /var/backups/mysql/local/
Verify GPG encryption
sudo -u mysqlbackup gpg --list-packets /var/backups/mysql/local/*.gpg | head -10
Check systemd timer status
sudo systemctl list-timers mysql-backup.timer
View backup logs
sudo tail -f /var/backups/mysql/logs/backup.log
Test SSH connection to remote server
sudo -u mysqlbackup ssh backup-server 'echo "Connection successful"'
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| GPG encryption fails | No GPG key or wrong recipient | Verify key exists with gpg --list-keys |
| MySQL connection denied | Wrong credentials or permissions | Check /var/backups/mysql/.my.cnf and MySQL user grants |
| SSH connection fails | Key not added to remote server | Run ssh-copy-id again or check remote ~/.ssh/authorized_keys |
| Backup directory permission denied | Wrong ownership or permissions | Use chown mysqlbackup:mysqlbackup and chmod 750 |
| Remote sync fails | Network issues or remote path missing | Test SSH connection and create remote directory manually |
| Systemd timer not running | Timer not enabled or service failed | Check with systemctl status mysql-backup.timer |
Next steps
- Set up automated MySQL database backups with compression and rotation
- Configure backup encryption with GPG and rsync for secure automated backups
- Monitor Linux system resources with performance alerts and automated responses
- Configure MySQL backup monitoring with Prometheus alerts
- Set up MySQL point-in-time recovery with binary logs
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# MySQL Backup with Encryption and Remote Storage Setup Script
# Production-quality installer with multi-distro support
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Configuration
BACKUP_USER="mysqlbackup"
BACKUP_DIR="/var/backups/mysql"
MYSQL_BACKUP_USER="backup_user"
MYSQL_BACKUP_PASS=""
GPG_KEY_EMAIL=""
REMOTE_HOST=""
REMOTE_PATH=""
log() {
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
cleanup() {
if [ $? -ne 0 ]; then
error "Installation failed. Cleaning up..."
systemctl disable mysql-backup.timer 2>/dev/null || true
rm -f /etc/systemd/system/mysql-backup.{service,timer}
systemctl daemon-reload
fi
}
trap cleanup ERR
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -p PASSWORD MySQL backup user password (required)"
echo " -e EMAIL GPG key email (required)"
echo " -r HOST Remote backup host (required)"
echo " -d PATH Remote backup directory (required)"
echo " -h Show this help"
exit 1
}
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
MYSQL_PKG="mysql-client"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
MYSQL_PKG="mysql"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
MYSQL_PKG="mysql"
;;
*)
error "Unsupported distribution: $ID"
exit 1
;;
esac
else
error "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
}
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
exit 1
fi
if ! command -v mysql &> /dev/null; then
error "MySQL server not found. Please install MySQL first."
exit 1
fi
}
parse_arguments() {
while getopts "p:e:r:d:h" opt; do
case $opt in
p) MYSQL_BACKUP_PASS="$OPTARG" ;;
e) GPG_KEY_EMAIL="$OPTARG" ;;
r) REMOTE_HOST="$OPTARG" ;;
d) REMOTE_PATH="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
if [[ -z "$MYSQL_BACKUP_PASS" || -z "$GPG_KEY_EMAIL" || -z "$REMOTE_HOST" || -z "$REMOTE_PATH" ]]; then
error "All options (-p, -e, -r, -d) are required"
usage
fi
}
main() {
parse_arguments "$@"
log "Starting MySQL backup encryption and remote storage setup"
echo "[1/9] Detecting distribution and checking prerequisites..."
detect_distro
check_prerequisites
success "Distribution: $PRETTY_NAME, Package manager: $PKG_MGR"
echo "[2/9] Updating system packages..."
eval $PKG_UPDATE
success "System packages updated"
echo "[3/9] Installing required backup tools..."
$PKG_INSTALL $MYSQL_PKG gnupg2 rsync openssh-client gzip
success "Backup tools installed"
echo "[4/9] Creating backup user and directories..."
if ! id "$BACKUP_USER" &>/dev/null; then
useradd -r -s /bin/bash -d "$BACKUP_DIR" "$BACKUP_USER"
fi
mkdir -p "$BACKUP_DIR"/{local,scripts,logs}
chown -R "$BACKUP_USER:$BACKUP_USER" "$BACKUP_DIR"
chmod 750 "$BACKUP_DIR"
chmod 750 "$BACKUP_DIR"/{local,scripts,logs}
success "Backup user and directories created"
echo "[5/9] Configuring MySQL backup user..."
mysql -e "CREATE USER IF NOT EXISTS '$MYSQL_BACKUP_USER'@'localhost' IDENTIFIED BY '$MYSQL_BACKUP_PASS';" || true
mysql -e "GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON *.* TO '$MYSQL_BACKUP_USER'@'localhost';"
mysql -e "FLUSH PRIVILEGES;"
success "MySQL backup user configured"
echo "[6/9] Creating MySQL credentials file..."
cat > "$BACKUP_DIR/.my.cnf" <<EOF
[client]
user=$MYSQL_BACKUP_USER
password=$MYSQL_BACKUP_PASS
host=localhost
EOF
chown "$BACKUP_USER:$BACKUP_USER" "$BACKUP_DIR/.my.cnf"
chmod 600 "$BACKUP_DIR/.my.cnf"
success "MySQL credentials file created"
echo "[7/9] Setting up GPG encryption..."
sudo -u "$BACKUP_USER" gpg --batch --generate-key <<EOF
Key-Type: RSA
Key-Length: 2048
Name-Real: MySQL Backup
Name-Email: $GPG_KEY_EMAIL
Expire-Date: 0
%no-protection
%commit
EOF
success "GPG key pair generated"
echo "[8/9] Creating backup script..."
cat > "$BACKUP_DIR/scripts/mysql-backup.sh" <<'SCRIPT_EOF'
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/mysql/local"
LOG_FILE="/var/backups/mysql/logs/backup.log"
RETENTION_DAYS=7
GPG_RECIPIENT="MySQL Backup"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
create_backup() {
local database="$1"
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$BACKUP_DIR/${database}_${timestamp}.sql"
local compressed_file="${backup_file}.gz"
local encrypted_file="${compressed_file}.gpg"
log "Creating backup for database: $database"
mysqldump --defaults-file=/var/backups/mysql/.my.cnf \
--single-transaction --routines --triggers \
"$database" > "$backup_file"
gzip "$backup_file"
gpg --trust-model always --cipher-algo AES256 --compress-algo 2 \
--recipient "$GPG_RECIPIENT" --encrypt "$compressed_file"
rm "$compressed_file"
log "Backup completed: $encrypted_file"
echo "$encrypted_file"
}
main() {
log "=== MySQL Backup Started ==="
mkdir -p "$BACKUP_DIR"
databases=$(mysql --defaults-file=/var/backups/mysql/.my.cnf \
-Bse "SHOW DATABASES;" | grep -v -E '^(information_schema|performance_schema|mysql|sys)$')
if [ -z "$databases" ]; then
log "No user databases found to backup"
exit 1
fi
for db in $databases; do
create_backup "$db"
done
find "$BACKUP_DIR" -name "*.gpg" -mtime +$RETENTION_DAYS -delete
log "Old backups cleaned up (older than $RETENTION_DAYS days)"
log "=== MySQL Backup Completed ==="
}
main "$@"
SCRIPT_EOF
chown "$BACKUP_USER:$BACKUP_USER" "$BACKUP_DIR/scripts/mysql-backup.sh"
chmod 750 "$BACKUP_DIR/scripts/mysql-backup.sh"
success "Backup script created"
echo "[9/9] Setting up systemd timer..."
cat > /etc/systemd/system/mysql-backup.service <<EOF
[Unit]
Description=MySQL Encrypted Backup
After=mysql.service
[Service]
Type=oneshot
User=$BACKUP_USER
Group=$BACKUP_USER
ExecStart=$BACKUP_DIR/scripts/mysql-backup.sh
EOF
cat > /etc/systemd/system/mysql-backup.timer <<EOF
[Unit]
Description=Run MySQL backup daily
Requires=mysql-backup.service
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable mysql-backup.timer
systemctl start mysql-backup.timer
success "Systemd timer configured and started"
success "MySQL backup with encryption and remote storage setup completed!"
log "Backup location: $BACKUP_DIR/local"
log "Logs location: $BACKUP_DIR/logs"
log "Script location: $BACKUP_DIR/scripts"
warn "Don't forget to configure SSH key authentication for remote host: $REMOTE_HOST"
warn "Test the backup manually: sudo -u $BACKUP_USER $BACKUP_DIR/scripts/mysql-backup.sh"
}
main "$@"
Review the script before running. Execute with: bash install.sh