UMGUM.COM 

S3 + PGP synchronization ( Налаживаем автоматизированную выгрузку резервных копий из файловой системы во внешнее AWS:S3-хранилище, с обязательным шифрованием данных. )

14 сентября 2020  (обновлено 23 марта 2021)

OS: "Linux Debian 9/10", "Linux Ubuntu Server 16/18/20 LTS".
Apps: "GnuPG", "s3cmd (Amazon AWS S3 client)", "Bash".

Задача: наладить полностью автоматизированную выгрузку файлов резервных копий из файловой системы linux-сервера во внешнее AWS:S3-хранилище, с обязательным шифрованием данных.

Прежде всего придумаем, какой тип шифрования мы хотим: ассиметричный или симметричный? С одной стороны, ассиметричное шифрование через сертификаты гибче при работе в многокомпонентной среде, позволяя обеспечить уровень безопасности ключа шифрования выше, нежели симметричное. А с другой стороны, наша задача проста и никаких изысков не подразумевает. Когда шифрование и расшифровка производятся в единственной точке доверенной среды (в которой уже имеются все файлы в незашифрованном состоянии), то в усложнении схемы управления ключами нет острой необходимости - достаточно быть бдительными и не выпускать "парольную фразу" за пределы доверенной зоны.

Учитывая то, что ради простоты мы остановились на симметричном шифровании, для осуществления такового само собой напрашивается инструмент "GnuPG/OpenPGP/PGP" (в случае ассиметричного шифрования наиболее вероятным выбором будет "OpenSSL").

В качестве хранилища данных будем использовать "Amazon AWS S3". Впрочем, идентичный протокол взаимодействия поддерживает ещё масса "облачных сервисов", вроде "Google CS", "DigitalOcean", "Dreamhost", "IBM COS S3", "Wasabi S3" - несложно будет задействовать их при необходимости.


Прорабатываем методику.

Первая очевидная проблема синхронизации с попутным шифрованием в AWS:S3, и вообще в любое внешнее недоверенное хранилище данных, с использованием "client-side-encryption" в том, что внешнее хранилище содержит уже зашифрованные файлы, а в локальном хранилище они не зашифрованы, и сравнение их простейшей сверкой по хешу содержимого невозможно - оно ведь обязано быть различным для нешифрованных и шифрованных вариантов одного файла.

Если количество синхронизируемых файлов невелико - до тысяч и десятков тысяч (типично для хранилища архивов резервных копий небольшого предприятия) - можно обойтись простейшим алгоритмом, работающим по следующему алгоритму:

1. Рекурсивно обходим подлежащие синхронизации директории.
2. Для каждого файла генерируем sha1-хеш содержимого (чем слабее в нашем случае "хеш", тем быстрее пройдёт операция - в данном случае нам важно лишь отсутствие коллизий, а не криптостойкость).
3. Проверяем наличие ранее записанного sha1-хеша в специальной индексной директории.
  3.1. Если sha1-хеш содержимого файла отсутствует в индексной директории или его значение не совпадает с сохранённым ранее, то загружаем файл в удалённое хранилище.
  3.2. Сохраняем sha1-хеш содержимого в специальной индексной директории.
4. Запрашиваем из внешнего хранилища листинг файлов.
5. Перебираем листинг файлов внешнего хранилища, проверяя наличие одноимённых файлов в локальном хранилище.
  5.1. Удаляем из внешнего хранилища файлы, которые отсутствуют в локальном хранилище.

Возможно есть смысл использовать хеш MD5, на вычисление которого теоретически затрачивается чуть меньше ресурсов процессора, нежели на вычисление контрольной суммы SHA1. При этом мои практические тесты не показывают существенной разницы (разница длительности операции в 0.5%, в пользу "md5sum"), возможно потому, что основное время уходит на чтение данных из файла.

На этапе проработки решения задачи встречал упоминание о том, что размер загружаемого файла в AWS:S3 не должен превышать 5GB. Проверил, загрузив посредством утилиты "s3cmd" монолитный файл размером 50GB - никаких проблем. Утилита "s3cmd" выгружает на AWS:S3 данные на максимальной для моего соединения скорости, без каких-либо задержек, кусками по 15MB, при этом никак не нагружая процессор (кроме этапа шифрования посредством GPG, разумеется). Тем не менее, возможно перед выгрузкой (и шифрованием) есть смысл резать файлы утилитой "split" на куски до 3-5GB, чтобы упростить процессы шифрования и последующей передачей в удалённое хранилище - но здесь это не будет реализовано.

Также выяснилось, что предусмотренная разработчиками "s3cmd" интеграция с GPG имеет очень неудобную недоработку - промежуточные зашифрованные файлы создаются только в директории "/tmp", и поведение это мне изменить не получилось. Проблема в том, что выгружаемые во внешнее хранилище часто бывают размером гораздо большим, чем раздел временной файловой системы. Для обхода этого ограничения придётся самостоятельно шифровать файлы с последующей их выгрузкой, тем более, что это позволит в будущем легче воспользоваться отличными от GPG инструментами шифрования.

Подготовка "облачного хранилища".

Будем считать, что мы располагаем в "Amazon AWS" аккаунтом и учётной записью, обладающей доступ к IAM и S3.

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

Navigation Pane -> Access Manager -> Users -> Add user:
  User name: l2ce-backup.example.net
  Access type:
    Programmatic access: on
    AWS Management Console access: off
  Access key ID: XX...XX
  Secret access key: yy...yy
  Permissions -> Add user to group:
    Group: Backups
  Create User.

Создаём S3-хранилище с уникальным для всего "AWS S3" именем:

Navigation Pane -> Amazon S3 -> Buckets -> Create bucket:
  Bucket name: backup.example.net
  Region: EU (Frankfurt)
  Versioning: off
  Server access logging: off
  Tags: off
  Object-level logging: off
  Default encryption: off
  CloudWatch request metrics: off
  Block public access (bucket settings): Block all public access

Создаём выделенное для нашего S3-хранилища описание политики доступа, позволяющее доступ только к необходимому минимуму функционала:

Navigation Pane -> Access Manager -> Policies -> Create policy:
  Name: AmazonS3CustomAccessForBackupL2CE
  JSON: {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "VisualEditor0",
        "Effect": "Allow",
        "Action": [
          "s3:PutObject",
          "s3:GetObjectAcl",
          "s3:GetObject",
          "s3:ListBucket",
          "s3:DeleteObject",
          "s3:GetBucketLocation",
          "s3:PutObjectAcl"
        ],
        "Resource": [
          "arn:aws:s3:::backup.example.net",
          "arn:aws:s3:::backup.example.net/*"
        ]
      },
      {
        "Sid": "VisualEditor1",
        "Effect": "Allow",
        "Action": "s3:ListAllMyBuckets",
        "Resource": "*"
      }
    ]
  }

Применяем выделенную политику к пользователю, через которого будем работать с S3-хранилищем:

Navigation Pane -> Access Manager -> Users -> "l2ce-backup.example.net":
  Permissions policies -> Add:
    Attached directly: AmazonS3CustomAccessForBackupL2CE

Прежде, чем переходить к следующим этапам, желательно проверить, возможно ли подключение к S3-хранилищу с учётными данными созданного специально для этого пользователя.

Налаживаем синхронизацию и шифрование.

Установим необходимые утилиты "GnuPG" и "s3cmd" (последняя имеется в репозиториях "Debian/Ubuntu", но свежесть её там оставляет желать лучшего):

# aptitude install --without-recommends gnupg python-pip
# pip install s3cmd

Есть смысл сразу запустить интерактивный конфигуратор - чтобы проверить, возможно ли вообще подключение к удалённому хранилищу - но настройки лучше не сохранять, или удалить позже (файл "~/.s3cfg"), чтобы не компрометировать учётные данные:

# s3cmd --configure

....
Test access with supplied credentials? [Y/n] Y
....
Success. Your access key and secret key worked fine :-)
....
Save settings? [y/N] N
....
Configuration aborted. Changes were NOT saved.

Назовём наш комплект скриптов "l2ce-sync" (от "Local to Cloud Encrypt Sync"), и создадим необходимые для работы в дальнейшем файловые структуры:

# mkdir -p /usr/local/etc/l2ce
# mkdir -p /var/lib/l2ce/index

Заготовим "парольную фразу", которая будет использоваться PGP для симметричного шифрования (и расшифровывания) данных:

# pwgen -cnsy 64 1

Разместим полученную от генератора выше последовательность символов в текстовом файле "/usr/local/etc/l2ce/.gpg-passphrase".

Строго-настрого защитим файл с "парольной фразой" от всех, кроме суперпользователя операционной системы:

# chown root /usr/local/etc/l2ce/.gpg-passphrase
# chmod go-rwx /usr/local/etc/l2ce/.gpg-passphrase

Обращаю внимание на то, что "gpg-passphrase" представляет собой высочайшую ценность - в случае утраты этой "парольной фразы" расшифровать данные станет абсолютно невозможно. Я даже не привожу в инструкции выше команд записи в файл хранения "gpg-passphrase", дабы минимизировать риск затирания его содержимого.

Подготовим конфигурационный файл с параметрами подключения к AWS:S3 (утилита "s3cmd" конфигурируется через переменные окружения):

# vi /usr/local/etc/l2ce/.s3cmd

export AWS_ACCESS_KEY_ID="XX...XX"
export AWS_SECRET_ACCESS_KEY="yy...yy"
export AWS_DEFAULT_REGION="eu-central-1"

Закрываем доступ к паролям посторонним:

# chown root /usr/local/etc/l2ce/.s3cmd
# chmod go-rwx /usr/local/etc/l2ce/.s3cmd

Заготавливаем текстовый файл, где формируем построчным перечислением набор директорий, которые будем синхронизировать (с шифрованием при выгрузке, разумеется) с внешним S3-хранилищем:

# vi /usr/local/etc/l2ce/sync_dir.list

/mnt/storage0/bacula/dev0
#/mnt/storage0/bacula/dev1

Аналогично вышеприведённому перечню директорий для синхронизации составляем список исключений, если в таковых будет необходимость:

# vi /usr/local/etc/l2ce/sync_exclude.list

/.cache/
/tmp/

А вот и bash-скрипт, взаимодействующий с S3-хранилищем, синхронизирующий (в одну сторону - от локального к "облачному") и шифрующий выгружаемые файлы:

# vi /usr/local/etc/l2ce/l2ce-sync.sh && chmod +x /usr/local/etc/l2ce/l2ce-sync.sh

#!/bin/bash

# Задаём указатели на ресурсы
SYNC_DIR_LIST=/usr/local/etc/l2ce/sync_dir.list
SYNC_EXCLUDE_LIST=/usr/local/etc/l2ce/sync_exclude.list
INDEX_DIR=/var/lib/l2ce/index
#
GPG_PASSPHRASE=/usr/local/etc/l2ce/.gpg-passphrase
GPG_TMPDIR=/var/tmp
#
S3CMD_CREDENTIALS=/usr/local/etc/l2ce/.s3cmd
S3_BUCKET="s3://backup.example.net"
#S3_BUCKET="s3://backup.example.net.s3.amazonaws.com"
#
MAIL_FROM="backup@example.net"
MAIL_TO="admin@example.net"
REPORT=$(mktemp --tmpdir=/var/tmp);
LOG=/var/log/l2ce-sync.log
DATE=`date +"%Y-%m-%d %H:%M:%S"`
echo >> "${LOG}"
echo "${DATE}" >> "${LOG}"

# Проверяем наличие списков синхронизируемых файлов, а также исключений
[ ! -f "${SYNC_DIR_LIST}" ] && { echo "The file \"${SYNC_DIR_LIST}\" with the list of synchronized resources was not found. Aborting." | tee -a "${REPORT}" ; exit 1; }
[ ! -f "${SYNC_EXCLUDE_LIST}" ] && { echo "Exclusion list file \"${SYNC_EXCLUDE_LIST}\" is not found. Aborting." | tee -a "${REPORT}" ; exit 1; }
[ ! -d "${INDEX_DIR}" ] && { mkdir -p "${INDEX_DIR}"; }

# Профилактически очищаем файл списка синхронизируемых ресурсов от пустых строк и лишних пробелов
TMP_DIR_LIST=$(mktemp --tmpdir=/var/tmp)
grep -v -e '^[[:space:]]*$' "${SYNC_DIR_LIST}" | sed -e 's/^[[:space:]]*//; s/[[:space:]]*$//' > "${TMP_DIR_LIST}"
mv "${TMP_DIR_LIST}" "${SYNC_DIR_LIST}"

# Профилактически очищаем файл списка исключений от пустых строк и лишних пробелов
TMP_EXCLUDE_LIST=$(mktemp --tmpdir=/var/tmp)
grep -v -e '^[[:space:]]*$' "${SYNC_EXCLUDE_LIST}" | sed -e 's/^[[:space:]]*//; s/[[:space:]]*$//' > "${TMP_EXCLUDE_LIST}"
mv "${TMP_EXCLUDE_LIST}" "${SYNC_EXCLUDE_LIST}"

# Загружаем переменные окружения "s3cmd"
source "${S3CMD_CREDENTIALS}"

# Описываем функцию шифрования и выгрузки файлов на внешнее хранилище
function crypt-upload {

  # Шифруем файл перед выгрузкой в удалённое хранилище
  GPG_SESSION_DIR=$(mktemp -d ${GPG_TMPDIR}/.gpgXXXXX)
  mkdir -p "${GPG_SESSION_DIR}${SFILE_DIR}"
  /usr/bin/gpg -c --verbose --batch --yes --passphrase-file "${GPG_PASSPHRASE}" -o "${GPG_SESSION_DIR}${SFILE}" "${SFILE}" >> "${LOG}" 2>&1
  if [ "${?}" == "0" ] ; then

    # Выгружаем файл на внешнее хранилище
    s3cmd put --no-progress --stats --no-encrypt "${GPG_SESSION_DIR}${SFILE}" "${S3_BUCKET}${SFILE}" >> "${LOG}" 2>&1
    if [ "${?}" == "0" ] ; then

      # Сохраняем "хеш" содержимого файла в индексной директории
      mkdir -p "${INDEX_DIR}${SFILE_DIR}"
      echo "${SFILE_HASH}" > "${INDEX_DIR}${SFILE}"
      echo "The file \"${SFILE}\" is successfully encrypted and uploaded to the remote storage \"${S3_BUCKET}\"." | tee -a "${REPORT}"

    else
      echo "FAILED to upload a file \"${SFILE}\" to remote storage \"${S3_BUCKET}\"!" | tee -a "${REPORT}"
    fi
  else
    echo "FAILED to encrypt a file \"${SFILE}\" in temporary \"${GPG_SESSION_DIR}${SFILE}\"!" | tee -a "${REPORT}"
  fi

  # Удаляем директорию с промежуточным зашифрованным файлом
  rm -rf "${GPG_SESSION_DIR}"

return ${?}
}

###
### Выгрузка синхронизируемых файлов на внешнее хранилище
###
echo "Uploading synchronized files to external storage..." | tee -a "${REPORT}"

# Перебираем директории списка синхронизируемых
while read SDIR ; do

  # Проверяем существование указанной синхронизируемой директории
  [ ! -d "${SDIR}" ] && { echo "Local directory \"${SDIR}\" does not exist! Skipped." | tee -a "${REPORT}" ; continue; }

  # Перебираем все файлы в синхронизируемых директориях
  # (не обрабатываем файлы подпадающие под лист исключений)
  find "${SDIR}" -type f -print | grep -vFf "${SYNC_EXCLUDE_LIST}" | \
  while read SFILE ; do

    # Пресекаем возможные ошибочные попытки синхронизировать директорию индекса sha1-хешей
    [ $(echo "${SFILE}" | grep -iFq "${INDEX_DIR}") ] && { echo "The hash index directory should not be synchronized. The file \"${SFILE}\" is skipped." | tee -a "${REPORT}" ; continue; }

    # Вычленяем из полного адреса файла его директорию
    SFILE_DIR=$(dirname ${SFILE})

    # Запрашиваем "хеш" файла из индексной директории
    INDEXED_HASH=`cat "${INDEX_DIR}${SFILE}" 2>/dev/null`

    # Выясняем время изменения файлов и "хешей" (если таковые имеются)
    SFILE_CTS=$(stat -c %Z "${SFILE}" 2>/dev/null)
    INDEXED_HASH_CTS=$(stat -c %Z "${INDEX_DIR}${SFILE}" 2>/dev/null)
    [ -z "${INDEXED_HASH_CTS}" ] && { INDEXED_HASH_CTS="0"; }

    # Проверяем наличие "хеша" или отставание даты его изменения от исходного файла
    if [ -z "${INDEXED_HASH}" -o "${SFILE_CTS}" -gt "${INDEXED_HASH_CTS}" ] ; then

      # Генерируем "хеш" содержимого файла
      SFILE_HASH=`sha1sum "${SFILE}" | cut -d " " -f1`

      # Запускаем шифрование и выгрузку файла
      crypt-upload

      # Случайным образом запускаем профилактические проверки
      # (примерно по одной на десяток итераций)
    elif [ "$(( $RANDOM % 10 ))" == "0" ] ; then

      # Генерируем "хеш" содержимого файла
      SFILE_HASH=`sha1sum "${SFILE}" | cut -d " " -f1`

      # Проверяем совпадение "хеша" с имеющимся в индексной директории
      if [ "${INDEXED_HASH}" != "${SFILE_HASH}" ] ; then

        # Запускаем шифрование и выгрузку файла
        crypt-upload
      else

        # Проверяем наличие файла на внешнем хранилище
        LSRESULT=`s3cmd ls "${S3_BUCKET}${SFILE}" 2>/dev/null`
        if [ "${?}" == "0" -a -z "${LSRESULT}" ] ; then

          # Запускаем шифрование и выгрузку файла
          crypt-upload
        fi
      fi
    fi

  done
done <<< "$(cat ${SYNC_DIR_LIST})"

###
### Удаление из внешнего хранилища несинхронизируемых файлов
###
echo "Removing unsynchronized files from external storage..." | tee -a "${REPORT}"

# Запрашиваем из внешнего хранилища листинг всех файлов рекурсивно
ALIST=$(s3cmd ls --recursive "${S3_BUCKET}" 2>/dev/null)

# Перебираем листинг файлов внешнего хранилища
IFS=$'\n' # (разбор текстового массива по переносам строк)
for ALIST_ITEM in ${ALIST[@]} ; do

  # Вычленяем относительный адрес файла из полного URL внешнего хранилища
  AFILE=$(echo "${ALIST_ITEM}" | awk -F "${S3_BUCKET}" '{print $2}' 2>/dev/null)
  [ -z "${AFILE}" ] && { echo "FAILED allocate file name from the storage listing line \"${ALIST_ITEM}\"! Skipped." | tee -a "${REPORT}" ; continue; }

  # Выявляем несинхронизируемые файлы на внешнем хранилище
  # (отсутствующие в локальном хранилище и не входящие в список синхронизируемых)
  if [ ! -f "${AFILE}" -o $(echo "${AFILE}" | grep -cFf "${SYNC_DIR_LIST}" 2>/dev/null) -eq 0 ] ; then

    # Удаляем из внешнего хранилища несинхронизируемые файлы
    s3cmd del "${S3_BUCKET}${AFILE}" >> "${LOG}" 2>&1
    if [ "${?}" == "0" ] ; then
      echo "Obsolete file \"${S3_BUCKET}${AFILE}\" has been successfully deleted from the remote storage." | tee -a "${REPORT}"

      # Профилактически удаляем "хеш" из локальной индексной директории
      rm -f "${INDEX_DIR}${AFILE}"
    else
      echo "FAILED delete an obsolete file \"${S3_BUCKET}${AFILE}\" from remote storage!" | tee -a "${REPORT}"
    fi
  fi

done
unset IFS

# Отправляем почтовое уведомление администратору сервиса
echo -e "Content-Type: text/plain; charset=UTF-8\nFrom: ${MAIL_FROM}\nSubject: l2ce-sync: Report from ${HOSTNAME} on uploading encrypted backups to AWS:S3\n$(echo ${DATE})\n$(cat ${REPORT})\nSee the detailed event log in the file ${LOG}" | sendmail ${MAIL_TO}

# Вливаем отчёт в журнал и удаляем временный файл
cat "${REPORT}" >> "${LOG}"
rm -f "${REPORT}"

exit $?

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

# chown -R root /usr/local/etc/l2ce
# chmod -R go-rwx /usr/local/etc/l2ce

Запуск осуществляется самым простейшим образом, без аргументов:

# /usr/local/etc/l2ce/l2ce-sync.sh

Налаживаем регулярный запуск синхронизации.

На моей практике простая сверка синхронности локального и "облачного" хранилища при отсутствии выгрузок (только перебор всех файлов с избирательной сверкой "хешей") проходит для общего количества около 1500 файлов примерно за 10-12 минут.

Проверка синхронности и выгрузка появившихся за сутки файлов резервных копий (ежедневно прирастает около 10-15GB) для общего количества около 1500 файлов у меня занимает примерно полчаса-час времени.

Исходя из вышеприведённого считаю удобным запускать синхронизацию перед началом рабочего дня, когда все задания резервного копирования уже завершились, а персонал предприятия активно работать ещё не начал:

# vi /etc/crontab

....
# Regular synchronization of "backup.example.net" backups to external storage AWS-S3
0 8  * * *  root /usr/local/etc/l2ce/l2ce-sync.sh 1>/dev/null &

Налаживаем загрузку из S3-хранилища и расшифровку файлов.

Зашифрованный симметричным методом файл можно расшифровать одной командой:

$ /usr/bin/gpg -d --verbose --batch --yes --passphrase-fd 0 -o ./file.decrypted ./file

....
gpg: AES256 encrypted data
gpg: encrypted with 1 passphrase
gpg: original file name='file'

Напишем bash-скрипт, загружающий из S3-хранилища интересующие нас файлы (или наборы таковых, по маске) и расшифровывающих их в указанное место локальной файловой системы:

# vi /usr/local/etc/l2ce/l2ce-decrypt.sh && chmod +x /usr/local/etc/l2ce/l2ce-decrypt.sh

#!/bin/bash

# Принимаем входящие переменные
GET_FILTER=${1}
TARGET_DIR=${2}
[ -z "${GET_FILTER}" -o -z "${TARGET_DIR}" ] && { echo "Example usage: $0 /home/user/.backup /var/tmp/decrypt-place" | tee -a "${REPORT}" ; exit 1; }

# Задаём указатели на ресурсы
GPG_PASSPHRASE=/usr/local/etc/l2ce/.gpg-passphrase
GPG_TMPDIR=/var/tmp
#
S3CMD_CREDENTIALS=/usr/local/etc/l2ce/.s3cmd
S3_BUCKET="s3://backup.example.net"
#S3_BUCKET="s3://backup.example.net.s3.amazonaws.com"
#
REPORT=$(mktemp --tmpdir=/var/tmp);
LOG=/var/log/l2ce-decrypt.log
DATE=`date +"%Y-%m-%d %H:%M:%S"`
echo >> "${LOG}"
echo "${DATE}" >> "${LOG}"

# Загружаем переменные окружения "s3cmd"
source "${S3CMD_CREDENTIALS}"

# Запрашиваем из внешнего хранилища листинг подходящих файлов рекурсивно
AFLIST=$(s3cmd ls --recursive "${S3_BUCKET}" 2>/dev/null | grep "${GET_FILTER}" 2>/dev/null)

# Перебираем листинг файлов внешнего хранилища
IFS=$'\n' # (разбор текстового массива по переносам строк)
for AFLIST_ITEM in ${AFLIST[@]} ; do

  # Вычленяем относительный адрес файла из полного URL внешнего хранилища
  AFILE=$(echo "${AFLIST_ITEM}" | awk -F "${S3_BUCKET}" '{print $2}' 2>/dev/null)
  [ -z "${AFILE}" ] && { echo "FAILED allocate file name from the storage listing line \"${AFLIST_ITEM}\"! Skipped." | tee -a "${REPORT}" ; continue; }

  # Пропускаем загрузку в случае наличия расшифрованного файла
  [ -f "${TARGET_DIR}${AFILE}" ] && { echo "The target file (probably decrypted) already exists at \"${TARGET_DIR}${AFILE}\". Skipped." | tee -a "${REPORT}" ; continue; }

  # Подготавливаем место для загрузки шифрованного файла
  GPG_SESSION_DIR=$(mktemp -d ${GPG_TMPDIR}/.gpgXXXXX)
  mkdir -p "${GPG_SESSION_DIR}${AFILE_DIR}"

  # Загружаем файл из внешнего хранилища
  s3cmd get --no-progress --stats --no-encrypt "${S3_BUCKET}${AFILE}" "${GPG_SESSION_DIR}${AFILE}" >> "${LOG}" 2>&1
  if [ "${?}" == "0" ] ; then

    # Подготавливаем место для размещения расшифрованного файла
    AFILE_DIR=$(dirname ${AFILE})
    mkdir -p "${TARGET_DIR}${AFILE_DIR}"

    # Расшифровываем загруженный из внешнего хранилища файл
    /usr/bin/gpg -d --verbose --batch --yes --passphrase-file "${GPG_PASSPHRASE}" -o "${TARGET_DIR}${AFILE}" "${GPG_SESSION_DIR}${AFILE}" >> "${LOG}" 2>&1
    if [ "${?}" == "0" ] ; then
     echo "The file \"${S3_BUCKET}${AFILE}\" is successfully downloaded from the remote storage and decrypted to the \"${TARGET_DIR}${AFILE}\"." | tee -a "${REPORT}"
    else
      echo "FAILED decrypt a file \"${S3_BUCKET}${AFILE}\" in temporary \"${GPG_SESSION_DIR}${AFILE}\"!" | tee -a "${REPORT}"
    fi
  else
    echo "FAILED download a file \"${AFILE}\" from remote storage \"${S3_BUCKET}\"!" | tee -a "${REPORT}"
  fi

  # Удаляем директорию с промежуточным зашифрованным файлом
  rm -rf "${GPG_SESSION_DIR}"

done

# Вливаем отчёт в журнал и удаляем временный файл
cat "${REPORT}" >> "${LOG}"
rm -f "${REPORT}"

exit $?

Закрываем скрипт от посторонних, во избежание конфузов:

# chown -R root /usr/local/etc/l2ce
# chmod -R go-rwx /usr/local/etc/l2ce

Скрипту загрузки и расшифровки в качестве аргументов подаём относительный адрес файла (или директории) в S3-хранилище и абсолютный адрес желаемого месторасположения расшифрованных файлов в локальной файловой системе:

# /usr/local/etc/l2ce/l2ce-decrypt.sh /home/user/.backup /var/tmp/decrypt-probe

Конфигурируем почтовую подсистему.

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

# aptitude install ssmtp

Конфигурационный файл с параметрами утилиты отправки почты "msmtp":

# vi /etc/ssmtp/ssmtp.conf

....
mailhub=mail.example.net
hostname=backup.example.net
FromLineOverride=YES

Проверяем возможность отправки почты с несущей операционной системы:

# echo "Test" | msmtp --debug --account=default username@example.net

Наладка ротации файлов журнала событий.

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

# vi /etc/logrotate.d/l2ce

/var/log/l2ce-*.log {
  monthly
  rotate 12
  compress
  delaycompress
  missingok
  notifempty
  copytruncate
  su root root
}

Проверяем корректность конфигурации, не воздействуя при этом на файлы журналов:

# logrotate -d /etc/logrotate.d/l2ce

Эксплуатация.

По состоянию на первый квартал 2021 года описанная в этой публикации схема работает без единого сбоя на двух предприятиях.


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )