Использование 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, требует дополнительных телодвижений по понижению уровня безопасности кук и в реальной жизни использоваться не должно.

Создание приложения в GitLab

Приложению должна быть разрешена работа с openid, profile и email.

URI /oauth2/callback без необходимости менять не нужно. Это значение, на которое по-умолчанию настроен oauth2-proxy.

Создание приложения в GitLab

При настройке переменных окружения нам понадобятся значения полей 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.

Заключительные замечания

У этого решения есть некоторая проблема с управляемостью. На каждый защищаемый домен нужно регистрировать приложение в GitLab и запускать экземпляр oauth2-proxy. Проблема регистрации в том, что приложения не завезли ни в terraform ни в ansible, и нужно пилить левый скрипт или, не чинясь, заводить их руками. Запуск oauth2-proxy можно оформить нормально, но кого-то может удавить жаба за 10М памяти на экземпляр. А вообще в эту задачу просится Admission Controller или оператор. Но это совсем другая история.