Безопасность¶
Стенд открыт в интернет и содержит кнопки, которые ломают сервис. Поэтому защита здесь не декоративная.
Узел¶
Ansible приводит узел в известное состояние: файрвол ufw пропускает только 22,
80 и 443 порты, fail2ban блокирует перебор пароля по SSH, вход по паролю и вход
пользователем root отключены, обновления безопасности устанавливаются
автоматически.
Файл доступа к кластеру k3s создаётся с правами 0640 вместо стандартных
0644: читать его может только владелец и группа. Секреты в базе состояния
кластера шифруются (--secrets-encryption), иначе они лежали бы там открытым
текстом.
Транспорт¶
Всё внешнее общение идёт по TLS. Сертификаты выпускает cert-manager через Let's Encrypt по проверке HTTP-01; обновление автоматическое, за тридцать дней до истечения. Отдельное правило оповещения следит за тем, чтобы автоматика действительно сработала.
Traefik перенаправляет весь HTTP на HTTPS и добавляет заголовки:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: strict-origin-when-cross-origin
frameDeny: true
Ограничение частоты — 40 запросов в секунду со всплеском до 80 на один адрес. Стенд рассчитан на посетителей, а не на исследователей его пропускной способности.
Контейнеры¶
Одинаковые правила для всех подов, без исключений:
- образ собирается многоэтапно, в итоговый слой попадает только среда исполнения Java и собранное приложение — ни компилятора, ни исходников;
- процесс работает не под
root, а под непривилегированным пользователем; - файловая система корня только для чтения, а изменяемые каталоги подключены отдельными томами; исключение — база данных, которой нужен доступ на запись к своему каталогу;
- все дополнительные возможности ядра сброшены, повышение прав запрещено,
профиль
seccomp— стандартный для среды исполнения; - у каждого контейнера есть запрос и предел по памяти и процессору: под с утечкой уходит в перезапуск, а не забирает узел целиком.
Интерфейс раздаёт непривилегированный Nginx на порту 8080: чтобы слушать 80-й, контейнеру понадобилась бы дополнительная возможность ядра, а смысла в этом нет.
Секреты¶
Пароли Grafana и PostgreSQL генерирует Terraform (random_password) и кладёт в
секреты Kubernetes. В репозитории их нет и быть не может: файл состояния
Terraform, где они хранятся, лежит в секрете кластера, а не в репозитории, и
внесён в список исключений .gitignore вместе со всеми *.tfstate*.
Пароль администратора Grafana достаётся командой:
kubectl -n observability get secret grafana-admin \
-o jsonpath='{.data.admin-password}' | base64 -d
Публичный доступ и пульт¶
Дашборды открыты анонимно в роли наблюдателя: без этого стенд нельзя было бы показать по ссылке. Роль наблюдателя не даёт править дашборды и заводить источники данных, а перед Grafana стоит ограничение частоты запросов.
Пульт со сценариями отказов открыт всем, и это самая спорная часть стенда. Ограничения, которые делают её приемлемой:
- частота запусков ограничена двенадцатью в минуту на весь стенд;
- каждый сценарий выключается сам не позже чем через десять минут;
- утечка памяти ограничена сверху 512 МБ — под уйдёт в перезапуск по своему лимиту, не задев соседей;
- сценарии влияют только на пространство имён приложения; ни Prometheus, ни Grafana, ни база их не чувствуют;
- сценарии можно выключить целиком одним параметром конфигурации.
Самое плохое, что может сделать посетитель, — уронить поды приложения. Они поднимутся сами, и это будет видно на дашборде: собственно, ради этого стенд и сделан.
Чего здесь нет¶
Честный список того, что нужно в промышленной среде и чего на стенде нет:
- сетевые политики между пространствами имён — на одном узле с k3s они требуют замены сетевого слоя, а показать было бы нечего;
- внешнее хранилище секретов (Vault, внешний оператор секретов) — ради двух паролей это лишний сервис на 300 МБ памяти;
- подпись образов и проверка подписи при запуске — правильная практика, которая на одноузловом стенде без реестра сводится к ритуалу;
- резервное копирование базы — обращения жителей синтетические и пересоздаются за минуту.
Каждый пункт — не забытый, а взвешенный. Именно поэтому он записан.