Использование OAuth2 с Nginx Ingress Controller
Постоянно в хозяйстве появляются интерфейсы, у которых своей аутентификации нет, а доступ нужно выдать ограниченной группе доверенных товарищей. Например, к интерфейсу шибко популярного Prometheus. Или часто к какому-то самопису.
Как мы решаем эту проблему? Бывает, никак. Закрываем доступ из внешней сети, а во внутренней, понятно, все свои. Следующий пункт -- собираем все админки за nginx, кладем htpasswd типа admin:<хеш страшно длинного пароля> и настраиваем basic auth через него в серверах всех своих админок. Ну и, наконец, выдаём каждому по паролю, и раскладываем htpasswd побольше. На этом этапе уже ропщет и команда, которая прилежно копипастит пароли от сервиса к сервису, и поддерживающий конфигурацию админ. До вершины эволюции этого подхода -- раздачи каждому разных паролей для разных сервисов -- на практике доходят редко, потому что настроения в коллективе уже на предыдущей стадии приближаются к революционным.
Что можно предложить для снятия боли от поддержки настройки доступа к зоопарку админок? Документация по Nginx Ingress Controller рассказывает о сравнительно несложном универсальном способе прикрутить аутентификацию OAuth2 к любому веб-серверу. Речь о настройке для каждой админки oauth2-proxy. Пользователям станет безусловно легче. OAuth2 полностью снимет вопрос с генерацией и раздачей паролей каждый раз, когда заводится новый интерфейс. Насколько легче станет админу, вопрос открытый. Сравнение сложности поддержки инсталляций oauth2-proxy вместо файликов htpasswd зависит от личных предпочтений и уровня владения средствами автоматизации, особенно в контексте Kubernetes. Чтобы сопоставить решение со своими предпочтениями, рассмотрим пример.
Постановка задачи
Далеко ходить не будем, возьмём для примера этот сайт. Сайт хостится в карманном кластере Kubernetes с Nginx Ingress Controller. Допустим, я хочу сделать отдельную инсталляцию сайта с черновыми вариантами статей и защитить доступ к ней от всех, кроме группы доверенных редакторов. Исходные тексты сайта хранятся в GitLab, поэтому за провайдером OAuth2 тоже далеко не пойдём. GitLab хорош ещё и тем, что oauth2-proxy поддерживает базовую авторизацию пользователей проверкой их принадлежности к группе GitLab.
Итак, создадим сайт http://drafts.remizov.org, доступ к которому должен быть только у пользователей GitLab, входящих в группу https://gitlab.com/remizov-org-drafts.
Настройка GitLab
Поскору разберёмся с GitLab.
Создадим группу, в которую будем включать редакторов.
После этого зарегистрируем будущую инсталляцию oauth2-proxy как приложение.
Обратите внимание, что здесь я использую URL без шифрования, http://. Это сделано исключительно для того, чтобы сократить примеры манифестов Ingress, требует дополнительных телодвижений по понижению уровня безопасности кук и в реальной жизни использоваться не должно.
Приложению должна быть разрешена работа с openid, profile и email.
URI /oauth2/callback без необходимости менять не нужно. Это значение, на которое по-умолчанию настроен oauth2-proxy.
При настройке переменных окружения нам понадобятся значения полей Application ID и Secret.
После закрытия формы значения будут доступны через список приложений.
Настройка Kubernetes
Итак, у нас уже используется Nginx Ingress Controller. Сайт установлен и работает. Подключение к нему настроено при помощи Ingress:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: drafts-remizov-org spec: rules: - host: drafts.remizov.org http: paths: - backend: serviceName: remizov-org servicePort: 80 path: /
Установка OAuth2 proxy
Значение поля Secret, полученное при регистрации приложения, сохраним в секрете Kubernetes. Там же сохраним случайную строку для создания защищённых кук:
kubectl create secret generic oauth2-proxy-secret \ --from-literal=client_secret=e2fa6cf3a7fa5e3f7102b661ef4fd491601965d3f43632732d01d6f28919297a \ --from-literal=cookie_secret=ymOhGQEKH3cVMipUEoM39QSdfDLQJUFxrUEQexJ7eOMMu6SH2UVnfJBV93a4fwtL
Создадим минимальный Deployment для запуска oauth2-proxy.
Понадобится установить переменные окружения
- OAUTH2_PROXY_CLIENT_ID
значение из поля Application ID формы регистрации приложения
- OAUTH2_PROXY_CLIENT_SECRET
сошлёмся на секрет, где хранится значение Secret
- OAUTH2_PROXY_COOKIE_SECRET
и значение cookie_secret
- OAUTH2_PROXY_PROVIDER
Один из многочисленных возможных провайдеров OAuth2. В нашем случае это gitlab.
- OAUTH2_PROXY_GITLAB_GROUP
Группа, членство в которой даёт доступ к нашему приложению. В нашем случае -- remizov-org-drafts.
Ещё одна переменная, которая не должна понадобиться в реальной жизни, а в этот пример попала только для поддержки незащищённых запросов по http://
- OAUTH2_PROXY_COOKIE_SECURE
Управление использованием защищённых кук, по-умолчанию "true".
apiVersion: apps/v1 kind: Deployment metadata: name: oauth2-proxy spec: selector: matchLabels: app: oauth2-proxy template: metadata: labels: app: oauth2-proxy spec: containers: - name: oauth2-proxy image: quay.io/pusher/oauth2_proxy:v5.0.0 imagePullPolicy: IfNotPresent args: - --http-address - :4180 - --email-domain - '*' ports: - name: application containerPort: 4180 protocol: TCP env: - name: OAUTH2_PROXY_CLIENT_ID value: 3783106dc885fb92b6f1294060a4432ac7b25c5c5952dde03f83d1eccd9c3a75 - name: OAUTH2_PROXY_CLIENT_SECRET valueFrom: secretKeyRef: name: oauth2-proxy-secret key: client_secret - name: OAUTH2_PROXY_COOKIE_SECRET valueFrom: secretKeyRef: name: oauth2-proxy-secret key: cookie_secret - name: OAUTH2_PROXY_PROVIDER value: gitlab - name: OAUTH2_PROXY_GITLAB_GROUP value: remizov-org-drafts # Don't use it in production - name: OAUTH2_PROXY_COOKIE_SECURE value: "false"
Немного о ключах приложения.
Если не установить --http-address :4180, сервис привяжется к 127.0.0.1. Для нашего варианта установки это не подойдёт.
--email-domain -- обязательный параметр, указывает домен, к которому должны принадлежать почтовые адреса пользователей. Мы уже ограничиваем доступ группой GitLab, дополнительное ограничение нам не требуется, поэтому разрешаем любой домен.
Создадим Service для публикации приложения в кластере:
apiVersion: v1 kind: Service metadata: name: oauth2-proxy spec: type: ClusterIP selector: app: oauth2-proxy ports: - name: http port: 80 targetPort: application protocol: TCP
и Ingress для доступа снаружи:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: oauth2-proxy spec: rules: - host: drafts.remizov.org http: paths: - backend: serviceName: oauth2-proxy servicePort: 80 path: /oauth2
Здесь важно, чтобы домен совпал с доменом, который мы собираемся закрыть, а путь по-умолчанию был /oauth2. Если хочется поменять путь, поменяйте его и здесь, и в приложении при помощи ключа -proxy-prefix и в форме свойств приложения GitLab в поле Callback URL. Чувствуете, что лучше не трогать?
Настройка Ingress для приложения
Итак, сейчас, если на наш сайт будут приходить запросы от сервера OAuth2, их обработает приложение oauth2-proxy. Теперь нужно настроить основное приложение так, чтобы неавторизованные запросы перенаправлялись на сервер авторизации.
Выше был показан Ingress, созданный для сайта. Нужно добавить к нему две аннотации:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: drafts-remizov-org annotations: nginx.ingress.kubernetes.io/auth-signin: http://$host/oauth2/start?rd=$escaped_request_uri nginx.ingress.kubernetes.io/auth-url: http://$host/oauth2/auth . . .
Авторизация приложения в GitLab
Теперь откроем сайт в браузере. Если мы не ошиблись с настройками, то будем перенаправлены на страницу авторизации приложения:
Подтверждаем авторизацию и, наконец, попадаем на сайт, но уже с удостоверением от GitLab.
Заключительные замечания
У этого решения есть некоторая проблема с управляемостью. На каждый защищаемый домен нужно регистрировать приложение в GitLab и запускать экземпляр oauth2-proxy. Проблема регистрации в том, что приложения не завезли ни в terraform ни в ansible, и нужно пилить левый скрипт или, не чинясь, заводить их руками. Запуск oauth2-proxy можно оформить нормально, но кого-то может удавить жаба за 10М памяти на экземпляр. А вообще в эту задачу просится Admission Controller или оператор. Но это совсем другая история.