Привет, любители серверов! При развертывании некоторых простых веб-приложений в домашней лаборатории я часто сталкиваюсь с проблемой отсутствия авторизации в них. Если вы когда-либо мечтали о волшебной палочке, которая позволит защитить такие приложения, да еще и внедрить единый вход (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': Разрешает однофакторную аутентификацию для этого домена.
- authentication_backend: Настраивает бэкенд для аутентификации пользователей.
- users_database.yml: Этот файл содержит данные пользователей для аутентификации.
- users: Перечисляет пользователей в системе.
- shady: Пользователь со следующими атрибутами:
- disabled: Указывает, отключена ли учетная запись пользователя.
- displayname: Отображаемое имя пользователя.
- password: Пароль пользователя (в реальном мире должен быть захеширован).
- email: Электронная почта пользователя.
- groups: Перечисляет группы, к которым принадлежит пользователь, например, 'admins'.
- shady: Пользователь со следующими атрибутами:
- users: Перечисляет пользователей в системе.
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 SSOhttps://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 SSOhttps://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 — это отличный повод начать, попробуйте, это весело (но это не точно).