"""Off-box копия рантайм-конфига не должна уезжать в S3 открытой (#2203). ЗАЧЕМ КОПИЯ. В дампах есть колонки под `pgp_sym_encrypt`. Ключ к ним лежит в рантайм-конфиге на самой машине. Дампы уезжают в S3 ежедневно, ключ — никуда: потеря машины означает «дампы есть, расшифровать нечем». ГЛАВНОЕ СВОЙСТВО, КОТОРОЕ ЗДЕСЬ СТОРОЖИТСЯ. Копия кладётся в тот же бакет, что и дампы — другого места без расширения периметра доступа нет (SSH между хостами отсутствует, проверено). Поэтому она обязана быть зашифрована: ключ рядом с шифротекстом обнуляет шифрование целиком. Отсюда fail-closed: без парольной фразы скрипт ПАДАЕТ, а не выгружает файл открытым. Молчаливая выгрузка ключа в бакет с дампами хуже отсутствия копии — она создаёт ложное чувство защищённости. Проверено прогоном на хосте 27.08 (подставной исходник, боевой не трогался): без фразы → код 1, файлов создано 0 с фразой → «Зашифровано и проверено расшифровкой», 110 байт своей фразой читается, чужой — нет """ from __future__ import annotations import re from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[3] SCRIPT = REPO_ROOT / "ops" / "backup-env-offbox.sh" CRON = REPO_ROOT / "ops" / "crontab-poincare.cron" def _text() -> str: return SCRIPT.read_text(encoding="utf-8") def _code() -> str: """Только исполняемые строки, без комментариев. В шапке скрипта лежит инструкция по восстановлению, и в ней тоже есть `gpg --decrypt`. Проверки порядка обязаны смотреть на код — иначе они сравнивают позиции в документации, а не в программе. """ lines = [ln for ln in _text().splitlines() if not ln.lstrip().startswith("#")] return chr(10).join(lines) def test_script_exists_and_is_wired_into_cron() -> None: """Скрипт без вызова — мёртвый код; страховка от проверки пустоты.""" assert SCRIPT.is_file(), "ops/backup-env-offbox.sh пропал" assert "backup-env-offbox.sh" in CRON.read_text(encoding="utf-8"), ( "скрипт не вызывается из crontab-poincare.cron — копия делаться не будет" ) def test_refuses_to_run_without_passphrase() -> None: """Без парольной фразы — выход с ошибкой, а не выгрузка открытого файла.""" guard = re.search( r'if \[\[ -z "\$\{ENV_OFFBOX_PASSPHRASE:-\}" \]\]; then(.+?)\bfi\b', _text(), re.S ) assert guard, "нет проверки на заданность парольной фразы" assert "exit 1" in guard.group(1), ( "проверка есть, но скрипт продолжает работу — файл уедет открытым" ) def test_encryption_happens_before_upload() -> None: """Шифрование обязано стоять ДО выгрузки, иначе порядок ничего не гарантирует.""" code = _code() assert code.index("--symmetric") < code.index("s3 cp"), "выгрузка идёт раньше шифрования" assert "--cipher-algo AES256" in code def test_passphrase_never_passed_as_command_line_argument() -> None: """Фраза уходит через дескриптор, а не аргументом. `--passphrase '<фраза>'` в командной строке видна в `ps` любому пользователю машины — на хосте, где крутится прод, это не теоретическая придирка. """ code = _code() assert "--passphrase-fd 3" in code, "фраза не передаётся через дескриптор" assert not re.search(r'--passphrase\s+"?\$\{?ENV_OFFBOX_PASSPHRASE', code), ( "фраза передаётся аргументом командной строки — видна в ps" ) def test_encrypted_file_is_verified_by_decrypting_it_back() -> None: """Проверка обратным чтением — иначе годами возили бы нечитаемый мусор. Отказ такой копии обнаруживается ровно в тот момент, когда она понадобилась, то есть в худший из возможных. """ code = _code() assert "--decrypt" in code, "нет проверки расшифровкой после шифрования" assert code.index("--symmetric") < code.index("--decrypt") < code.index("s3 cp"), ( "проверка расшифровкой стоит не между шифрованием и выгрузкой" )