Один лабораторный сервер, три роли доверия, один домен отказа
Точке входа VPN, частным сервисам и задачам CI нужны разные права даже на одном сервере. Разделите роли, изолируйте сети и сохраните восстановление через консоль.
Всё работает, пока сборщику не понадобится тот же сокет хоста, которым пользуется гость VPN, и тогда никто не может объяснить, почему задача может читать ключи шлюза. Всё поставили на одну машину, потому что так помещалось, а каждому гостю дали широкий доступ, потому что это был самый быстрый способ заставить всё работать. В результате хост, у которого граница доверия — кабель питания. Разделение ролей требует меньше работы, чем кажется, и начинается с их именования.
Три роли, которые сейчас делят всё
Точка входа VPN, набор частных сервисов и сборщик хотят разного. Первому нужен публичный порт и узкая сеть на частной стороне. Второму нужны постоянное хранилище и секреты приложения. Третий выполняет код, который написал кто-то другой.
Именно одинаковый доступ для всех трёх делает симптом запутанным, ведь тогда любой из них может повлиять на остальные. Задача сборки, которая может добраться до сокета гипервизора, — это задача, которая может остановить VPN. Гость с сервисами, у которого есть маршрут в сеть управления, может изменить конфигурацию коммутатора. Проблема в правах, а список процессов — лишь место, где это проявляется. Если сначала назвать роли, у каждого права появляется своё место.
Схема ниже — отправная точка для такого разделения, а не обещание высокой доступности. Считайте её гипотезой для проверки, а не готовым проектом.
Разделяйте три роли, а не три процесса
Три гостя на одном хосте дают три набора прав и три сетевых положения. Они не дают три домена отказа, и проект должен говорить это прямо.
Разделение помогает только тогда, когда компрометация одного гостя не даёт доступа остальным, а это настройка гипервизора, а не соглашение об именах. Назвать виртуальную машину шлюзом не мешает другому гостю добраться до хоста. Читайте конфигурацию виртуального коммутатора, а не имя хоста, и затем подтвердите, что каждый гость находится там, где вы думаете.
host
├─ vpn-gw 10.60.10.2/24 public 51820/udp only
├─ services 10.60.0.10/24 no route to the management network
└─ ci-runner 10.60.20.2/24 outbound only, no host socket
Запишите топологию, а затем проверьте её инструментами, которые ею владеют. Гость, оказавшийся не на том мосту, — это находка, а не подробность. Исправьте сеть, прежде чем менять конфигурацию любого сервиса.
Пусть гость VPN открывает только свои двери
Опубликуйте порт туннеля на маршрутизаторе и больше ничего. Порты администрирования гостей относятся к пути управления, а не к интернету, и интерфейс самого гипервизора тоже там.
Сеть на частной стороне должна доходить до сервисов, которые нужны удалённым пользователям, и на этом останавливаться. Аутентификация приложения остаётся включённой, потому что туннель — это транспорт, а не вход. Держите таблицу маршрутов гостя VPN настолько маленькой, чтобы описать её одним предложением. Если вы не можете описать её так просто, граница уже размылась.
virsh net-list --all
virsh net-dumpxml lab-private
sudo nft list ruleset | grep -n -E '51820|10.60'
Всё, что слушает на хосте и не нужно для управления, не должно слушать вообще. Это касается API гипервизора, который легко оставить открытым на публичном интерфейсе.
Задачам сборки нужна другая граница
Задача CI выполняет команды, которыми управляет репозиторий, поэтому её доверие зависит от того, кто может отправлять код. Оболочковый исполнитель запускает эти команды от пользователя раннера на хосте, а это самая широкая доступная граница.
Контейнерные исполнители уже, и привилегированный контейнер не из узких. Держите раннер в отдельном госте с собственной сетевой областью и дайте ему только доступ к артефактам и развёртыванию, который нужен задачам. Одноразовые работники уменьшают сохраняемое состояние; они не уменьшают достижимость и подверженность секретам. И то и другое всё ещё требует явных ограничений.
Гость с сервисами владеет данными, поэтому именно его план резервного копирования должен работать. Держите репозитории, загрузки и базу данных в одном расписании. Проверьте восстановление, а не читайте статус задания.
Планируйте отказ, который делят все трое
Один хост — это один домен отказа. Перезагрузка убирает туннель и все частные сервисы в один момент, и именно тогда путь внутрь важнее всего.
Сохраняйте доступ к консоли, не зависящий от гостя VPN, будь то физическая консоль, интерфейс управления вне полосы или консоль провайдера. Снимок, хранящийся на том же диске, не является независимой копией, как и резервная копия, которая выполняется внутри защищаемого гостя. Затем проверьте холодный запуск как последовательность, а не как список: хост, виртуальные сети, путь туннеля и одно названное приложение под обычной учётной записью.
virsh list --all
systemctl is-active libvirtd
curl -sS -m 5 -o /dev/null -w '%{http_code}\n' https://10.60.0.10/
ss -ltnp | grep -E ':51820|:443'
Порядок запуска процессов ничего не доказывает о маршрутах и DNS, поэтому проверяйте путь, а не состояния юнитов. Путь — это то, что удалённому пользователю действительно нужно.