Интеграция защищённого контура в Yandex Cloud: делимся опытом

sqflaw-wlq5pwykzzks5aaoqua4.png

Всем привет! Nixys на связи!

С нами (в основном, со мной) произошёл очередной интересный кейс.

Нашему клиенту (условимся называть его Заказчик) была необходима готовая среда разработки «под ключ». Лёгкая в администрировании, масштабируемая, с минимумом ответственности и с максимумом отказоустойчивости. В то же время она должна быть защищена от несанкционированного доступа, чтобы наработки компании не попали в чужие руки. На тот момент у клиента было несколько железных серверов, на обслуживание которых уходило много человеческих ресурсов. Если не поддерживать и не развивать такую среду, то со временем можно столкнуться с ощутимыми проблемами.Таким образом, планировалось создать продакшн-контур в отечественном облаке.

Минусы, которые мы видим:

  • По окончании настройки весь доступ к внешнему миру (и интернету, конечно) будет отключён, что сразу убьёт лёгкость администрирования, ведь все обновления будут идти через согласование с ИБ.

  • Закрытые контуры требуют больших инвестиций в разработку и дальнейшую поддержку, так как все компоненты должны быть реализованы внутри компании.

  • Компания может столкнуться с проблемами при интеграции новых технологий или продуктов от других поставщиков. Это может привести к проблемам при масштабировании и расширении бизнеса.

Задача звучала тривиально: есть максимально закрытая и безопасная инфраструктура c UserGate. Заказчик хочет, чтобы программисты создавали классные сервисы в Yandex Cloud и все бы были счастливы.

Из вводных (насколько мне известно) было:

Через множество брейнштормов и созвонов мы пришли к такому решению:

  1. Создаём ipsec-тунель между Yandex Cloud и UserGate по протоколу IKEv2.

  2. Где можем, авторизуемся через LDAP и Keycloak (вот, кстати, статья от моего коллеги, который уже прошёл эту битву).

  3. Используя Terraform-модули из репо nxs-marketplace-terraform, по щелчку пальцев разворачиваем инфраструктуру, жмём руки и на доброй ноте расходимся.

cpf10yug9kapdmq6wnlxkg_dvke.jpeg

Сама инфраструктура будет состоять из:

Настраиваем StrongSwan site-to-site IKEv2 ipsec tunnel

Для начала разберёмся, что такое IPSEC-туннель. Это виртуальный канал связи, который создается между двумя устройствами для безопасной передачи данных. Этот туннель шифрует данные перед их отправкой и дешифрует их после получения. Таким образом, даже если данные будут перехвачены в процессе передачи, злоумышленник не сможет их прочитать или изменить. Если сравнивать, например, с OpenVPN, который работает на уровне пользователя, то IPSEC, работающий на уровне ядра системы, позволяет повысить скорость и производительность.

Мы выбрали протокол IKEv2 (т. к. IKEv1, созданный в далёком 1990 году, просто морально устарел), а в качестве метода шифрования — PSK (Pre Shared Key — когда один и тот же ключ шифрования используется всеми участниками соединения). На самом деле перед нами стоял выбор: использовать PSK или сертификаты. Решили остановиться на PSK по нескольким причинам. Во-первых, это удобно: в случае компрометации быстрее и проще сгенерировать и передать новый ключ, нежели бодаться с сертификатами. Иногда время, потраченное на восстановление канала связи, может стоить очень дорого. Во-вторых, контур будет закрытый, знать про него будут только определённые люди.

Перейдём к делу.

Сперва было необходимо построить туннель между Yandex Cloud и закрытым контуром Заказчика. Всё просто. Что же могло пойти не так?

sevy3jwnpvp53cit30osp9nlc9y.png

Да как обычно — баг в последней (на момент мая 2024 года) версии прошивки в UserGate. При создании IPSEC-туннеля UserGate создаёт множественные соединения, которые разрываются по тайм-ауту. Баг подтвержден разработчиком, версия ПО отправлена на доработку. А процесс, увы, уже был запущен. Пришлось переделывать. Так что будьте осторожны.

Дальше сделали связку StrongSwan (виртуалка в Яндекс клауд) — StrongSwan (виртуалка за UserGate в инфраструктуре Заказчика).

Сейчас покажу, как мы это сделали.

  1. Устанавливаем StrongSwan.

  2. Конфигурим /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) на обоих хостах должно быть одинаково. Подробности можно найти в официальной документации.

  1. Настраиваем /etc/ipsec.secrets:

#source      destination
84.0.0.1     202.0.0.3  : PSK "тут какой-то ключ в base64" 

Тут важна только строчка с адресами и ключом, source/destination указаны для того, чтобы не запутаться.

  1. Настраиваем сеть как описано в этой статье.

  2. Добавляем правила в /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

Такие правила необходимо создать для каждой подсети, но важно указать корректный интерфейс и адреса.

Дополнительная информация с примерами есть на официальном сайте.

  1. Создаём статические маршруты до сетей назначения.

После делаем аналогичные операции на втором сервере, только меняем местами адреса сетей и хостов.

k0lqwdfuyjuhze9pso4a7zgqoya.png

Настраиваем инфраструктуру

После того как мы успешно протестировали передачу файлов размером в несколько гигабайт между удалёнными хостами через туннель и измерили скорость передачи, мы приступили ко второму этапу — настройке инфраструктуры. Это был самый сложный этап работы.

Порядок настройки, который я рекомендую, не является обязательным. Однако, если бы я настраивал всё именно в таком порядке с самого начала, проект был бы завершён гораздо быстрее. Большинство решений были приняты в первую очередь для того, чтобы упростить жизнь администраторам, которые будут всё это поддерживать.

  1. VPN. «Зачем? Ведь уже есть ipsec… тоннель», — скажут самые внимательные читатели. А тут всё просто. Нам и каким-то внешним подрядчикам так будет быстрее ходить (мне, конечно, сказали, что решение временное…. Но мы то с вами знаем, что нет ничего более долговечного чем временное;-). Выбор пал на Pritunl VPN. Плюсы очевидны: простота первичной настройки, начало работы сразу после установки, простое добавление сущностей, крайне легкий способ создания новых пользователей.

  2. Сервер 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. Выделю основные моменты, которые я использовал:

  1. создаём файл /etc/nginx/snippets/self-signed.confв котором указываем расположение сертификатов (которые мы выпускаем с помощью Vault):

ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
  1. Затем описываем конфигурацию 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 предлагает нам авторизоваться.

  1. Кластер всеми горячо любимого Kubernetes (далее k8s). Решение — Managed от Yandex Cloud.
    Во-первых: бюджеты позволяют;
    во-вторых: снимаем с себя и с коллег из будущего бОльшую часть ответственности за здоровье кластера;
    в-третьих: наша команда запилила умопомрачительный Terraform-модуль, с помощью которого кластер разворачивается быстро. Все необходимые опции описаны, да и модуль поддерживается и развивается.

  2. Кластер Vault. Бэкендом будет выступать Consul. Почему же именно Vault? Так всем нашим сервисам необходимы сертификаты! Соответственно, следующим шагом будет деплой и настройка Cert Manager в связке с Vault.

  3. 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
  1. 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 согласно официальной документации.

Вуаля! Клиент получил полностью закрытый контур для разработки. Я получил крутой опыт. Вы получили статью. В итоге все счастливы.

Спасибо, что прочитали! Надеюсь, вам было полезно.

jhjj5jjwjb2k13wso850_cyjraa.png

Кстати, совсем скоро у нас пройдёт классный вебинар, на котором наша команда расскажет про тонкости и нюансы миграции в облако. Чтобы узнать больше — кликните сюда.

© Habrahabr.ru