01
Сервис поднялся не в том режимеОтсутствие — не опечатка
Что видно
Прод работает как дев, и ни в логах, ни в проверках здоровья ничего не выглядит неправильным.
Поле конфигурации, объявленное как набор допустимых значений, ловит опечатку. Ловит пустую строку. Отсутствие — не ловит: у отсутствия берётся умолчание, а умолчанием обычно стоит послабляющий режим.
Опасен не сам режим, а то, что за ним висит. Система, тихо включившая свои отладочные удобства, неотличима от правильно стартовавшей — нет момента, в который что-то выглядит не так.
Чем доказывается
У имени окружения нет умолчания, и без него сервис не поднимается. Тест проверяет отказ, а не разбор значения: разбор никогда и не был тем местом, которое ломалось.
02
Смена настройки сделала нечитаемым то, что уже в базеСхему выбирает шифротекст, а не конфигурация
Что видно
После правки конфигурации записанное раньше перестаёт расшифровываться, и ошибка говорит про неверный ключ.
Если схему расшифровки выбирает конфигурация, то её смена задним числом переосмысляет данные, записанные по старым правилам.
Дорого здесь не это, а диагноз. Тот, кто разбирается, идёт искать проблему с ключом, которой нет: данные целы, их просто читают не тем способом. Каждая минута этого поиска тратится не в том репозитории.
Чем доказывается
Схему называет префикс самого шифротекста, отсутствие префикса означает наследие. Конфигурация не может переосмыслить историю, которую писала не она.
03
Риск описан и остался открытымНазвать — не значит закрыть
Что видно
Дефект задокументирован, все о нём знают, и он на месте.
Тест может нести риск в своём имени и ничего про него не утверждать. Комментарий может назвать дыру точно и оставить её ровно там, где она была.
Это отказ команд, которые хорошо документируют. Описанная дыра ощущается закрытой не так, как неописанная, и получает странный иммунитет: её все читали, поэтому никто ей не удивляется, поэтому никто не тревожится. Она стоит на виду неделями, и никто ничего не прячет.
Чем доказывается
У открытого риска есть утверждение, падающее до его закрытия, или строка в списке, которую документ отказывается опустить. Пустая строка читается как «покрыто» — единственное, что здесь никогда нельзя было предполагать.
04
Механизм написан и ни разу не выполнялсяНаписано, право выдано, вызвать некому
Что видно
Функция есть, права на неё выданы отдельной строкой политики — и она не вызывается ниоткуда.
Мёртвый код обычно безвреден. Этот — нет: все, кто ниже по течению, уже записали операцию, к которой он относится, как подтверждённую. Существование механизма делает ту работу, которую должно было делать его выполнение.
Отдельная строка в политике доступа — не случайность. Она означает, что как минимум двое про этот механизм думали, и всё равно ничего не выполнялось.
Чем доказывается
Проверка, что у каждого публичного механизма есть вызывающий, поставленная в CI: иначе новый такой же добавится незамеченным.
05
Зелёная проверка, которая иначе и не моглаПризнак — не диагноз
Что видно
Проверка зелёная, и была бы зелёной в любом случае.
Скан исходников по именам переменных находит те имена, которые знает. Он не находит значение, приехавшее под другим именем, чужое тело ответа, залогированное целиком одним вызовом, и переменную, которую на прошлой неделе переименовали.
Его зелёный означает «этим способом не нашлось». Это заметно меньшее утверждение, чем то, которое в него прочитают, и в зазоре между ними правило тихо перестаёт соблюдаться, продолжая отчитываться.
Чем доказывается
Ограничение публикуется рядом с проверкой, в той же таблице, что и утверждение, которое она подпирает. Признак, выданный за диагноз, хуже отсутствия проверки: он закрывает вопрос, который должен был держать открытым.
06
Контроль перестал применяться и промолчалТихий отказ — дефект безопасности, а не удобства
Что видно
Мера больше не действует, и никто об этом не сообщил.
Обязательный вход, у которого при отсутствии берётся умолчание. Исключение, проглоченное на пути, который выполняется только в проде. Фильтр, тихо пропускающий, когда его шаблон не совпал.
Каждое — контроль, переставший работать и продолжающий выглядеть на месте. Каждое обнаружится ровно тем событием, ради предотвращения которого ставилось. Тишина здесь не нейтральна: она и есть отчёт о том, что всё в порядке.
Чем доказывается
Отсутствие обязательного входа — отказ, а не умолчание. И у отказа есть собственный тест: отказ, который никто ни разу не вызвал, — это отказ, работу которого никто не видел.
07
Здоров, готов и не может делать свою работуГотовность проверяла зависимость, а не способность
Что видно
Обе пробы зелёные, каждый вход — пятисотка.
Проба готовности, спрашивающая, отвечает ли хранилище ключей, проверяет не то. Между «хранилище отвечает» и «мы можем подписать» помещаются запечатанное хранилище — поднятое и доступное, но ещё не получившее своих ключей после перезапуска, — протухшая ролевая учётка и значение, зашифрованное схемой, которой больше нет. Все три оставляют зависимость прекрасно доступной, а способность — отсутствующей.
Чем доказывается
Готовность выполняет саму способность и отказывает, когда не может. И парное правило, которое важнее: то же запечатанное состояние обязано ломать выпуск и обязано не ломать проверку — иначе штатная пауза превращается в остановку всего, что ниже.
08
Запрос сам выбрал, сколько будет стоитьЦену назначает отправитель, если её не назначили вы
Что видно
Один запрос останавливает весь процесс, включая ручку здоровья.
Тело запроса без потолка доезжает до разборщика, а стоимость разбора выбирает тот, кто прислал байты.
Измерено на своём коде: 640 КБ структурированного двоичного формата стоили 29,5 секунды процессорного времени внутри однопоточного событийного цикла. Всё остальное ждало — включая проверку здоровья, и поэтому проблема разбора предъявляется как недоступность сервиса.
Чем доказывается
Потолок на размер тела, выбранный как решение о стоимости, а не выставленный как настройка. И точность в том, чем он не является: это не ограничение частоты, и он не означает, что байты не долетели.
09
Сервис ответил на вопрос, который не егоОтвечать фактом, а не решением
Что видно
Один сервис решает, что можно другому.
«Сессия есть» и «можно» — разные утверждения, и слой идентичности вправе произносить только первое.
Когда он произносит второе, его представление о достаточности оказывается вшито в каждого вызывающего. В день, когда одному из них понадобится порог строже — перевод крупнее, новая юрисдикция, клиент, который сам попросил, — правку придётся делать в чужой кодовой базе, руками команды, которая этот риск не несёт и оценить его не может.
Чем доказывается
Отдавать факты: чем именно подтверждена личность, когда, с какой стойкостью. Решать — вызывающему. Выглядит менее услужливо. Именно это держит решение рядом с деньгами.
10
Сильный фактор снимался слабымКонструкция прочна настолько, насколько дёшев самый дешёвый способ её разобрать
Что видно
Аккаунт защищён сильным вторым фактором, и снять этот фактор можно предъявлением слабого.
Добавление сильного второго фактора поднимает планку ровно до того места, где путь восстановления её опускает.
Если сильный фактор снимается предъявлением более слабого, то аккаунт защищён слабым — что бы ни было написано на экране подключения. Арифметика здесь не сложение, а минимум по всем путям, которыми фактор можно снять. А пути восстановления обычно проектирует тот, кто оптимизирует нагрузку на поддержку.
Чем доказывается
Снятие фактора требует того же фактора, а не уровня стойкости, которому удовлетворяет и что-то более слабое. Разница — между тем, **каким способом** подтвердили, и тем, **насколько стойко** подтвердили. Два утверждения, которые сливаются в одно при первом удобном случае.
11
Ключ ротировали, и старый продолжал работатьРотация — это смена и вывод из обращения
Что видно
Ротация проведена, обе стороны считают её завершённой, старый ключ принимается.
Ротация — две операции, и теряется вторая.
Вывод старого ключа из обращения обязан произойти всюду, где читают проверки, а таких мест обычно больше одного: публикуемый набор ключей для внешних и прямой поиск для своих маршрутов. Отфильтруй в одном — второе продолжит принимать. Это хуже, чем не ротировать вовсе: теперь все уверены, что сделано, и за скомпрометированным материалом никто не смотрит.
Та же форма — слоем ниже. Хранилище отчитывается о ротации, и ротация действительно произошла; не произошло поднятие минимальной версии, принимаемой к расшифровке, и снятая версия продолжает разворачивать старые дампы. Обе стороны прочитали успех операции как наступление свойства.
Чем доказывается
Вывод из обращения живёт в хранилище, которое читают все проверки, а не внутри каждой. Команда возвращает не ноль, пока не сделаны обе половины: команда, вышедшая с нулём на середине, говорит «готово» ровно там, где сделано наполовину. И тест ротирует, а потом предъявляет старый ключ каждому пути, какой есть.
12
Журнал, который мог править тот, о ком он написанСвидетельство определяется правами, а не намерением
Что видно
Журнал полон, и роль, чьи действия он фиксирует, может его переписать.
Таблица становится свидетельством тогда, когда роль, чьи действия она записывает, не может её изменить. Комментарий в модели «журнал аудита, не править» — это намерение, а намерения не являются контролем.
Довод распространяется и на форму: журнал, схему которого я меняю в одиночку, не является журналом по той же причине, по которой им не является журнал, который я могу очистить.
Чем доказывается
Отдельная роль-владелец; у записываемой роли — вставка и чтение и больше ничего. И кто-то действительно пробовал от этой роли `UPDATE`, `DELETE`, `TRUNCATE` и `ALTER` и смотрел, как каждая отказывает, — потому что это свойство живёт в боевой базе и больше нигде. На тестовой роль другая и грантов нет вовсе, поэтому прогон показывает, что журнал пишется, и не может показать, что журнал сопротивляется. Половина, которую он покрывает, — не интересная.
13
Проба сказала обратное правдеНулевой код возврата — не доказательство
Что видно
Проверка прав прошла, права не выданы.
Выдача себе права, которого у тебя нет, проходит без ошибки и без эффекта.
Проба, читающая только код возврата, сообщает об успехе — и сообщает ровно там, где ошибиться дороже всего, потому что эта проверка и заведена, чтобы поймать именно это. Проба не сломана: она отвечает на вопрос о команде, а не о мире.
Чем доказывается
Проверять последствием, а не кодом возврата. Попробовать запрещённую операцию и требовать, чтобы она отказала.
14
Запись аудита удалилась вместе с тем, о ком написанаКаскад — удобство, стирающее свидетельство
Что видно
Пользователя удалили, и вместе с ним исчезла запись о том, что с ним делали.
Каскадный внешний ключ — очевидное умолчание, и он тихо делает свидетельство слабее того, о чём свидетельствует: строка, которую никому нельзя удалить напрямую, исчезает в тот момент, когда исчезает её субъект.
Этого никто не решает. Оно приезжает вместе со схемой, переживает ревью, потому что везде остальное так и делается, и обнаруживается в день, когда спрашивают, что делали с аккаунтом, которого больше нет.
Чем доказывается
Ссылка обнуляется, а не каскадит — и проверяется поведением на боевой базе, а не чтением схемы: схема и есть то, что выглядело правильным с самого начала.