Главная/Справочники/Справочник

Security checklist

Проверки для образа, запуска, Compose и конвейера. У каждой — команда и уровень критичности.

Container — граница между вашими сервисами, а не между вами и злоумышленником. Ни один пункт этого списка не делает его пригодным для недоверенного кода (урок 19.2).

Уровни

УровеньЗначение
БлокирующийРабота не принимается; исправить до выкатки
ВысокийИсправить в ближайшую итерацию
СреднийЗапланировать
НизкийПо возможности

Образ

ПроверкаКомандаУровень
Секретов нет в слояхdocker history --no-trunc ОБРАЗ | grep -iE 'password|secret|token|api_key'Блокирующий
Секретов нет в переменныхdocker image inspect ОБРАЗ --format '{{json .Config.Env}}'Блокирующий
Пользователь заданdocker image inspect ОБРАЗ --format '{{.Config.User}}'Блокирующий
Пользователь числовойзначение соответствует ^[1-9][0-9]*(:[0-9]+)?$Высокий
Базовый образ закреплёнgrep '^FROM' Dockerfile — тег или digest, не latestВысокий
Инструментов сборки нетdocker run --rm ОБРАЗ which gcc make — ничегоВысокий
Оболочки нет либо она оправданаdocker run --rm --entrypoint sh ОБРАЗ -c 'exit 0'Средний
Пакетный менеджер не оставил кэшdocker run --rm ОБРАЗ ls /var/lib/apt/lists — пустоСредний
.dockerignore исключает .git и секретыcat .dockerignoreВысокий
Образ просканированtrivy image --severity HIGH,CRITICAL ОБРАЗВысокий
Результат сканирования прочитанОтчёт с решением по каждой находкеСредний
SBOM созданsyft ОБРАЗ -o spdx-jsonСредний

Про первую строку. Отсутствие находок в docker history не доказывает отсутствия секрета: он мог попасть в слой файлом, а не аргументом инструкции. Надёжная проверка:

bash
docker save ОБРАЗ -o /tmp/образ.tar
mkdir -p /tmp/распакованное && tar -xf /tmp/образ.tar -C /tmp/распакованное
grep -rl -E 'BEGIN (RSA|OPENSSH|EC) PRIVATE KEY' /tmp/распакованное/ || echo "ключей не найдено"
rm -rf /tmp/образ.tar /tmp/распакованное

Про числовой пользователь. USER app защищает не хуже USER 10001, но Kubernetes с runAsNonRoot: true такой pod не запустит: имя снаружи не разрешается (урок 18.2).


Запуск

ПроверкаКомандаУровень
Нет --privilegeddocker inspect ИМЯ --format '{{.HostConfig.Privileged}}'falseБлокирующий
Сокет Docker не смонтированdocker inspect ИМЯ --format '{{json .Mounts}}' | grep docker.sockБлокирующий
Корень хоста не смонтировантот же вывод, поиск "Source":"/"Блокирующий
Процесс не от rootdocker exec ИМЯ id -u — не 0Блокирующий
Capabilities снятыdocker inspect ИМЯ --format '{{json .HostConfig.CapDrop}}'Высокий
Запрет повышения привилегийdocker inspect ИМЯ --format '{{json .HostConfig.SecurityOpt}}'Высокий
Корень только для чтенияdocker inspect ИМЯ --format '{{.HostConfig.ReadonlyRootfs}}'Высокий
seccomp не отключёнSecurityOpt не содержит seccomp=unconfinedВысокий
Ограничения ресурсов заданыdocker inspect ИМЯ --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}'Высокий
PID namespace свойdocker inspect ИМЯ --format '{{.HostConfig.PidMode}}' — пустоВысокий
Сеть не hostdocker inspect ИМЯ --format '{{.HostConfig.NetworkMode}}'Средний
Порты публикуются осознанноdocker port ИМЯСредний

Три конфигурации, эквивалентные root на хосте:

ЧтоПочему
--privilegedСнимает seccomp, AppArmor, возвращает все capabilities
Монтирование /var/run/docker.sockПозволяет создать container с любыми параметрами
cap_add: [SYS_ADMIN]Монтирование и значительная часть операций root

Вторая опаснее прочих тем, что выглядит безобидно — «просто файл».

Проверка одной командой

bash
docker inspect ИМЯ --format '
privileged: {{.HostConfig.Privileged}}
readonly:   {{.HostConfig.ReadonlyRootfs}}
pid:        {{.HostConfig.PidMode}}
network:    {{.HostConfig.NetworkMode}}
capdrop:    {{json .HostConfig.CapDrop}}
capadd:     {{json .HostConfig.CapAdd}}
secopt:     {{json .HostConfig.SecurityOpt}}
memory:     {{.HostConfig.Memory}}'

Compose

ПроверкаКомандаУровень
Паролей нет в открытом видеgrep -riE 'password\s*[:=]\s*[^$/]' compose*.yamlБлокирующий
Файлы секретов в .gitignoregit check-ignore secrets/*Блокирующий
Секреты передаются файлом_FILE-переменные, а не значенияВысокий
Образы по конкретным тегамgrep 'image:' compose.yaml | grep -v latestВысокий
Внутренняя сеть для данныхdocker network inspect ПРОЕКТ_backend --format '{{.Internal}}'Высокий
База и кэш не публикуют портыdocker compose ps --format '{{.Service}} {{.Ports}}'Высокий
Ограничения у всех сервисовсм. команду нижеВысокий
Ротация логов настроенаlogging.options.max-size у всехСредний
Публикация только на 127.0.0.1 в разработкеgrep 'ports:' -A 2 compose.dev.yamlСредний
bash
docker compose config | python3 -c '
import sys, yaml
d = yaml.safe_load(sys.stdin)
for name, svc in d["services"].items():
    limits = (svc.get("deploy") or {}).get("resources", {}).get("limits", {})
    log = (svc.get("logging") or {}).get("options", {})
    print(f"{name:<12} лимиты={limits or \"НЕТ\"} ротация={log.get(\"max-size\", \"НЕТ\")}")'

Конвейер

ПроверкаУровень
Секреты не печатаются в журнал сборкиБлокирующий
Отсутствие сканера отличимо от чистого результатаВысокий
Публикация только при полностью пройденном конвейереВысокий
Тег неизменяемый, по SHA коммитаВысокий
Развёртывание по digestВысокий
Базовые образы обновляются регулярноВысокий
Секреты сборки через --secret, не ARGБлокирующий
Права токена минимальныСредний
Образ подписанНизкий

Про маскирование секретов. Механизм маскирования в CI подменяет точное совпадение строки. Он не сработает, если значение преобразовано:

ПреобразованиеМаскирование
Вывод как естьРаботает
Base64Не работает
Часть строкиНе работает
Обратный порядокНе работает
Другой регистрНе работает

Отсюда правило: полагаться на маскирование нельзя, секрет не должен попадать в вывод вообще (урок 16.2).


Организационные проверки

Инструментами не проверяются. Ответы записываются словами.

Вопрос
Кто, кроме автора, может собрать этот образ?
Когда последний раз обновлялся базовый образ?
Кто читает отчёты сканирования и что делает с находками?
Что произойдёт при утечке образа наружу?
Какие секреты пришлось бы отозвать?
Кто имеет права на выгрузку в registry?
Есть ли процедура отзыва скомпрометированного ключа?

Ноль находок инструментами не отвечает ни на один из этих вопросов. Они наносят наибольший ущерб и не обнаруживаются никаким линтером (урок 19.4).


Граница применимости

Полезно назвать прямо, чтобы список не читался шире, чем он есть.

От чего защищаетОт чего нет
Ошибки приложения, влияющие на хостУязвимости ядра
Влияние сервисов друг на другаНедоверенный код
Случайное раскрытие секретовВредоносную зависимость в requirements.txt
Исчерпание ресурсов одним сервисомПобочные каналы

Для недоверенного кода — user namespace, gVisor, Kata, микро-ВМ или отдельная машина. Выбор зависит от того, кто именно недоверенный (урок 19.2).


Навигация

Вернуться к справочникам
Production checklist
Каталог антипаттернов
Раздел 12. Security
Главное оглавление

Markdown на GitHub ↗