#!/usr/bin/env bash # Off-box копия рантайм-конфига продукта (#2203, четвёртый пункт приёмки). # # ЗАЧЕМ. В дампах есть колонки под `pgp_sym_encrypt`. Ключ к ним лежит в # рантайм-конфиге на самой машине. Дампы уезжают в S3 ежедневно, ключ — никуда. # Потеря машины означает: дампы есть, расшифровать нечем. Ради этой связки вся # сложность с шифрованием в базе и заводилась, и именно она сегодня не работает. # # КУДА КЛАДЁМ И ПОЧЕМУ ИМЕННО ТУДА. В тот же S3, но под отдельный префикс # `env/` и ТОЛЬКО в зашифрованном виде. Разбор вариантов: # # - Рядом с дампами в открытом виде — нельзя. Ключ рядом с шифротекстом # обнуляет шифрование: одна утечка доступа к бакету отдаёт и то и другое. # - На соседний хост по SSH — потребовало бы завести доверие между машинами, # которого сейчас нет (проверено: `Permission denied (publickey)`), то есть # РАСШИРИТЬ периметр доступа ровно тогда, когда #3075 его сужает. # - Отдельный бакет с отдельными ключами — правильнее всего, но требует новых # учётных данных, которых у скрипта нет. # # Выбран третий по надёжности, но единственный исполнимый без расширения # доступа: шифротекст в S3, парольная фраза — вне S3, у владельца. # # FAIL-CLOSED. Без парольной фразы скрипт НЕ выгружает файл открытым — он # падает с явным сообщением. Молчаливая выгрузка ключа в тот же бакет, где # лежат дампы, хуже отсутствия копии: она создаёт ложное чувство защищённости. # # ОДИН ШАГ ЗА ВЛАДЕЛЬЦЕМ. Парольная фраза обязана жить там, где переживёт смерть # машины, иначе копия бесполезна: расшифровать будет нечем. Поэтому её нельзя # сгенерировать здесь и оставить только на хосте. # # 1. Придумать/сгенерировать фразу и положить её в vault # (`meta/00_credentials.md`) — это отдельная система на другой машине. # 2. Добавить строкой в /etc/default/gendesign-backup: # ENV_OFFBOX_PASSPHRASE='<та же фраза>' # # Восстановление: скачать .gpg из s3:///env/, затем # gpg --batch --decrypt --passphrase '<фраза>' <файл>.gpg > .env.runtime set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=./lib-backup.sh source "$SCRIPT_DIR/lib-backup.sh" BACKUP_ENV_FILE="${BACKUP_ENV_FILE:-/etc/default/gendesign-backup}" SOURCE_FILE="${ENV_OFFBOX_SOURCE:-/opt/gendesign/backend/.env.runtime}" STAGE_DIR="${ENV_OFFBOX_STAGE:-/opt/gendesign/backups/env}" KEEP="${ENV_OFFBOX_KEEP:-14}" log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*"; } # shellcheck source=/dev/null [[ -f "$BACKUP_ENV_FILE" ]] && source "$BACKUP_ENV_FILE" if [[ ! -f "$SOURCE_FILE" ]]; then log "ОШИБКА: нечего копировать — $SOURCE_FILE не найден." notify "🔴 off-box копия рантайм-конфига: исходный файл не найден на $(hostname)" exit 1 fi # Fail-closed: см. шапку. Открытым в бакет с дампами — никогда. if [[ -z "${ENV_OFFBOX_PASSPHRASE:-}" ]]; then log "ОШИБКА: ENV_OFFBOX_PASSPHRASE не задан в $BACKUP_ENV_FILE." log "Открытым в тот же бакет, где лежат дампы, выгружать НЕ будем — это" log "обнулило бы шифрование колонок. Как задать — см. шапку скрипта." notify "🔴 off-box копия рантайм-конфига НЕ делается: не задана парольная фраза (см. ops/backup-env-offbox.sh)" exit 1 fi if [[ -z "${S3_BUCKET:-}" || -z "${S3_ENDPOINT:-}" || -z "${S3_ACCESS_KEY:-}" || -z "${S3_SECRET_KEY:-}" ]]; then log "ОШИБКА: не заданы S3_* — выгружать некуда." notify "🔴 off-box копия рантайм-конфига: не заданы S3-параметры на $(hostname)" exit 1 fi mkdir -p "$STAGE_DIR" chmod 700 "$STAGE_DIR" STAMP="$(date -u +'%Y%m%d_%H%M%S')" OUT="$STAGE_DIR/env-runtime-$(hostname -s)-${STAMP}.gpg" # --batch + --passphrase-fd 3: фраза уходит через дескриптор, а не аргументом # командной строки — иначе она видна в `ps` любому пользователю машины. if ! gpg --batch --yes --symmetric --cipher-algo AES256 \ --passphrase-fd 3 --output "$OUT" "$SOURCE_FILE" 3<<<"$ENV_OFFBOX_PASSPHRASE"; then log "ОШИБКА: шифрование не удалось, выгрузки не будет." rm -f "$OUT" notify "🔴 off-box копия рантайм-конфига: шифрование не удалось на $(hostname)" exit 1 fi chmod 600 "$OUT" # Проверяем, что получившийся файл действительно расшифровывается ТОЙ ЖЕ фразой. # Без этой проверки мы бы годами возили в S3 нечитаемый мусор и узнали об этом # в момент, когда он понадобился. if ! gpg --batch --decrypt --passphrase-fd 3 --output /dev/null "$OUT" 3<<<"$ENV_OFFBOX_PASSPHRASE" 2>/dev/null; then log "ОШИБКА: файл зашифрован, но обратно не читается — выгрузки не будет." rm -f "$OUT" notify "🔴 off-box копия рантайм-конфига: проверка расшифровкой провалилась на $(hostname)" exit 1 fi log "Зашифровано и проверено расшифровкой: $(basename "$OUT") ($(stat -c %s "$OUT") байт)" log "Выгружаю в s3://${S3_BUCKET}/env/" # AWS_CA_BUNDLE — та же причина, что в ops/backup.sh: собственный CA-набор # aws-cli не содержит корня, которым подписан сертификат Selectel. if ! docker run --rm \ -e AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY" \ -e AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \ -e AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ -v "$STAGE_DIR":/env:ro \ amazon/aws-cli:latest \ --endpoint-url "$S3_ENDPOINT" \ s3 cp --no-progress "/env/$(basename "$OUT")" "s3://${S3_BUCKET}/env/"; then log "ОШИБКА: выгрузка в S3 не удалась." notify "🔴 off-box копия рантайм-конфига: выгрузка в S3 не удалась на $(hostname)" exit 1 fi log "Выгрузка ok." # Локальная ротация. Держим несколько последних: рантайм-конфиг меняется редко, # и старая копия — единственный способ расшифровать дамп, снятый ДО правки. mapfile -t OLD < <(ls -1t "$STAGE_DIR"/env-runtime-*.gpg 2>/dev/null | tail -n +$((KEEP + 1))) if ((${#OLD[@]})); then printf '%s\n' "${OLD[@]}" | xargs -r rm -f log "Локально удалено копий сверх ${KEEP}: ${#OLD[@]}" fi log "Готово."