---
title: Единая авторизация (SSO) для домашней лаборатории
slug: homelab-k3s-sso
canonical: https://shady2k.ru/posts/homelab-k3s-sso/
date: 2024-08-11
kind: article
author: human
lang: ru
summary: Как закрыть единым входом приложения без своей авторизации через
  forward auth в ingress-nginx на K3s, и почему из Authelia и Authentik я выбрал
  Authentik.
tags:
  - homelab
  - kubernetes
  - security
---

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

Напомню, что вся моя инфраструктура, включая этот сайт, работает на Kubernetes, а именно на K3s. Подробнее можно прочитать в [этой статье](/posts/k3s-homelab-setup/). Так что, если вы готовы, берите свой любимый напиток, устраивайтесь поудобнее и давайте начнем!

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

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

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

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

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

Плюсы:

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

Минусы:

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

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

Плюсы:

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

Минусы:

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

## Настройка K3s

Прежде чем перейти к интересному, убедитесь, что ваш K3s установлен и работает. Если нет, ознакомьтесь с [официальным руководством по установке K3s](https://docs.k3s.io/). Это очень просто, K3s представляет собой один бинарный файл, установка с одной нодой делается одной командой.

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

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

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

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: authelia
```

Конфигурацию Authelia будем хранить в секретах и подключать в контейнер с приложением. В приложенной конфигурации вам нужно задать `encryption_key` и добавить своего пользователя в `users`. Как добавлять пользователей [описано здесь](https://www.authelia.com/reference/guides/passwords/). В `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'.

```yaml
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: 'shady2k@gmail.com'
        groups:
          - 'admins'
```

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

```yaml
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.

```yaml
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 аннотации.

```yaml
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`.

```yaml
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/`](https://test.shady2k.ru/), запрос будет перенаправлен в Authelia, она проверяет наличие cookie у домена `shady2k.ru`, если cookie нет — пользователя перенаправит на страницу авторизации [`https://sso.shady2k.ru/`](https://sso.shady2k.ru/), если есть, откроется наше приложение.

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

**Authentik** более комплексное решение, поэтому для него обязательно нужно развернуть [Redis](https://artifacthub.io/packages/helm/bitnami/redis) и [PostgreSQL](https://artifacthub.io/packages/helm/bitnami/postgresql), я предпочитаю Helm чарты Bitnami. Повторяем шаг по созданию пространства имен и разворачиваем [Helm чарт Authentik](https://artifacthub.io/packages/helm/goauthentik/authentik).

```yaml
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.

![](/images/homelab-k3s-sso/01.webp)

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

## Forward auth (domain level)

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

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

```yaml
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.

![](/images/homelab-k3s-sso/02.webp)

- `Applications - Applications`, создаем приложение, в UI settings задаем `Launch URL`, адрес нашего приложения, выбираем ранее созданного провайдера.
- `Applications - Outposts`, выбираем созданную интеграцию с Kubernetes, добавляем наше приложение, чтобы оно оказалось в разделе `Selected Applications`. В разделе `Advanced settings - Configuration`, убеждаемся, что в `authentik_host` указан наш URL SSO [`https://sso.shady2k.ru`](https://sso.shady2k.ru).

## Forward auth (single application)

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

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

```yaml
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` с адресом нашего приложения.

![](/images/homelab-k3s-sso/03.webp)

- `Applications - Applications`, создаем приложение, в UI settings задаем `Launch URL`, адрес нашего приложения, выбираем ранее созданного провайдера.
- `Applications - Outposts`, выбираем созданную интеграцию с Kubernetes, добавляем наше приложение, чтобы оно оказалось в разделе `Selected Applications`. В разделе `Advanced settings - Configuration`, убеждаемся, что в `authentik_host` указан наш URL SSO [`https://sso.shady2k.ru`](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 будет использовать его. [Ссылка на обсуждение здесь](https://github.com/kubernetes/ingress-nginx/issues/2170#issuecomment-567792670). То есть `secretName` указываем только в том пространстве имен, где создавался сертификат, а для outpost секрет не указываем. [Ссылка на конфигурацию outpost](https://docs.goauthentik.io/docs/outposts/embedded/), параметр `kubernetes_ingress_secret_name`**.**

```yaml
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 секунд, соединение будет постоянно рваться.

```yaml
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 — это отличный повод начать, попробуйте, это весело (но это не точно).

---
