DarkRiDDeR14 мин

Почему preview, approval и audit trail не доказывают безопасный change

НадёжностьПроцессы

У массовой автоматизации есть опасная иллюзия: если pipeline показал preview, получил approval и оставил audit log, то change якобы стал безопасным. Симптом заметен после incident: в log есть аккуратный record, reviewer помнит кнопку approve, а реальный scope оказался шире или состояние цели успело измениться. Цена ошибки — не только неверные объекты. Команда тратит часы на доказательство того, что именно произошло, и может повторить ошибку rollback-скриптом, которому доверили старый контекст.

Причина в смешении типов доказательств. Preview отвечает о предложении, authority — о допустимом пределе, approval — о согласии на связанный request, audit trail — о записанном событии, verification — о наблюдаемом результате. Ни один из них не выводится из другого автоматически. Технически зрелый процесс проверяет переход между карточками и умеет остановиться при любом разрыве. Он не называет «green pipeline» доказательством правильности, пока не названо, что конкретно было измерено.

Пять утверждений, которые нельзя склеивать

Артефакт и его логическое значение
АртефактЧто можно утверждать честноТиповая ложная интерпретацияНужное следующее доказательство
previewв фиксированный момент рассчитан proposed change для зафиксированного selectionреальный execute будет таким жеповторная проверка preconditions рядом с execute
authoritypolicy ограничила допустимый selector или размер scopeоперация полезна и корректнаreview содержимого preview и intent
approvalкто-то разрешил связанный request в своей ролиreviewer проверил каждую техническую детальbinding id/digest и понятная review policy
audit trailсистема зафиксировала событие по выбранному contractсобытие необратимо доказано и state верныйсвойства storage плюс independent verification
verificationавторитетный reader наблюдал заданное условиевсе последствия change устраненыmonitoring, residual-risk owner или новый rollback plan
  1. Зафиксировать proposal. Сохранить operation id, selector, dense targets, digest и version, а не только красивый diff.
  2. Сравнить scope с authority. Проверить selector и limit до того, как появится approval request.
  3. Связать approval буквально. Operation, preview, target и authority ids должны совпасть без implicit default.
  4. Отделить receipt от evidence. После execute читать authoritative target и только затем решать, закрывать operation или готовить новый plan.
Матрица из строк authority level и столбцов target scope, preview digest, approval binding, execution и verification. Зелёная клетка разрешает только bounded scope; красные клетки показывают, что превышение лимита, другой selector или чужой digest должны остановить цепочку.
Authority сравнивается со scope, но не с «качеством идеи». Поэтому она защищает от расширения операции, а не заменяет review или verification.

Preview — снимок намерения с известным сроком годности

Terraform отделяет speculative plan от apply: изменения target system между ранним plan и final apply могут изменить результат. Это относится и к самописному orchestration: состояние меняют другой job, человек или controller. Preview — вход в review, а не билет на исполнение через час.

Чтобы превратить preview в contract, сохраняют не только human-readable diff. Нужны stable operation id, model/config version, selector, dense target list, target digest, expected precondition и proposal digest. Digest — не мистическая защита: он просто позволяет проверять равенство того, что видел reviewer, и того, что получил executor. Надёжность hash, подписи и collision resistance — отдельные свойства конкретной реализации. В fixed model digest — статическая строка именно поэтому; fixture проверяет связь, а не криптографию.

Dry-run не обладает одной силой

Называть любой безопасный расчёт dry-run вредно. Kubernetes reference для kubectl apply различает client dry-run и server dry-run. Первый печатает object без отправки; второй отправляет server-side request, но не сохраняет ресурс. Это два разных наблюдения: один проверяет serialization и локальную обработку, другой может пройти часть server-side пути. Они полезны, но neither превращается в утверждение о будущем persistent change.

В инженерном process рядом с названием preview пишут source of truth и side-effect boundary. Например: «local model preview, no external read», «server admission dry-run, no persistence» или «read-only inventory with fixed snapshot». Тогда reviewer понимает, какие questions ещё открыты. Если source inventory старый, target list может быть уже неверен. Если server dry-run прошёл, policy может поменяться до execute. Если preview собран на client, он может не знать admission rule. Каждая граница добавляет самостоятельный check, а не больше уверенности в одном цветном статусе.

Authority ограничивает мощность, не доказывает смысл

Authority card в хорошем дизайне мала и машинно проверяема: level, allowed selector, maximum count, time window и исключения. Она не хранит длинное объяснение change и не решает за owner, стоит ли менять поле. Её задача — не позволить approval от малой операции «переехать» на больший scope. Если preview содержит три targets, а authority допускает два, executor не округляет limit и не просит пользователя «подтвердить ещё раз» в том же запуске. Он возвращает stop-scope-exceeds-authority-limit.

GitHub Docs для environments показывает похожую, но не тождественную механику: job ожидает protection rules, а секреты environment недоступны до их прохождения. Это gate доступа к стадии запуска. В документе есть и bypass boundary: возможность администратора обходить правила зависит от настройки environment. Следствие для собственного runner простое: bypass — тоже операция с отдельным identity, reason и audit event. Его нельзя скрыть внутри общего approved, иначе расследование не отличит нормальный путь от исключения.

import {
  createFixedSyntheticOperationCard,
  createFixedSyntheticAuthority,
  previewSyntheticAutomation,
  requestSyntheticApproval,
} from './upgrade-2025-05.mjs';

const preview = previewSyntheticAutomation(
  createFixedSyntheticOperationCard('fixed-over-scope-v1'),
);
const gate = requestSyntheticApproval(
  preview,
  createFixedSyntheticAuthority('fixed-over-scope-v1'),
);

console.log({ accepted: gate.accepted, reason: gate.reason, targetCount: gate.targetCount });
// Expected: false, stop-scope-exceeds-authority-limit, 3.
// The values are fixed synthetic in memory; no target is read or changed.

Пример показывает не «проверку прав пользователя», а exact contract. Card с тремя synthetic targets и authority максимум на два закрывает путь до approval. Никакой тайный fallback не оставляет третью запись «на потом», никакой реальный job не создаётся. Для production нужны identity provider, policy owner, storage и real-time conditions; они намеренно не входят в fixture.

Approval обязан связывать четыре значения

Минимальный approval связывает operation id, preview digest, target digest и authority id. Если хотя бы одно значение другое, согласие не относится к request. Отдельно полезны policy version, reviewer role, expiry и reason. Но поля не делают approval автоматическим доказательством: reviewer мог видеть неполное представление, policy могла быть слишком широкой, а time window — слишком длинным. Поэтому в high-impact operations approval сопровождают change owner, expected verification и stop owner, а не только именем reviewer.

Target mismatch — показательная авария. Preview может содержать synthetic-targets-a-b-v1, approval — synthetic-targets-a-c-v1, а оба выглядят похожими для человека. В fixed fixture это не «warning»: approval получает stop-approval-does-not-bind-preview-target-and-authority. Хороший runner использует такую же fail-closed семантику для отсутствующего, неизвестного и forged field. Иначе позитивный путь testируется, а ошибочный request случайно получает полезный default.

Audit trail хранит причинную цепочку, но не наблюдает state

Audit trail нужен потому, что у команды несколько источников истины: queue, executor, policy engine и target system. Он связывает phase transition одним operation id и удобен для восстановления последовательности. Но audit record может сказать «simulation recorded» и не может сам прочитать target. Поэтому audit fields не заменяют verification fields. Если их смешать, dashboard будет показывать «verified» по факту наличия log, хотя реальные targets никто не открыл.

Verification и rollback не должны быть скрытым retry

Verification начинается с измеримого expected state. Не «проверьте всё», а «reader X видит field Y со значением Z на scope digest D; отсутствующие targets перечислены; stale data имеет отдельный reason». Если read не прошёл, state не равен expectation или источник не авторитетен, status остаётся stopped. Green execution receipt не переписывают как verification.

Rollback нужен именно в этот момент, но не как автоматическая инверсия исходного payload. Что вернуть зависит от observed state: часть targets могла измениться вручную, исходное значение уже неактуально, а побочные объекты требуют другого порядка. Поэтому rollback plan содержит preconditions: собрать observed state, создать новый bounded scope, получить новую authority, подготовить новый preview и approve отдельную операцию. «Rollback planned» не равно «rollback executed» и не даёт права делать новый write.

Fail-closed ветки fixed fixture
НарушениеГде обнаруживаетсяРезультатПочему это важно
unknown или missing fieldpreview contractstop без proposalне возникает default operation
forged authorityauthority gatestop before approvalнельзя расширить limit подменой card
approval с другим target digestapproval bindingstop before simulationreview не переносится на другой selection
sparse или cyclic structurecanonical comparisonreject без exceptionfixture не скрывает некорректный input
forged audit recordverification entrystop, не verifiedrecord не становится state evidence

Ограничения и следующий проверяемый шаг

В model нет реального permission check, scheduler, external time, inventory, API, storage или production target. Все values fixed synthetic и лежат в памяти. Поэтому fixture не проверяет network race, cryptographic digest, identity, доступность audit store или честность reviewer. Она проверяет более узкую вещь: каждый переход принимает только exact card и закрывается на mismatch, missing, unknown, sparse или cyclic input.

Следующий проверяемый шаг: возьмите самый короткий batch automation и перечислите для него пять предикатов из первой таблицы. Для каждого напишите owner и data source. Затем удалите один binding field — например target digest — и попробуйте доказать, что executor всё ещё не сможет применить старый approval к новому scope. Если доказательство строится на дисциплине оператора, а не на contract, добавьте gate до расширения автоматизации.

Историческая граница мая 2025

В этой статье Terraform v1.10.5 используется только для границы speculative plan, GitHub Docs commit от 31 января 2025 — только для behaviour environment protection rules и bypass setting, Kubernetes reference commit от 14 марта 2024 — только для различия client/server dry-run. Все ссылки закреплены immutable commits и существовали к маю 2025. Матрица authority, stop codes и fixed cards — собственный учебный механизм автора; они не названы гарантией сторонних систем.

Проверяемые источники

  • HashiCorp Terraform plan, immutable commit 898e397 for v1.10.5 (22 January 2025) — Документ говорит, что plan сам не выполняет proposed changes; speculative plan описывает эффект без намерения применить его, а изменения target system между plan и apply могут изменить final effect, поэтому final non-speculative plan надо перепроверять. Граница: Документация Terraform не задаёт универсальный protocol approval, состав audit trail или rollback для произвольной автоматизации и не доказывает корректность конкретного change.
  • GitHub Docs: managing environments for deployment, immutable commit 6a92295 (31 January 2025) — GitHub описывает, что job, ссылающийся на environment, не стартует и не получает environment secrets до прохождения protection rules; required reviewer может пропустить job, а bypass зависит от отдельной настройки environment. Граница: Это поведение GitHub Actions environments в описанной версии. Оно не подтверждает семантическую верность diff, полноту preview, независимость reviewer или безопасность другого CI и production процесса.
  • Kubernetes kubectl apply reference, immutable website commit c889d9b (14 March 2024) — Справочник kubectl apply различает dry-run=client, который печатает object без отправки, и dry-run=server, который отправляет server-side request без persistence ресурса. Граница: Этот справочник не обещает, что последующий реальный apply увидит то же состояние, пройдёт те же policy или не затронет другие объекты. Он описывает только режим dry-run конкретной команды.