SSO

Единая авторизация (SSO) для домашней лаборатории

· 9 минуты на чтение
Единая авторизация (SSO) для домашней лаборатории
Photo by FlyD / Unsplash

Привет, любители серверов! При развертывании некоторых простых веб-приложений в домашней лаборатории я часто сталкиваюсь с проблемой отсутствия авторизации в них. Если вы когда-либо мечтали о волшебной палочке, которая позволит защитить такие приложения, да еще и внедрить единый вход (SSO), то вам повезло. Сегодня мы погружаемся в мир Single Sign-On (SSO) и forward authentication с использованием Authelia и Authentik в среде K3s.

Напомню, что вся моя инфраструктура, включая этот сайт, работает на Kubernetes, а именно на K3s. Подробнее можно прочитать в этой статье. Так что, если вы готовы, берите свой любимый напиток, устраивайтесь поудобнее и давайте начнем!

Что такое SSO и Forward Authentication?

Представьте, что вы приходите на вечеринку и вам нужно представляться при входе в каждый зал. Утомительно, правда? SSO — это как VIP-бейдж, который позволяет вам пройти через несколько приложений с единой авторизацией. Везде используется один и тот же логин и пароль, а авторизацию нужно будет пройти только один раз.

Многие приложения поддерживают OIDC, для них внедрить SSO не составляет труда. Если же разработчик не позаботился о авторизации, то нас спасет Forward authentication. Он устанавливается на уровне reverse proxy (например, в NGINX или Traefic), проверяет ваш ID и пускает вас внутрь, не беспокоя приложение. Что самое замечательное, само приложение понятия не имеет, что у вас там есть какая-то авторизация.

Знакомьтесь: Authelia и Authentik

Authelia

Представьте Authelia как швейцарский нож аутентификации. Это open-source решение, поддерживающее двухфакторную аутентификацию и отлично работающие с обратными прокси.

Плюсы:

  • Безопасность. Надежная двухфакторная аутентификация и гибкий контроль доступа.
  • Интеграция. Работает с различными обратными прокси.
  • Функциональность. Обширные возможности аутентификации. Настроить можно практически все.
  • Конфигурация. Есть веб-интерфейс, настраивается все прямо там.
  • Аналитика. Позволяет просмотреть все события по каждому приложению и отображает графики.

Минусы:

  • Аппаратные требования. Требует немного больше ресурсов (ЦП и ОЗУ) по сравнению с Authentik.
  • Сложность. Больше функций означает более крутой порог обучения.

Authentik

Authentik — это легковесный претендент, идеальный для Kubernetes-сред. Это как минималистичный друг, который справляется с задачами без лишней суеты.

Плюсы:

  • Простота. Легко развернуть и настроить.
  • Легковесность. Идеально для сред с ограниченными ресурсами.

Минусы:

  • Функциональность. Не так много функций, как у Authelia.
  • Масштабируемость. Может потребоваться доработка по мере роста пользовательской базы.
  • Конфигурация. Все настройки проводятся в файле, интерфейс есть только для логина.

Настройка K3s

Прежде чем перейти к интересному, убедитесь, что ваш K3s установлен и работает. Если нет, ознакомьтесь с официальным руководством по установке K3s. Это очень просто, K3s представляет собой один бинарный файл, установка с одной нодой делается одной командой.

Развертывание Authelia в K3s

У Authelia есть Helm chart, но его так и не смог сконфигурировать, приложение не стартовало, поэтому решил развернуть сам. Самая большая проблема в том, что документация очень скудная, а поиск в интернете касается устаревших версий. Развертывать будем без использования PV, Redis и PostgreSQL. При необходимости вы можете их использовать, моей целью было поднять очень легковесный stateless контейнер с хранением данных в памяти.

Начинаем с создания нового пространства имен, лучше всего не ставить все в default, а придерживаться правила: каждому приложению свое пространство имен.

apiVersion: v1
kind: Namespace
metadata:
  name: authelia

Конфигурацию Authelia будем хранить в секретах и подключать в контейнер с приложением. В приложенной конфигурации вам нужно задать encryption_key и добавить своего пользователя в users. Как добавлять пользователей описано здесь. В authelia_url задаете URL, который должен быть доступен извне, там будут проходить авторизацию пользователи. Блок rules нужен для задания правил авторизации для ваших приложений. У меня каждое приложение на своем поддомене.

  • configuration.yml: Это основной конфигурационный файл для Authelia.
    • authentication_backend: Настраивает бэкенд для аутентификации пользователей.
      • password_reset: Отключает функцию сброса пароля.
      • file: Указывает путь к файлу базы данных пользователей.
    • session: Настраивает управление сессиями.
      • cookies: Перечисляет домены и URL-адреса для куки сессий.
    • storage: Настраивает параметры хранения.
      • encryption_key: Используется для шифрования конфиденциальных данных.
      • local: Указывает путь к локальной базе данных SQLite.
    • notifier: Настраивает параметры уведомлений.
      • disable_startup_check: Определяет, отключать ли проверки при запуске.
      • filesystem: Указывает файл для хранения уведомлений.
    • access_control: Настраивает политики контроля доступа.
      • default_policy: Устанавливает политику доступа по умолчанию на "deny" (запрет).
      • rules: Определяет конкретные правила доступа для доменов.
        • domain: 'test.shady2k.ru': Разрешает однофакторную аутентификацию для этого домена.
  • users_database.yml: Этот файл содержит данные пользователей для аутентификации.
    • users: Перечисляет пользователей в системе.
      • shady: Пользователь со следующими атрибутами:
        • disabled: Указывает, отключена ли учетная запись пользователя.
        • displayname: Отображаемое имя пользователя.
        • password: Пароль пользователя (в реальном мире должен быть захеширован).
        • email: Электронная почта пользователя.
        • groups: Перечисляет группы, к которым принадлежит пользователь, например, 'admins'.
apiVersion: v1
kind: Secret
metadata:
  name: authelia-config
  namespace: authelia
stringData:
  configuration.yml: |
    authentication_backend:
      password_reset:
        disable: true
      file:
        path: /config/users_database.yml
    session:
      cookies:
        - domain: shady2k.ru
          authelia_url: 'https://sso.shady2k.ru'
    storage:
      encryption_key: 'xxx'
      local:
        path: /tmp/db.sqlite3
    notifier:
      disable_startup_check: false
      filesystem:
        filename: /tmp/notification.txt
    access_control:
      default_policy: deny
      rules:
        - domain: 'test.shady2k.ru'
          policy: one_factor
  users_database.yml: |
    users:
      shady:
        disabled: false
        displayname: 'Alexandr'
        password: 'xxx'
        email: '[email protected]'
        groups:
          - 'admins'

Поскольку у нас нет PV (PersistentVolume), создаем Deployment. Обратите внимание на enableServiceLinks: false.

kind: Deployment
apiVersion: apps/v1
metadata:
  name: authelia
  namespace: authelia
  labels:
    app: authelia
spec:
  replicas: 1
  selector:
    matchLabels:
      app: authelia
  template:
    metadata:
      labels:
        app: authelia
    spec:
      enableServiceLinks: false
      containers:
        - name: authelia
          image: authelia/authelia:4.38
          imagePullPolicy: IfNotPresent
          resources:
            requests:
              cpu: "50m"
              memory: "256Mi"
            limits:
              cpu: "250m"
              memory: "512Mi"
          ports:
            - name: http
              containerPort: 9091
          volumeMounts:
            - name: config
              mountPath: "/config"
              readOnly: true
      volumes:
        - name: config
          secret:
            secretName: authelia-config

Создаем Service и Ingress.

apiVersion: v1
kind: Service
metadata:
  name: authelia-service
  namespace: authelia
spec:
  selector:
    app: authelia
  ports:
  - protocol: TCP
    port: 9091
    targetPort: 9091

---

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: authelia-sso-ingress
  namespace: authelia
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  rules:
  - host: sso.shady2k.ru
    http:
      paths:
      - pathType: Prefix
        path: /
        backend:
          service:
            name: authelia-service
            port:
              number: 9091
  tls:
    - hosts:
      - sso.shady2k.ru
      secretName: authelia-secret-sso-tls

Осталось защитить наше приложение. Для этого добавляем в Ingress аннотации.

nginx.ingress.kubernetes.io/auth-method: 'GET'
nginx.ingress.kubernetes.io/auth-url: 'http://authelia-service.authelia.svc.cluster.local:9091/api/authz/auth-request'
nginx.ingress.kubernetes.io/auth-signin: 'https://sso.shady2k.ru?rm=$request_method'
nginx.ingress.kubernetes.io/auth-response-headers: 'Remote-User,Remote-Name,Remote-Groups,Remote-Email'

Полный пример Ingress для поддомена test.shady2k.ru.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  namespace: test
  annotations:
    nginx.ingress.kubernetes.io/auth-method: 'GET'
    nginx.ingress.kubernetes.io/auth-url: 'http://authelia-service.authelia.svc.cluster.local:9091/api/authz/auth-request'
    nginx.ingress.kubernetes.io/auth-signin: 'https://sso.shady2k.ru?rm=$request_method'
    nginx.ingress.kubernetes.io/auth-response-headers: 'Remote-User,Remote-Name,Remote-Groups,Remote-Email'
spec:
  ingressClassName: nginx
  rules:
  - host: test.shady2k.ru
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: test-service
            port:
              number: 9999
  tls:
    - hosts:
      - test.shady2k.ru
      secretName: test-secret-tls

На этом настройка завершена. При переходе по адресу https://test.shady2k.ru/, запрос будет перенаправлен в Authelia, она проверяет наличие cookie у домена shady2k.ru, если cookie нет — пользователя перенаправит на страницу авторизации https://sso.shady2k.ru/, если есть, откроется наше приложение.

Развертывание Authentik в K3s

Authentik более комплексное решение, поэтому для него обязательно нужно развернуть Redis и PostgreSQL, я предпочитаю Helm чарты Bitnami. Повторяем шаг по созданию пространства имен и разворачиваем Helm чарт Authentik.

apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
  name: authentik
  namespace: kube-system
spec:
  chart: authentik
  repo: https://charts.goauthentik.io/
  targetNamespace: authentik
  version: 2024.6.3
  set:
  valuesContent: |-
    authentik:
      log_level: debug
      secret_key: xxx
      postgresql:
        host: authentik-postgresql
        user: file:///postgres-creds/username
        password: file:///postgres-creds/password
      redis:
        host: authentik-redis-master.authentik.svc.cluster.local
    server:
      resources:
        limits:
          cpu: 1000m
          memory: 1Gi
        requests:
          cpu: 100m
          memory: 256Mi
      ingress:
        enabled: false
      volumes:
        - name: postgres-creds
          secret:
            secretName: authentik-postgresql-secrets
      volumeMounts:
        - name: postgres-creds
          mountPath: /postgres-creds
          readOnly: true
    worker:
      resources:
        limits:
          cpu: 500m
          memory: 1Gi
        requests:
          cpu: 100m
          memory: 256Mi
      volumes:
        - name: postgres-creds
          secret:
            secretName: authentik-postgresql-secrets
      volumeMounts:
        - name: postgres-creds
          mountPath: /postgres-creds
          readOnly: true
    postgresql:
      enabled: false
    redis:
      enabled: false

После развертывания чарта у нас появятся pod с одним сервером и одним worker Authentik.

Для защиты приложения используем следующий аннотации. Убил кучу времени, прежде чем разобрался.

Authentik

Forward auth (domain level)

В этом режиме вы создаете одного Proxy провайдера, авторизация проходит для всего домена сразу, это нужно в том случае, если вы не хотите создавать разграничения прав по различным приложениям.

Таким образом, в аннотации nginx.ingress.kubernetes.io/auth-signin используем адрес нашего Authentik outpost, который доступен публично. В каждом защищаемом приложении аннотация одинаковая.

nginx.ingress.kubernetes.io/auth-url: |-
    http://ak-outpost-authentik-embedded-outpost.authentik.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: |-
    https://sso.shady2k.ru/outpost.goauthentik.io/start?rd=$scheme%3A%2F%2F$host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: |-
    Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
nginx.ingress.kubernetes.io/auth-snippet: |
    proxy_set_header X-Forwarded-Host $http_host;

Внутри веб-интерфейса Authentik:

  • System - Outpost integrations, создаем интеграцию с локальным Kubernetes, Kubeconfig не нужен.
  • Applications - Providers, для каждого приложения используется один и тот же провайдер, назначите вы приложению провайдер или нет не имеет значения. Создаем Proxy provider, выбираем default-authentication-flow и default-provider-authorization-implicit-consent соответственно. В разделе Forward auth (domain level) указываем Authentication URL с нашим SSO и Cookie domain.
  • Applications - Applications, создаем приложение, в UI settings задаем Launch URL, адрес нашего приложения, выбираем ранее созданного провайдера.
  • Applications - Outposts, выбираем созданную интеграцию с Kubernetes, добавляем наше приложение, чтобы оно оказалось в разделе Selected Applications. В разделе Advanced settings - Configuration, убеждаемся, что в authentik_host указан наш URL SSO https://sso.shady2k.ru.

Authentik

Forward auth (single application)

В этом режиме для каждого приложения (на отдельном поддомене) вы создаете одного Proxy провайдера, авторизация проходит для каждого приложения отдельно.

Таким образом, в аннотации nginx.ingress.kubernetes.io/auth-signin используем адрес нашего приложения. В каждом защищаемом приложении аннотация своя.

nginx.ingress.kubernetes.io/auth-url: |-
    http://ak-outpost-authentik-embedded-outpost.authentik.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: |-
    https://test.shady2k.ru/outpost.goauthentik.io/start?rd=$scheme%3A%2F%2F$host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: |-
    Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
nginx.ingress.kubernetes.io/auth-snippet: |
    proxy_set_header X-Forwarded-Host $http_host;

Внутри веб-интерфейса Authentik:

  • System - Outpost integrations, создаем интеграцию с локальным Kubernetes, Kubeconfig не нужен.
  • Applications - Providers, для каждого приложения используется свой провайдер. Создаем Proxy provider, выбираем default-authentication-flow и default-provider-authorization-implicit-consent соответственно. В разделе Forward auth (single application) указываем Authentication URL с адресом нашего приложения.
  • Applications - Applications, создаем приложение, в UI settings задаем Launch URL, адрес нашего приложения, выбираем ранее созданного провайдера.
  • Applications - Outposts, выбираем созданную интеграцию с Kubernetes, добавляем наше приложение, чтобы оно оказалось в разделе Selected Applications. В разделе Advanced settings - Configuration, убеждаемся, что в authentik_host указан наш URL SSO https://sso.shady2k.ru. Убедитесь, что класс ingress указан верно kubernetes_ingress_class_name.

Как это работает? После редактирования настроек authentik Embedded Outpost в ваш кластер деплоится ингресс ak-outpost-authentik-embedded-outpost, в котором терминируются все соединения с нашими доменами и path /outpost.goauthentik.io. Таким образом, если приходит запрос с любым другим path, обрабатывает запрос ingress приложения, если в пути есть /outpost.goauthentik.io, обрабатывает запрос ингресс authentik outpost. О том, что можно использовать один и тот же домен в ингресс разных неймспейсов, стало для меня открытием.

А как же быть с TLS, спросите вы? Похоже, что nginx загружает Ingress и связанные с ним секреты, и если есть какой-либо другой Ingress в другом пространстве имен, ссылающийся на тот же общий секрет по имени, он уже будет загружен фиктивным Ingress, поэтому nginx будет использовать его. Ссылка на обсуждение здесь. То есть secretName указываем только в том пространстве имен, где создавался сертификат, а для outpost секрет не указываем. Ссылка на конфигурацию outpost, параметр kubernetes_ingress_secret_name.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: dummy-load-certificates
  namespace: ingress-nginx
spec:
  tls:
  - hosts:
    - “*.example.com"
    secretName: cert-example.com
  rules:
  - host: “*.example.com"

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: example-app
  namespace: example-app
spec:
  tls:
  - hosts:
    - app.example.com
  rules:
    - host: app.example.com
      http:
        paths:
        - path: /
          backend:
            serviceName: example-app
            servicePort: 80

Если вдруг не работает websocket, попробуйте добавить аннотацию nginx.ingress.kubernetes.io/websocket-services. Также рекомендуется увеличить время соединения для websocket, по умолчанию 60 секунд, соединение будет постоянно рваться.

nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/websocket-services: test-service

Подведение итогов

Как показала практика, контейнер с Authelia потребляет до 400 Мб ОЗУ и совсем немного CPU. Контейнер с Authentik потребляет до 500 Мб ОЗУ на сервер и чуть меньше на воркер, CPU также совсем немного. Разница потребления ОЗУ почти в два раза.

На мой взгляд, Authentik выигрывает веб-интерфейсом и возможностями детальной конфигурации и расширения в будущем. Однако в сети есть куча недовольных отзывов, что каждая новая версия ломает всю конфигурацию. Все правильно поднять и настроить я смог только со третьего раза. Документация есть, но во многих местах есть недосказанность. В моей конфигурации пришлось вносить изменения в аннотацию Ingress, шерстя issue на Github. Но настройки кастомизации процесса авторизации и аутентификации очень широкие, выглядит как конструктор процесса.

Authelia — очень легковесное решение, которое запускается очень быстро и не требует Redis и PostgreSQL. При необходимости вы можете их использовать, моей целью было поднять очень легковесный stateless контейнер с хранением данных в памяти. Необходимость настраивать Authelia через файлы конфигурации немного раздражает, но тогда она не была бы такой легкой.

По итогу, оба решения работают хорошо, никаких проблем с ними не возникает. Пока решил остановиться на Authentik.

Это были отличные четыре дня исследований, чтения документации и конфигураций. Хочется надеяться, что эта статья кому-то поможет. Если вы давно мечтали о SSO — это отличный повод начать, попробуйте, это весело (но это не точно).