Пропавшая база приносит убытки, но украденная копия приносит их вдвойне: у кого на руках дамп, у того и пароли, и контакты, и история заказов. Файл выгрузки MySQL или MariaDB, лежащий в каталоге или облаке без шифрования, сводит защиту на нет, потому что читается любым текстовым редактором. Поэтому шифрование копий - не модная опция, а часть самой процедуры, наравне с расписанием и ротацией. Схема ниже строится на двух инструментах: mysqldump снимает логический дамп, mariabackup делает физическое горячее копирование, openssl закрывает оба потока шифром на лету.

Порядок простой: сначала роли и гранты, затем скрипт дампа с шифрованием потока, физические копии с добавочными уровнями, ротация, перенос на вторую машину и проверка восстановления. Все инструменты бесплатны и живут в репозиториях любого дистрибутива, вся схема умещается в пару скриптов и один файл ключа.

Слои схемы защиты от логического дампа до журналов операций

Логический дамп - это текст SQL. mysqldump читает базу построчно, и включённый по умолчанию ключ --quick не даёт утилите складывать строки в память. Такой файл переносится между версиями и платформами, читается глазами, восстанавливается частями. Слабое место - скорость: база на сотни гигабайт выгружается и заливается часами.

Физическая копия снимает файлы как есть. mariabackup переносит данные InnoDB в горячем режиме, почти не мешая рабочим запросам, а восстановление сводится к возврату файлов на место и короткой подготовке. Инструмент принадлежит стеку MariaDB: физические горячие копии MySQL делают утилитой XtraBackup или встроенным плагином clone, mariabackup на MySQL не рассчитан.

Третий слой - бинлоги. Между двумя копиями сервер пишет журнал всех операций, и при аварии недостающий хвост докатывается утилитой mysqlbinlog прямо до секунды перед сбоем. Четвёртый слой - шифрование и вынос копий за пределы машины: архив, который не читается без ключа, и вторая площадка закрывают сразу два сценария потери.

Ориентир по версиям. У MariaDB длинной поддержкой закрыты серии 11.4 и 11.8, за ними выходят короткие 12.x и 13.x, у MySQL линия с долгой поддержкой - 8.4. mysqldump присутствует в обоих стеках, в MariaDB он вызывается и под именем mariadb-dump, ключи почти совпадают, различия заметны в мелочах вроде имени опции записи координат бинлога.

Подготовка пользователя и параметров сервера перед первым запуском

Дамп снимается служебной учёткой с минимальным набором прав. Для логической выгрузки MySQL нужны SELECT, SHOW VIEW, TRIGGER и EVENT на нужных схемах, PROCESS, если не передан ключ --no-tablespaces, а при включённых GTID и --single-transaction ещё и RELOAD. Право PROCESS требуется потому, что утилита читает метаданные табличных пространств; ключ --no-tablespaces снимает это требование, но тогда определения пространств в дамп не попадут. Одним блоком:

CREATE USER 'backup'@'localhost' IDENTIFIED BY 'S0me-Long-Passphrase';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS, RELOAD ON *.* TO 'backup'@'localhost';

Для mariabackup хватает RELOAD, PROCESS, LOCK TABLES и REPLICATION CLIENT:

CREATE USER 'xb'@'localhost' IDENTIFIED BY 'S0me-Long-Passphrase';
GRANT RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'xb'@'localhost';

Пароли держат в ~/.my.cnf с правами 600, утилиты обоих стеков читают секцию [client] молча:

# ~/.my.cnf
[client]
user=backup
password=S0me-Long-Passphrase
host=localhost

На стороне сервера для отката по времени включается журнал операций, его глубина в свежих версиях MySQL ограничивается переменной binlog_expire_logs_seconds. Для больших дампов по сети помогает поднятый max_allowed_packet и увеличенные таймауты чтения, локальная выгрузка через сокет снимает эти вопросы вовсе.

Скрипт логического дампа со шифрованием потока на лету

Скрипт выстраивает конвейер: mysqldump отдаёт текст, gzip сжимает, openssl шифрует. Порядок именно такой, потому что шифр превращает данные в шум, который не сжимается: сначала сжатие, потом запирание.

#!/bin/bash
set -euo pipefail

BASE=/var/backups/mysql
KEY=/etc/backup/backup.key
STAMP=$(date +%F_%H%M)
LOG=/var/log/mysql-dump.log

exec >>"$LOG" 2>&1

mysqldump --single-transaction --quick --hex-blob --routines --events --triggers --source-data --all-databases | gzip -9 | openssl enc -aes-256-cbc -pbkdf2 -iter 60000 -salt -pass file:"$KEY" -out "$BASE/all-$STAMP.sql.gz.enc"

sha256sum "$BASE/all-$STAMP.sql.gz.enc" > "$BASE/all-$STAMP.sha256"

openssl enc -d -aes-256-cbc -pbkdf2 -iter 60000 -pass file:"$KEY" -in "$BASE/all-$STAMP.sql.gz.enc" | zcat | tail -n 2

find "$BASE" -name 'all-*.enc' -mtime +14 -delete
find "$BASE" -name 'all-*.sha256' -mtime +14 -delete

echo "$(date +%F_%T) dump ok: $(du -sm "$BASE/all-$STAMP.sql.gz.enc" | awk '{print $1}') MiB"

Пояснения. Ключ --single-transaction даёт согласованный снимок InnoDB без блокировок. Хранимые процедуры, события и триггеры выгружаются ключами --routines, --events и --triggers, двоичные поля - ключом --hex-blob, иначе блобы ломаются при переносе между версиями. Ключ --source-data записывает в дамп координаты бинлога, требует включённого журнала операций и в MariaDB называется --master-data. При включённых GTID в игру вступает ключ --set-gtid-purged, определяющий, попадёт ли в дамп отметка о перенесённых транзакциях: при переносе на сервер с другой историей поведение ключа стоит проверить заранее. Контрольная строка с tail нужна не для галочки: конец дампа помечен комментарием "-- Dump completed", по нему видно, что файл дожил до конца. Соль и 60 тысяч итераций PBKDF2 делают перебор парольной фразы дорогим удовольствием.

Физическое горячее копирование через mariabackup с добавочными уровнями

Когда база вырастает за сотни гигабайт, ночные дампы перестают успевать. Физическое копирование решает иначе: mariabackup переносит файлы табличных пространств, почти не нагружая сервер, раз в неделю снимается полная копия, в остальные дни - добавочные. В начале и в конце съёма инструмент коротко блокирует таблицы, чтобы зафиксировать согласованность метаданных, остальное время запись не останавливается.

mariabackup --backup --target-dir=/var/backups/mysql/full-2026-10-04
mariabackup --backup --target-dir=/var/backups/mysql/inc-2026-10-09 --incremental-basedir=/var/backups/mysql/full-2026-10-04

Снятая копия не готова к старту сервера, её доводит ключ --prepare. Цепочка собирается строго по порядку: полная готовится с --apply-log-only, последняя добавочная поверх неё - без него:

mariabackup --prepare --apply-log-only --target-dir=/var/backups/mysql/full-2026-10-04
mariabackup --prepare --target-dir=/var/backups/mysql/inc-2026-10-09 --incremental-dir=/var/backups/mysql/full-2026-10-04

Восстановление сводится к остановке службы, очистке каталога данных, возврату файлов и поправке владельца:

systemctl stop mariadb
rm -rf /var/lib/mysql/*
mariabackup --copy-back --target-dir=/var/backups/mysql/inc-2026-10-09
chown -R mysql:mysql /var/lib/mysql
systemctl start mariadb

Добавочная копия строится от позиции LSN в журнале, поэтому цепочка собирается только последовательно, перепрыгивать уровни нельзя.

Шифрование физических копий и правила хранения ключей

Физическую копию шифруют двумя путями. Первый - шифровать поток прямо при съёме: mariabackup умеет отдавать данные в формате xbstream, и конвейер не оставляет на диске открытых файлов вовсе:

mariabackup --backup --stream=xbstream --user=xb | openssl enc -aes-256-cbc -pbkdf2 -iter 60000 -salt -pass file:/etc/backup/backup.key -out /var/backups/mysql/full-$STAMP.xbs.enc

Обратный путь - дешифровка и распаковка в пустой каталог, затем --prepare:

openssl enc -d -aes-256-cbc -pbkdf2 -iter 60000 -pass file:/etc/backup/backup.key -in full-2026-10-09.xbs.enc | xbstream -x -C /srv/restore
mariabackup --prepare --target-dir=/srv/restore

Второй путь - шифровать уже снятый каталог перед переносом, конвейером вида "tar -C $FULL -cf - . | openssl enc ... -out copy.tar.enc". Оба варианта равнозначны, потоковый короче и не требует свободного места под промежуточный архив.

Правила ключей короткие. Файл ключа лежит с правами 600, на разделе, куда не дотягивается резервная копия, и никогда не переносится в одном наборе с копиями. Ключ меняется при смене персонала или компрометации хоста, старые копии при этом дочитываются прежним ключом, поэтому ключи хранятся и после ротации. Раз в квартал контрольная дешифровка копии месячной давности подтверждает, что связка ключа и архива жива.

Ротация по возрастам и перенос копий на вторую машину

Экономика. Текстовый дамп сжимается gzip-ом в 5-10 раз: база на 80 ГБ превращается в архив 8-15 ГБ, шифрование размер почти не меняет. Физическая копия занимает объём данных целиком, добавочные - лишь дельту. Полная копия раз в неделю с месячной глубиной хранения, добавочные ежедневно с двухнедельной глубиной и суточные дампы за две недели дают ориентировочно 550-650 ГБ при исходных 80 ГБ.

Хранить всё в одном каталоге - значит рано или поздно упереться в диск, поэтому правила удаления прописываются явно:

find "$BASE" -name 'full-*' -mtime +35 -delete
find "$BASE" -name 'inc-*' -mtime +16 -delete
find "$BASE" -name 'all-*.enc' -mtime +30 -delete

Перед записью проверяется свободное место, а ночной rsync выносит копии за пределы машины:

FREE=$(df -Pm "$BASE" | awk 'NR==2{print $4}')
[ "$FREE" -lt 40960 ] && { echo "mysql-backup: low space $FREE MiB" >&2; exit 3; }
rsync -az --delete "$BASE/" Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.:/srv/mysql/

Ключ шифрования на второй хост не копируется: там хранятся только шифрованные архивы.

Проверка восстановления и уведомления о каждом запуске

Копия, которую ни разу не восстанавливали, - просто файл с оптимистичным именем. Репетиция логического дампа поднимает запасной экземпляр на свободном сокете и заливает в него расшифрованный поток:

openssl enc -d -aes-256-cbc -pbkdf2 -iter 60000 -pass file:"$KEY" -in "$BASE/all-$STAMP.sql.gz.enc" | zcat | mysql --protocol=socket -S /tmp/restore.sock
mysql --protocol=socket -S /tmp/restore.sock -e "select count(*) from shop.orders"

Хвост бинлогов после копии докатывается утилитой mysqlbinlog с позиции из дампа:

mysqlbinlog --start-position=157 binlog.000042 binlog.000043 | mysql --protocol=socket -S /tmp/restore.sock

Уведомления держатся на коде возврата и почте. Функция отправляет письмо прямо из скрипта:

notify() {
  curl -sS --url "smtp://mail.example.com:587" --ssl-reqd --mail-from Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. --mail-rcpt Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. --user Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.:app-password' -T - <<EOF
From: Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
To: Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
Subject: mysql backup on $(hostname)

$1
EOF
}

trap 'notify "mysqldump FAILED, see $LOG"' ERR
notify "dump ok, $STAMP, size $SIZE MiB"

Для систем мониторинга годится короткий вебхук с теми же полями:

curl -fsS -X POST "https://hooks.example.com/mysql-backup" --data-urlencode "host=$(hostname)" --data-urlencode "stamp=$STAMP"

Расписание и обработку падений удобно держать в systemd: таймер с Persistent=true доигрывает пропущенные ночные запуски, а директива OnFailure в юните отправляет письмо при ненулевом коде возврата.

Перед вводом схемы в бой стоит пройти короткий чек-лист приёмки:

  1. сквозной прогон от снятия копии до живой базы на тестовой машине с замером времени;
  2. контрольная дешифровка копии недельной давности ключом из хранилища секретов;
  3. проверка свободного места и скорости на обеих сторонах переноса;
  4. письма об успехе и о сбое, полученные в обоих режимах;
  5. запись в журнале с датой, размером и контрольной суммой последней копии.

Схема готова не тогда, когда впервые отработали скрипты, а когда восстановления перестают быть событием: раз в месяц расшифрованная копия поднимает запасную базу за предсказуемые минуты, а письма с цифрами приходят сами. Шифрование при этом перестаёт быть ритуалом и становится обычным свойством копии - тем же, чем является имя файла или права доступа.