Использование 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 или оператор. Но это совсем другая история.