Skip to content

0071. S3-совместимый vault резервных копий (B3) ​

Статус: proposed: решение вводит внешнего провайдера и исходящую границу доверия, до решения Ian Panaev код не пишется (AGENTS.md, «Обязательные границы»). Дата: 2026-10-07. Дополняет ADR 0054, ADR 0056 и ADR 0070; закрывает остаток этапа B3 плана BACKUP_RECOVERY («S3-compatible contract suite»). Не решает Object Lock и неизменяемость (этап B4) и не касается основного хранилища blob (ADR 0002).

Контекст ​

Vault ADR 0054 живёт в каталоге на диске или NAS. Копия «вне площадки» требует объектного хранилища: облачного S3, MinIO, Ceph или S3-совместимого сервиса провайдера. План уже называет «S3-совместимый vault» вторым адаптером назначения, а ADR 0054 оставил для него узкий порт.

Факты кода, на которые опирается решение:

  • Приложение видит vault только через порты CaptureVault, ReadableVault и MaintainedVault (backup-ports.ts, backup-agent-ports.ts); capture, verify, restore и retention не знают о каталогах. Единственная реализация — FileVault.
  • Опубликованная точка в файловом vault — каталог, публикуется одним rename из points/.staging/, а COMMITTED записывается последним. В объектном хранилище rename каталога нет.
  • Blob неизменяем и именуется ID загрузки (ADR 0054), SHA-256 и размер проверяются до публикации. Шифрование (ADR 0070) работает над потоком файла и привязано к имени файла внутри vault, а не к пути на диске.
  • Исключительную блокировку prune и общую блокировку capture держит PostgreSQL источника (18471/21), а не vault: хранилище блокировок не нужно.
  • Фиксированного предела размера blob нет (AGENTS.md): потолок задаёт раскладка загрузки MAX_OBJECT_BYTES = 10 000 частей × 1 ГиБ ≈ 9,77 ТиБ (object-size.ts), оператор может задать меньший. Зашифрованный файл blob больше содержимого на 44 + 16 × ⌈размер / 1 МиБ⌉ байт (ADR 0070).
  • Многочастная загрузка S3: часть от 5 МиБ до 5 ГиБ (последняя может быть меньше), не более 10 000 частей, объект не больше 5 ТиБ. Поэтому фиксированный размер части ограничивает объект (16 МиБ × 10 000 ≈ 156 ГиБ), а blob больше 5 ТиБ в один объект S3 не помещается.

Решение ​

  1. Новый адаптер S3Vault реализует те же три порта; capture, verify, restore, retention, агент, manifest, каталог и API не меняются. Vault выбирается адресом s3://<bucket>/<prefix>; vault init создаёт vault.json (версия 2, зашифрованный) под префиксом. Vault на S3 всегда зашифрован (ADR 0070): открытый vault на S3 не создаётся и не подключается (vault init --no-encryption для s3:// отвергается). Данные уходят за пределы доверенной площадки, а ключ остаётся у оператора.
  2. Раскладка объектов. vault.json, keys/<slot>.json, blobs/<xx>/<id> как в файловом vault. Точка пишется сразу в points/<pointId>/<attempt>/… (attempt — случайный UUID попытки), без копирования. Публикация — условная запись points/<pointId>/COMMITTED с If-None-Match: *: проигравший конкурент получает 412 и сообщает exists; запись COMMITTED называет префикс попытки и SHA-256 манифеста. Чужие и брошенные префиксы попыток удаляет prune, как брошенные каталоги .staging. Читатель видит только то, что назвала COMMITTED.
  3. Blob. Поток шифруется и отправляется многочастной загрузкой. Размер части выбирается до начала загрузки по известному размеру зашифрованного файла E, как в object-size.ts: начиная с 16 МиБ, удваивается, пока ⌈E / часть⌉ > 10 000; так часть не меньше ⌈E / 10 000⌉, кратна 16 МиБ и не превышает 1 ГиБ для любого объекта до 5 ТиБ (512 МиБ × 10 000 ≈ 4,88 ТиБ, 1 ГиБ × 10 000 ≈ 9,77 ТиБ). Часть в память целиком не загружается: она передаётся потоком, повтор перечитывает её диапазон из источника. Blob, чей зашифрованный размер больше 5 ТиБ, S3 не принимает: адаптер проверяет размер до CreateMultipartUpload и отказывает отдельным кодом vault_object_too_large (не vault_full: квота тут ни при чём); захват завершается этой ошибкой без частичных загрузок и без точки, предупреждение и журнал называют код, а не объект. Такой blob копируется только в файловый vault. Проверка возможностей при подключении сообщает этот предел. SHA-256 и размер содержимого считаются во время потока, CompleteMultipartUpload вызывается только при совпадении, иначе AbortMultipartUpload. До завершения объект не виден, поэтому hasBlob и повтор capture видят только законченные blob. Незавершённые загрузки прерываются при prune и политикой бакета (правило жизненного цикла AbortIncompleteMultipartUpload, оно проверяется при подключении).
  4. Проверка возможностей при подключении (служба на сервере-источнике, как в BACKUP_RECOVERY): TLS и проверка сертификата; запись, чтение и удаление пробного объекта; условная запись If-None-Match: * отвергает вторую запись того же ключа (без неё публикация не атомарна — backend отвергается с понятным кодом vault_unsupported); многочастная загрузка; DeleteObjects; чтение «после записи» и список в пределах префикса. Результат показан в консоли; включённое версионирование бакета предупреждает («prune освобождает место только после удаления старых версий»). Object Lock не проверяется и не обещается: консоль пишет «защита от удаления не подтверждена».
  5. Доступ. Учётные данные (access key id и secret) лежат в файле ARKVORY_BACKUP_VAULT_S3_CREDENTIALS_FILE с правами 0600, как ключ агента (ADR 0070); в аргументах командной строки, переменных окружения со значениями, журналах, API и manifest их нет. Рекомендуемая роль в бакете: чтение, запись и удаление под выделенным префиксом. Источник, у которого есть право удаления, может уничтожить копии: это задокументировано, отдельный профиль «источник не может удалять» — этап B4.
  6. Endpoint задаёт администратор (--vault-endpoint, регион, адресация path-style для MinIO). Исходящий адрес — доверенная настройка администратора, а не пользовательский ввод: в отличие от вебхуков (ADR 0069) закрытые сети разрешены (MinIO в LAN — типичный случай), но схема только https (http — лишь с явным --vault-allow-insecure для localhost и тестов), перенаправления не выполняются, тело ответа ограничено.
  7. Повторы и сбои. Повтор на 5xx, 429 и сетевых ошибках с экспоненциальной паузой и пределом (до 6 попыток на запрос, общий дедлайн операции); отмена задания прерывает загрузку. Ошибки доступа сводятся к кодам vault_unavailable, vault_key_*, vault_full (квота) и integrity_mismatch; heartbeat, предупреждения и метрики как у файлового vault. Свободное место неизвестно: volume() для S3 возвращает «неизвестно», предупреждение vault_low_space не выдаётся, остаётся предупреждение по бюджету.
  8. Клиент S3. Клиентская библиотека — зависимость и граница безопасности (подпись запросов, TLS). Рассмотрены два варианта: (а) SigV4 поверх node:https в самом коде — около 400 строк, без новых пакетов, но подпись и разбор XML-ответов поддерживаются нами; (б) @aws-sdk/client-s3 — сопровождаемый клиент, но десятки транзитивных пакетов в цепочке поставки. Рекомендация: (а) с тестами против опубликованных тестовых векторов SigV4 и контрактным набором на MinIO; решение за Ian Panaev.
  9. Совместимость. Файловые vault не меняются. Схема БД, API и manifest не меняются, кроме аддитивного признака вида vault в статусе (vault.kind: file или s3). Старый релиз отвергает s3:// как неподдерживаемый адрес.

Альтернативы ​

  • restic или rclone за адаптером (ADR 0037): внешний бинарник, лицензия, SBOM, спайк на Windows, второй формат хранения и второй механизм ключей; не даёт нашей модели атомарной публикации точки и проверки manifest. Остаётся запасным вариантом, порт не мешает.
  • Монтирование бакета как каталога (s3fs, rclone mount): семантика rename, fsync и блокировок не гарантируется; атомарность публикации и сверка размера (hasBlob) ломаются. Отвергнуто.
  • Копирование готового файлового vault в S3 внешним инструментом (rclone sync): простой запасной путь для оператора, но не интегрирован в статус, retention и проверку; зашифрованный vault (ADR 0070) уже безопасно копируется так. Документируется как поддерживаемый ручной вариант, а не как замена.
  • Дедупликация чанками, packs: отвергнута в ADR 0054, не пересматривается.

Последствия ​

  • Добавляется исходящая граница доверия и, при варианте (б), зависимость; нужны записи в SECURITY.md и в модели угроз.
  • Стоимость: запросы и исходящий трафик при restore и verify (--deep читает всё); консоль показывает объём чтения перед запуском и не запускает глубокую проверку по расписанию без явного включения.
  • Скорость ограничена каналом и числом одновременных частей (по умолчанию 4); шифрование (ADR 0070) около 260 МиБ/с на поток не является узким местом.
  • Без условной записи backend не принимается: часть старых S3-совместимых систем будет отвергнута, список проверенных backend ведётся в документации.
  • Restore с нуля на новом сервере работает с s3://-адресом, файлом учётных данных и recovery kit, без сервера-источника (автономный recovery wizard — отдельный этап B3).

Проверка ​

Контрактный набор адаптера (одни и те же тесты для FileVault и S3Vault): capture → verify --deep → restore в пустую цель; конкурентная публикация одной точки (ровно один committed, второй exists); прерванная загрузка blob не видна; подменённый, усечённый и переставленный объект отвергается; prune удаляет только неперечисленные blob и брошенные попытки; повтор после 500, 429, обрыва соединения и истечения дедлайна; неверные и просроченные учётные данные, закрытый бакет, переполненная квота; backend без условной записи отвергается при подключении; учётные данные и ключи не попадают в журнал, argv, ответы API. Интеграционный гейт поднимает MinIO (образ закреплён по digest). Реальные S3 AWS и облачные провайдеры в гейт не входят; результат ручной проверки фиксируется в IMPLEMENTATION_STATUS как непроверенный до выполнения.

Открытые вопросы для Ian Panaev ​

  1. Принять ли внешнего провайдера и исходящую границу доверия (S3).
  2. Клиент: SigV4 в коде (рекомендация) или @aws-sdk/client-s3.
  3. Минимальный набор backend для приёмки: MinIO и один облачный (какой?).
  4. Принимается ли правило «vault на S3 только зашифрованный».
  5. Принимается ли отказ vault_object_too_large для blob больше 5 ТиБ (такой blob копируется только в файловый vault).

Arkvory · ProAnimaStudio