Интеграция защищённого контура в Yandex Cloud: делимся опытом
Всем привет! Nixys на связи!
С нами (в основном, со мной) произошёл очередной интересный кейс.
Нашему клиенту (условимся называть его Заказчик) была необходима готовая среда разработки «под ключ». Лёгкая в администрировании, масштабируемая, с минимумом ответственности и с максимумом отказоустойчивости. В то же время она должна быть защищена от несанкционированного доступа, чтобы наработки компании не попали в чужие руки. На тот момент у клиента было несколько железных серверов, на обслуживание которых уходило много человеческих ресурсов. Если не поддерживать и не развивать такую среду, то со временем можно столкнуться с ощутимыми проблемами.Таким образом, планировалось создать продакшн-контур в отечественном облаке.
Минусы, которые мы видим:
По окончании настройки весь доступ к внешнему миру (и интернету, конечно) будет отключён, что сразу убьёт лёгкость администрирования, ведь все обновления будут идти через согласование с ИБ.
Закрытые контуры требуют больших инвестиций в разработку и дальнейшую поддержку, так как все компоненты должны быть реализованы внутри компании.
Компания может столкнуться с проблемами при интеграции новых технологий или продуктов от других поставщиков. Это может привести к проблемам при масштабировании и расширении бизнеса.
Задача звучала тривиально: есть максимально закрытая и безопасная инфраструктура c UserGate. Заказчик хочет, чтобы программисты создавали классные сервисы в Yandex Cloud и все бы были счастливы.
Из вводных (насколько мне известно) было:
Через множество брейнштормов и созвонов мы пришли к такому решению:
Создаём ipsec-тунель между Yandex Cloud и UserGate по протоколу IKEv2.
Где можем, авторизуемся через LDAP и Keycloak (вот, кстати, статья от моего коллеги, который уже прошёл эту битву).
Используя Terraform-модули из репо nxs-marketplace-terraform, по щелчку пальцев разворачиваем инфраструктуру, жмём руки и на доброй ноте расходимся.
Сама инфраструктура будет состоять из:
Настраиваем StrongSwan site-to-site IKEv2 ipsec tunnel
Для начала разберёмся, что такое IPSEC-туннель. Это виртуальный канал связи, который создается между двумя устройствами для безопасной передачи данных. Этот туннель шифрует данные перед их отправкой и дешифрует их после получения. Таким образом, даже если данные будут перехвачены в процессе передачи, злоумышленник не сможет их прочитать или изменить. Если сравнивать, например, с OpenVPN, который работает на уровне пользователя, то IPSEC, работающий на уровне ядра системы, позволяет повысить скорость и производительность.
Мы выбрали протокол IKEv2 (т. к. IKEv1, созданный в далёком 1990 году, просто морально устарел), а в качестве метода шифрования — PSK (Pre Shared Key — когда один и тот же ключ шифрования используется всеми участниками соединения). На самом деле перед нами стоял выбор: использовать PSK или сертификаты. Решили остановиться на PSK по нескольким причинам. Во-первых, это удобно: в случае компрометации быстрее и проще сгенерировать и передать новый ключ, нежели бодаться с сертификатами. Иногда время, потраченное на восстановление канала связи, может стоить очень дорого. Во-вторых, контур будет закрытый, знать про него будут только определённые люди.
Перейдём к делу.
Сперва было необходимо построить туннель между Yandex Cloud и закрытым контуром Заказчика. Всё просто. Что же могло пойти не так?
Да как обычно — баг в последней (на момент мая 2024 года) версии прошивки в UserGate. При создании IPSEC-туннеля UserGate создаёт множественные соединения, которые разрываются по тайм-ауту. Баг подтвержден разработчиком, версия ПО отправлена на доработку. А процесс, увы, уже был запущен. Пришлось переделывать. Так что будьте осторожны.
Дальше сделали связку StrongSwan (виртуалка в Яндекс клауд) — StrongSwan (виртуалка за UserGate в инфраструктуре Заказчика).
Сейчас покажу, как мы это сделали.
Устанавливаем StrongSwan.
Конфигурим /etc/ipsec.conf:
config setup
charondebug="all"
uniqueids=yes
strictcrlpolicy=no
conn connection
authby=secret
left=10.12.0.21
leftid=84.0.0.1
leftsubnet=10.12.0.0/24,10.13.0.0/24
right=202.0.0.3
rightid=%any
rightsubnet=10.1.1.0/24,10.1.4.0/24
ike=aes256-sha256-modp2048,aes256-sha256-modp1024,aes256-sha1-modp2048,aes256-sha1-modp1024!
esp=aes256-sha256,aes256-sha1!
keyingtries=3
ikelifetime=1h
lifetime=8h
dpdaction=hold
auto=start
type=tunnel
keyexchange=ikev2
Если коротко, то left — это локальный (local) сервер, а right — удалённый (remote).
Всё, что связано с шифрованием (ike, esp) на обоих хостах должно быть одинаково. Подробности можно найти в официальной документации.
Настраиваем /etc/ipsec.secrets:
#source destination
84.0.0.1 202.0.0.3 : PSK "тут какой-то ключ в base64"
Тут важна только строчка с адресами и ключом, source/destination указаны для того, чтобы не запутаться.
Настраиваем сеть как описано в этой статье.
Добавляем правила в /etc/ufw/before.rules:
# разрешаем ssh
-A ufw-before-input -p tcp -m tcp --dport 22 -j ACCEPT
-A ufw-before-output -p tcp -m tcp --sport 22 -j ACCEPT
Включаем перенаправление трафика ESP:
-A ufw-before-forward --match policy --pol ipsec --dir in --proto esp -s 10.12.0.0/24 -j ACCEPT
-A ufw-before-forward --match policy --pol ipsec --dir out --proto esp -d 10.12.0.0/24 -j ACCEPT
# разрешаем forwarding
-A ufw-before-forward -s 10.1.1.0/24 -d 10.12.0.0/24 -i eth0 -m policy --dir in --pol ipsec --reqid 1 --proto esp -j ACCEPT
-A ufw-before-forward -s 10.12.0.0/24 -d 10.1.1.0/24 -o eth0 -m policy --dir out --pol ipsec --reqid 1 --proto esp -j ACCEPT
-A ufw-before-forward -s 10.1.4.0/24 -d 10.12.0.0/24 -i eth0 -m policy --dir in --pol ipsec --reqid 1 --proto esp -j ACCEPT
-A ufw-before-forward -s 10.12.0.0/24 -d 10.1.4.0/24 -o eth0 -m policy --dir out --pol ipsec --reqid 1 --proto esp -j ACCEPT
Такие правила необходимо создать для каждой подсети, но важно указать корректный интерфейс и адреса.
Дополнительная информация с примерами есть на официальном сайте.
Создаём статические маршруты до сетей назначения.
После делаем аналогичные операции на втором сервере, только меняем местами адреса сетей и хостов.
Настраиваем инфраструктуру
После того как мы успешно протестировали передачу файлов размером в несколько гигабайт между удалёнными хостами через туннель и измерили скорость передачи, мы приступили ко второму этапу — настройке инфраструктуры. Это был самый сложный этап работы.
Порядок настройки, который я рекомендую, не является обязательным. Однако, если бы я настраивал всё именно в таком порядке с самого начала, проект был бы завершён гораздо быстрее. Большинство решений были приняты в первую очередь для того, чтобы упростить жизнь администраторам, которые будут всё это поддерживать.
VPN. «Зачем? Ведь уже есть ipsec… тоннель», — скажут самые внимательные читатели. А тут всё просто. Нам и каким-то внешним подрядчикам так будет быстрее ходить (мне, конечно, сказали, что решение временное…. Но мы то с вами знаем, что нет ничего более долговечного чем временное;-). Выбор пал на Pritunl VPN. Плюсы очевидны: простота первичной настройки, начало работы сразу после установки, простое добавление сущностей, крайне легкий способ создания новых пользователей.
Сервер GitLab в Docker Compose файле. Почему? Удобство сопровождения! А именно:
меньше проблем с обновлениями;
всё находится в одном месте, что снижает порог вхождения для следующих инженеров, которые будут работать над проектом;
всё можно разместить за внешним Nginx, но это скорее опционально (контур-то у нас закрытый).
После того, как Gitlab будет развернут и настроен, каждый следующий шаг описываем с помощью IaC. Мы настраиваем CI/CD для всего, что можем, чтобы клиент одним нажатием кнопки мог создавать волшебство или вершить историю (нужное подчеркнуть). Делаем это всё максимально читабельным и удобным. И, конечно же, в каждый проект размещаем Readme с подробным описанием сервиса.
Немного про настройку GitLab
Ничего особенного не было придумано, docker-compose.yaml был взят из официальной документации. Вот как он выглядит:
version: '3'
services:
gitlab:
image: gitlab/gitlab-ce:<тут какая-то версия образа>
logging:
options:
max-size: "1024m"
restart: always
hostname: 'gitlab.example.com'
container_name: 'gitlab'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://gitlab.example.com'
nginx['redirect_http_to_https'] = false
nginx['custom_gitlab_server_config'] = 'proxy_request_buffering off;'
letsencrypt['enable'] = false
prometheus['enable'] = false
alertmanager['enable'] = false
grafana['enable'] = false
registry['enable'] = false
gitlab_rails['gitlab_shell_ssh_port'] = 2222
ports:
- '8080:80'
- '8443:443'
- '2222:22'
volumes:
- './volumes/etc/gitlab:/etc/gitlab'
- './volumes/log/gitlab:/var/log/gitlab'
- './volumes/data/gitlab:/var/opt/gitlab'
Следующий пункт — настройка Nginx. Подготовка почвы для сертификатов — на базе статей из мной горячо любимого Digital Ocean. Выделю основные моменты, которые я использовал:
создаём файл
/etc/nginx/snippets/self-signed.conf
в котором указываем расположение сертификатов (которые мы выпускаем с помощью Vault):
ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
Затем описываем конфигурацию SSL в
/etc/nginx/snippets/ssl-params.conf
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 77.88.8.8 77.88.8.1 valid=300s;
resolver_timeout 5s;
preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
И создаём DHparam-файл с помощью команды: sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096
Для чего это нужно? Этот протокол, позволяет двум и более сторонам получить общий секретный ключ через незащищённый от прослушивания канал связи. Полученный ключ используется для шифрования дальнейшего обмена с помощью алгоритмов симметричного шифрования. Он не защищён от уязвимости «Man-in-the-middle» и используется преимущественно как дополнительная мера защиты. Момент: генерация ключа может занимать рандомное количество времени, от нескольких секунд до десятков минут, так что если у вас подзавис процесс генерации, налейте горячего напитка, откиньтесь в кресле и посмотрите видео с котиками или почитайте что-нибудь полезное.
Далее сохраняем конфигурацию NGINX с указанными includ«ами:
server {
listen 80;
server_name gitlab.example.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
include snippets/self-signed.conf;
include snippets/ssl-params.conf;
server_name gitlab.example.com;
client_max_body_size 3000m;
access_log /var/log/nginx/git.access.log;
error_log /var/log/nginx/git.error.log;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;
add_header X-Frame-Options "SAMEORIGIN";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Затем выполняем следующие команды: sudo nginx -s reload
Скорее всего, вы увидите что-то вроде:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Это значит, что все окей. Теперь — самое время запустить nginx и проверить, что ранее запущенный GitLab предлагает нам авторизоваться.
Кластер всеми горячо любимого Kubernetes (далее k8s). Решение — Managed от Yandex Cloud.
Во-первых: бюджеты позволяют;
во-вторых: снимаем с себя и с коллег из будущего бОльшую часть ответственности за здоровье кластера;
в-третьих: наша команда запилила умопомрачительный Terraform-модуль, с помощью которого кластер разворачивается быстро. Все необходимые опции описаны, да и модуль поддерживается и развивается.Кластер Vault. Бэкендом будет выступать Consul. Почему же именно Vault? Так всем нашим сервисам необходимы сертификаты! Соответственно, следующим шагом будет деплой и настройка Cert Manager в связке с Vault.
Cert Manager. Запускаем в кластере Kubernetes. Для того, чтобы быстро и просто получать сертификаты из Vault, настраиваем Cluster issuer. Для наших целей манифест будет выглядеть вот так:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: vault-issuer
spec:
vault:
auth:
kubernetes:
mountPath: /v1/auth/kubernetes
role: clusterissuer
secretRef:
key: token
name: issuer-token-lmzpj
path: pki/sign/example
server: http://vault.example.com
Это немного отличается от того, что написано в инструкции и, соответственно, нам нужны небольшие корректировки.
имя роли указываете то, которое вам необходимо. Как видите, у меня не самая богатая фантазия, поэтому «clusterissuer»
path у меня «pki/sign/example» (это расположение ваших сертификатов в Vault).
Ну и для того, чтобы наши Ingress стали защищёнными, добавляем в манифесты аннотацию с именем cluster issuer, пример ниже:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-name
namespace: example
labels:
app: example
annotations:
cert-manager.io/cluster-issuer: vault-issuer
meta.helm.sh/release-name: example-name
meta.helm.sh/release-namespace: example
spec:
ingressClassName: nginx
rules:
- host: example.example.com
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: example
port:
number: 8080
После завершения истории с сертификатами, делаем все сервисы безопасными. Добавляем во все ингрессы аннотацию:
annotations:
cert-manager.io/cluster-issuer: vault-issuer
Keycloak. Как я писал выше, мы планировали всё сделать на базе связки Keycloak + LDAP, но наши планы изменились: как оказалось, вместо LDAP уже настроили ADFS. Без проблем, подумали мы и начали работу с тем, что уже есть.
Нам нужна была единая точка авторизации в AD через Keycloak ⬌ ADFS.
В итоге стали настраивать SSO на базе SAML 2.0.
SSO. Упрощаем людям жизнь: один раз авторизовался и весь день свободен. Пока жива сессия, вы можете заходить в любые сервисы, которые добавлены в Keycloak. Как это работает: пользователи заводят в Active Directory, создают имя пользователя и пароль и всё, с помощью этих учётных данных пользователь будет авторизован в системе на время жизни сессии.
SAML. Если грубо, то это стандартный язык разметки, который используется для обмена информацией о безопасности между различными системами. Он позволяет передавать аутентификационные данные (например, имя пользователя и пароль) между сервисами без необходимости повторного ввода этих данных.
Не трогай — убьёт
Приглашаю в комментарии на холивар между сторонниками SAML и OIDC. Как говорится, в споре рождается истина :)
После настройки Keycloak мы настраиваем авторизацию всем сервисам, которые были развёрнуты ранее:
GitLab
Vault
Grafana
Kibana
Всё, что касается Keycloak, расписывать не будем, ибо статья превратится в галерею. Со стороны Keycloak настраивается identity provider согласно официальной инструкции. А вот и она.
После настраиваются Клиенты, каждый под свой сервис, кроме Kibana.Для Kibana используем oauth2-proxy. Инфомацию по его настройке и конфигурации можно найти в этой статье.
Остальное настраиваем через SAML согласно официальной документации.
Вуаля! Клиент получил полностью закрытый контур для разработки. Я получил крутой опыт. Вы получили статью. В итоге все счастливы.
Спасибо, что прочитали! Надеюсь, вам было полезно.
Кстати, совсем скоро у нас пройдёт классный вебинар, на котором наша команда расскажет про тонкости и нюансы миграции в облако. Чтобы узнать больше — кликните сюда.