Небольшой дисклеймер: я технарь, а не писатель — поэтому в оформлении статьи мне помогал AI. Все конфиги, скрипт и правила писал сам и проверял на реальном железе. AI просто помог не превратить это в простыню из bullet points 😅
Привет, друзья! С вами pensecfort. Сегодня мы закрываем одну из самых частых болей в любой инфраструктуре — управление доступом к инструментам мониторинга.
Эта статья — текстовое дополнение к одноимённому видео на YouTube. Если вы предпочитаете смотреть, вот ссылочка на видео https://youtu.be/YpBtcoefblY или вот альтернатива https://rutube.ru/video/a48737880959683a83e97cf9e8fab4c0/. Если предпочитаете читать — вы попали по адресу. Здесь всё то же самое: реальные конфиги, мои скрипты, и ни капли маркетингового булшита.
🤔 Зачем вообще нужен Authentik?
Представьте типичную ситуацию. У вас есть Wazuh, Grafana, Portainer, Proxmox, несколько сетевых девайсов, и у каждого сервиса — своя база пользователей. Пользователь заходит в Wazuh с одним паролем, в Grafana — с другим, в Portainer — с третьим. Когда сотрудник уходит из команды, вам нужно вспомнить, где вообще у него есть доступ, и руками удалить его из каждого сервиса. Это хаос. Это дыры в безопасности. И это реальная головная боль для любого, кто обслуживает инфраструктуру.
Authentik — self-hosted Identity Provider, то есть ваш собственный центр управления удостоверениями. Думайте о нём как о личном Google-аккаунте, но для вашей инфраструктуры. Вы сами контролируете данные, вы сами управляете доступом, и ничего не уходит в облако третьих сторон.
Какие конкретные проблемы он закрывает:
Единый вход (SSO). Один аккаунт — доступ ко всем сервисам. Пользователь логинится один раз в Authentik и автоматически заходит в Wazuh, Grafana, и всё остальное, что вы подключили. Никаких отдельных паролей.
Централизованное управление пользователями. Уволился сотрудник — заблокировали один аккаунт в Authentik. Всё. Доступ ко всем сервисам сразу отрезан. Не надо бегать по системам.
2FA из коробки. Включили TOTP один раз — и он работает для всех подключённых сервисов. Не нужно настраивать двухфакторку отдельно в каждом приложении.
RBAC — разграничение доступа по ролям. Разработчик видит только логи своего проекта. Аналитик SOC видит алерты, но не может менять конфиги. Администратор видит всё. Это настраивается через группы и роли в Authentik и пробрасывается в Wazuh через SAML.
Полный аудит входов. Вы всегда видите, кто, когда и откуда заходил. И эти логи мы будем собирать прямо в Wazuh — ваша система мониторинга будет следить в том числе за самой системой доступа.
Короче говоря, Authentik превращает разрозненную инфраструктуру в единую управляемую экосистему.
Сегодня сделаем три вещи:
Настроим SSO — вход в Wazuh через Authentik по протоколу SAML.
Включим обязательный 2FA через TOTP.
Настроим сбор логов Authentik в Wazuh с готовыми правилами.
⚙️ Часть 1. Интеграция SSO: Authentik → Wazuh через SAML
Развёртывание самого Authentik здесь не разбираем — исходим из того, что он у вас уже запущен. Официальная документация по интеграции живёт по адресу: https://integrations.goauthentik.io/monitoring/wazuh/ — там есть точные технические детали, но статья написана сухо и без контекста. Я покажу своими словами и со своими конфигами.
На стороне Authentik
Шаг 1 — Создаём группу и добавляем пользователя
Идём в Directory → Groups, создаём группу wazuh-administrators (wazuh-alalytics или иную другую, которую вы захотите). Добавляем в неё нужных пользователей через вкладку Users → Add existing user. Именно по группам потом будет работать RBAC — это фундамент всей схемы.
Шаг 2 — Property Mappings: передаём роли в SAML-ответ
Идём в Customization → Property Mappings → Create. Выбираем тип SAML Provider Property Mapping. Это ключевой момент — именно здесь мы говорим Authentik, что нужно передавать в SAML-ответ атрибут Roles, который Wazuh потом читает и назначает внутренние роли.
Параметры маппинга:
Поле | Значение |
|---|---|
Name | Wazuh Roles Mapping |
SAML Attribute Name |
|
Friendly Name | (оставляем пустым) |
Expression | (см. ниже) |
if ak_is_group_member(request.user, name="wazuh-administrators"): yield "wazuh-admin" # Если необходимо ещё какие то группы добавить elif ak_is_group_member(request.user, name="wazuh-analytics"): yield "wazuh-analytics"
Выражение простое: если пользователь состоит в группе wazuh-administrators — добавляем ему роль wazuh-admin в атрибут Roles. Если понадобятся дополнительные роли (например, wazuh-readonly) — добавляем аналогичные if-блоки, либо elif. Жмём Finish.
Шаг 3 — Создаём SAML Provider
Идём в Applications → Providers → Create → SAML Provider. Заполняем поля:
Name: Wazuh
Authorization Flow: выбираем дефолтный или свой кастомный flow аутентификации
ACS URL: здесь важный момент, который я хочу разобрать отдельно
Есть два варианта:
https://wazuh.yourdomain.com/_opendistro/_security/saml/acs— это SP-initiated flow. Пользователь открывает дашборд Wazuh, его редиректит на Authentik, он логинится и возвращается обратно.https://wazuh.yourdomain.com/_opendistro/_security/saml/acs/idpinitiated— это IdP-initiated flow. Пользователь сначала заходит в Authentik или на его портал приложений, выбирает Wazuh, и его автоматически пускает внутрь.
Я использую второй вариант — мне нравится работать через единый портал Authentik. Все сервисы в одном месте, пользователи видят только то, к чему у них есть доступ. Это удобнее и чище с точки зрения UX. Поэтому в моём конфиге idp_initiated: true — об этом чуть ниже.
Остальные важные поля:
Issuer:
wazuh-samlService Provider Binding:
PostProperty Mappings: добавляем созданный нами маппинг ролей
NameID Property Mapping: выбираем, что будет использоваться как имя пользователя в Wazuh (например,
authentik default SAML Mapping: NameилиEmail)
Шаг 4 — Создаём Application и скачиваем metadata
Идём в Applications → Applications → Create. Привязываем созданный Provider к приложению. После создания возвращаемся к провайдеру — там будет раздел Related objects → Metadata → Download. Скачиваем XML-файл с метаданными. Он нам понадобится на стороне Wazuh.
На стороне Wazuh Indexer
⚠️ Важно для кластера. Если у вас несколько нод wazuh-indexer — все операции с файлами и
securityadmin.shнужно выполнить на каждой ноде.
Копируем скачанный idp-metadata.xml на сервер:
cp idp-metadata.xml /etc/wazuh-indexer/opensearch-security/ chown wazuh-indexer:wazuh-indexer /etc/wazuh-indexer/opensearch-security/idp-metadata.xml chmod 640 /etc/wazuh-indexer/opensearch-security/idp-metadata.xml
Теперь открываем главный конфиг аутентификации. Сначала сделайте бэкап:
cp /etc/wazuh-indexer/opensearch-security/config.yml \ /etc/wazuh-indexer/opensearch-security/config.yml.bak vim /etc/wazuh-indexer/opensearch-security/config.yml
Этот файл управляет тем, как пользователи могут заходить в систему. Мы настраиваем два метода одновременно — Basic Auth как запасной вариант и SAML как основной.
--- authc: basic_internal_auth_domain: description: "HTTP basic authentication" http_enabled: true transport_enabled: true order: 0 http_authenticator: type: basic challenge: false authentication_backend: type: intern saml_auth_domain: http_enabled: true transport_enabled: false order: 1 http_authenticator: type: saml challenge: true config: idp_initiated: true idp: metadata_file: "/etc/wazuh-indexer/opensearch-security/idp-metadata.xml" entity_id: "wazuh-saml" sp: entity_id: "wazuh-saml" kibana_url: "https://wazuh.yourdomain.com/" roles_key: Roles exchange_key: "сюда_вставляете_ключ_из_openssl_rand_-hex_32" authentication_backend: type: noop
Разберём ключевые параметры:
idp_initiated: true — включает flow, при котором пользователь начинает из Authentik, а не из дашборда Wazuh. Именно поэтому я и выбрал IdP-initiated flow: один портал для всего, и этот параметр его включает на стороне Wazuh.
metadata_file — путь к XML с метаданными Authentik. Там все сертификаты и эндпоинты, которые Wazuh использует для проверки SAML-ответов.
entity_id — уникальный идентификатор. Должен совпадать с Issuer, указанным в Authentik при создании провайдера.
kibana_url — базовый URL вашего дашборда. Обязательно с / в конце! Authentik использует его для формирования ссылок возврата после аутентификации.
roles_key: Roles — атрибут из SAML-ответа, в котором Authentik передаёт группы пользователя. Именно тот, что мы настраивали в Property Mappings.
exchange_key — секретный ключ для подписи внутренних JWT-токенов после SAML-аутентификации. Генерируем:
openssl rand -hex 32
⚠️ Ключ должен быть одинаковым на всех нодах кластера. Если он разный — пользователь будет залогинен на одной ноде и выбит на другой.
challenge: false в basic-блоке — важно, чтобы браузер не показывал стандартное окно Basic Auth и не конфликтовал с SAML.
authentication_backend: noop в SAML-блоке — после успешного SAML Wazuh сразу принимает пользователя, не идёт проверять его в internal_users.yml.
Как это работает в итоге:
Пользователь открывает дашборд (или заходит через портал Authentik).
Basic Auth не проходит (нет заголовка) — переходим к SAML (order: 1).
Wazuh редиректит на Authentik (или при IdP-initiated Authentik сам инициирует flow).
Пользователь логинится + проходит 2FA.
Authentik присылает SAML Response с атрибутом
Roles.Wazuh создаёт внутренний токен через
exchange_keyи пускает пользователя.
Применяем конфиг:
export JAVA_HOME=/usr/share/wazuh-indexer/jdk/ && bash \ /usr/share/wazuh-indexer/plugins/opensearch-security/tools/securityadmin.sh \ -f /etc/wazuh-indexer/opensearch-security/config.yml \ -icl -key /etc/wazuh-indexer/certs/admin-key.pem \ -cert /etc/wazuh-indexer/certs/admin.pem \ -cacert /etc/wazuh-indexer/certs/root-ca.pem \ -h indexer.yourdomain.com -p 443 -nhnv
⚠️ Необходимо явно указать порт, а ту по умолчанию подставляется 9200 по крайней мене у меня так происходило. Если у вас кластер достаточно применить конфиг на одной из нод. НО файлы на диске ДОЛЖНЫ быть на каждой ноде.
Замените indexer.yourdomain.com на FQDN вашей ноды.
Маппинг ролей (roles_mapping.yml)
Теперь говорим Wazuh, что роль wazuh-admin из SAML-атрибута соответствует внутренней роли all_access. Открываем файл (не забудьте бэкап):
cp /etc/wazuh-indexer/opensearch-security/roles_mapping.yml \ /etc/wazuh-indexer/opensearch-security/roles_mapping.yml.bak vim /etc/wazuh-indexer/opensearch-security/roles_mapping.yml
Добавляем или дополняем секцию all_access:
all_access: reserved: true hidden: false backend_roles: - "wazuh-admin" - "admin" hosts: [] users: [] and_backend_roles: [] description: "Maps admin to all_access"
Wazuh очень гибкий в плане RBAC: можно ограничить доступ до конкретного агента, группы агентов или типа логов. Применяем маппинг:
export JAVA_HOME=/usr/share/wazuh-indexer/jdk/ && bash \ /usr/share/wazuh-indexer/plugins/opensearch-security/tools/securityadmin.sh \ -f /etc/wazuh-indexer/opensearch-security/roles_mapping.yml \ -icl -key /etc/wazuh-indexer/certs/admin-key.pem \ -cert /etc/wazuh-indexer/certs/admin.pem \ -cacert /etc/wazuh-indexer/certs/root-ca.pem \ -h indexer.yourdomain.com -p 443 -nhnv
Настройка Wazuh Dashboard
Если в wazuh.yml у вас run_as: true — нужен дополнительный маппинг на уровне дашборда. Идём в веб-интерфейс: Server Management → Security → Roles mapping → Create Role mapping и настраиваем:
Role Name:
authentik_adminsRoles:
administratorCustom rules: User field =
backend_roles, Operation =FIND, Value =wazuh-admin
Затем добавляем параметры в конфиг Wazuh Dashboard:
vim /etc/wazuh-dashboard/opensearch_dashboards.yml
opensearch_security.auth.type: "saml" server.xsrf.allowlist: - "/_opendistro/_security/saml/acs" - "/_opendistro/_security/saml/logout" - "/_opendistro/_security/saml/acs/idpinitiated" opensearch_security.session.keepalive: false
Разберём что здесь происходит:
auth.type: "saml" — главный переключатель. Без него дашборд не знает про SAML и продолжает требовать логин/пароль.
Если нужно оставить оба метода одновременно (например, пока тестируете и хотите сохранить Basic Auth как запасной вариант):
opensearch_security.auth.type: ["basicauth", "saml"] opensearch_security.auth.multiple_auth_enabled: true
Я у себя оставил Basic Auth и SAML. Если что-то сломается на уровне Authentik, можно зайти напрямую на wazuh-indexer/wazuh-dashboard и временно включить basic обратно.
xsrf.allowlist — белый список эндпоинтов, исключённых из XSRF-защиты. Без этого SAML просто не сработает: Authentik успешно проверит пользователя и отправит подтверждение обратно, а Wazuh ответит «нет секретного заголовка» и заблокирует вход. Эта строка это предотвращает. Обратите внимание — в списке три эндпоинта, включая /idpinitiated, который нужен именно для нашего IdP-initiated flow.
session.keepalive: false — когда keepalive включён, дашборд пытается продлевать сессию в фоне, и при SAML это часто приводит к конфликтам. Ставим false и отдаём управление сессией Authentik.
Перезапускаем дашборд:
sudo systemctl restart wazuh-dashboard
Всё, вход теперь через Authentik. В моём случае пользователи хранятся локально в Authentik, но можно подключить AD, FreeIPA или любой другой LDAP-провайдер — Authentik это поддерживает из коробки.
🔐 Часть 2. Включаем обязательный 2FA через TOTP
Теперь сделаем вход безопаснее — добавим обязательный второй фактор. Это можно сделать двумя способами: пользователь настраивает его сам, или вы делаете его принудительным для всех.
В статье я укажу принудительный вариант (если хотите посмотреть как включается у пользователя, посмотрите в видео) — это правильная практика для production-среды.
В Authentik идём в Flows → Flows. Находим дефолтный flow аутентификации (обычно называется default-authentication-flow). Заходим в него и смотрим на список Stage Bindings.
Добавляем Stage:
Нажимаем Edit Stage в default-authentication-mfa-validation
В настройках Not configured action указываем Force the user to configure an authenticator — это ключевой параметр
В Device classes оставляем TOTP или добавляем всё что хотите (WebAuthn, Static tokens)
Устанавливаем порядок (order) после stage проверки пароля, но до финального Stage
После этого каждый вход в Authentik — а значит и в Wazuh, и во все остальные подключённые сервисы — будет требовать код из Google Authenticator или Microsoft Authenticator, ну или что вы указали в Device classes.
Пользователь при первом входе увидит QR-код, отсканирует его приложением-аутентификатором и привяжет свой телефон. Повторно настраивать это нигде больше не нужно — 2FA работает на уровне Authentik для всего.
💡 Лайфхак: если пользователь потерял телефон или сбросил приложение — зайдите в Directory → Users → выберите пользователя → вкладка MFA Authenticators и удалите привязанный TOTP. При следующем входе он пройдёт заново через QR-код.
📋 Часть 3. Сбор логов Authentik в Wazuh — самое вкусное
Теперь самое интересное — заставим Wazuh следить за самим Authentik. Каждый вход, каждый отказ в доступе, каждое изменение пользователя — всё это попадёт в дашборд как алерт. Ваша система безопасности будет следить в том числе за собой.
Шаг 1 — Создаём API-токен в Authentik
Идём в Admin → Directory → Tokens and App Passwords → Create.
Параметр | Значение |
|---|---|
Identifier |
|
User |
|
Intent | API Token |
Expiring | Выключено |
⚠️ Токен можно скопировать сразу в Tokens and App Passwords в Таблице увидите Action. Там 3 действия: edit, permissions, СOPY.
Я рекомендую создать для этого отдельного сервис-пользователя с минимальными правами, а не использовать akadmin. Так безопаснее — скомпрометированный токен сервис-аккаунта не даст полного доступа к системе.
Шаг 2 — Настраиваем retention событий
Admin → System → Settings → Event retention. По умолчанию 365 дней. Если скрипт регулярно забирает события — можно уменьшить до 30-90 дней, чтобы не раздувать базу Authentik. Всё равно основное хранение теперь в Wazuh.
Шаг 3 — Скрипт сбора логов
Все события Authentik логируются автоматически — пользовательские действия, системные события, ошибки конфигурации. Пароли и credentials из событий вырезаются Authentik автоматически, так что в логах они не появятся.
API endpoint для получения событий:
GET https://your-authentik-domain/api/v3/events/events/
Я написал скрипт, который забирает события через API, дедуплицирует их между запусками через state-файл и отправляет в Wazuh. Логи сразу пишутся в JSON-формате — отдельный декодер не нужен, Wazuh читает JSON нативно.
Скрипт полностью:
#!/usr/bin/env python # -*- coding: utf-8 -*- """ ╔════════════════════════════════════════════════════════════════════════════╗ ║ Authentik → Wazuh Events Integration ║ ║ ║ ║ Developed by pensecfort ║ ║ Author: pensecfort ║ ║ Purpose: Reliable forwarding of Authentik audit events to Wazuh SIEM ║ ║ ║ ║ © pensecfort • 2026 ║ ╚════════════════════════════════════════════════════════════════════════════╝ """ import json import socket import os import sys import datetime import time import requests import urllib3 from base64 import b64encode from datetime import timezone # --------------------------------------------------------------------------- # AUTHENTIK CONFIGURATION # --------------------------------------------------------------------------- AUTHENTIK_URL = "https://authentik.example.com" # ← замените на ваш URL AUTHENTIK_TOKEN = "your-authentik-api-token" # ← токен из шага 1 AUTHENTIK_PAGE_SIZE = 1000 # max events per API page AUTHENTIK_VERIFY_SSL = True AUTHENTIK_LOOKBACK_MINUTES = 60 # используется только при первом запуске (нет state-файла) # --------------------------------------------------------------------------- # WAZUH CONFIGURATION # --------------------------------------------------------------------------- WAZUH_URL = "wazuh.example.com" # ← FQDN вашего Wazuh WAZUH_USER = "integration" # ← пользователь с правом /events WAZUH_PASSWORD = "your-wazuh-password" # ← пароль WAZUH_API_ENDPOINT = f"https://{WAZUH_URL}/events" WAZUH_VERIFY_SSL = True WAZUH_BATCH_SIZE = 100 # число событий на один POST-запрос # --------------------------------------------------------------------------- # OUTPUT TOGGLES — включайте нужное # --------------------------------------------------------------------------- WRITE_EVENTS_TO_FILE = True # писать события в JSON-файл (для Filebeat/логротации) SEND_TO_WAZUH_API = True # отправлять события через REST API Wazuh WRITE_ERROR_LOG = True # писать ошибки скрипта в отдельный лог # --------------------------------------------------------------------------- # FILE PATHS — укажите реальные пути на вашем сервере # --------------------------------------------------------------------------- EVENTS_FILE_PATH = "/var/log/authentik/events.json" # ← путь к файлу событий ERROR_LOG_PATH = "/var/log/authentik/errors.log" # ← путь к логу ошибок STATE_FILE_PATH = "/var/lib/authentik/state.json" # ← путь к state-файлу # Отключаем InsecureRequestWarning, если SSL-верификация выключена для одного из сервисов if not AUTHENTIK_VERIFY_SSL or not WAZUH_VERIFY_SSL: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # --------------------------------------------------------------------------- # SYSLOG-FORMAT ERROR LOGGER # --------------------------------------------------------------------------- _HOSTNAME = socket.gethostname() _PID = os.getpid() _COMPONENT = "authentik_wazuh" CRITICAL = "CRITICAL" ERROR = "ERROR" WARNING = "WARNING" INFO = "INFO" DEBUG = "DEBUG" def log_error(level: str, message: str, **extra): """ Пишет строку ошибки в stderr и опционально в ERROR_LOG_PATH. """ timestamp = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") fields = " | ".join(f"{k}={v}" for k, v in extra.items()) if extra else "" line = f"{timestamp} {_HOSTNAME} {_COMPONENT}[{_PID}]: {level.upper()} {message}" if fields: line += f" | {fields}" print(line, file=sys.stderr) if WRITE_ERROR_LOG: try: os.makedirs(os.path.dirname(ERROR_LOG_PATH), exist_ok=True) with open(ERROR_LOG_PATH, "a") as f: f.write(line + "\n") except Exception as e: print(f"{timestamp} {_HOSTNAME} {_COMPONENT}[{_PID}]: ERROR Failed to write error log | error={e}", file=sys.stderr) # --------------------------------------------------------------------------- # STATE — сохраняем последнее обработанное событие между запусками # --------------------------------------------------------------------------- def load_last_state() -> dict: """ Загружает последнее сохранённое состояние (timestamp и UUID события). Возвращает словарь: {'timestamp': str, 'last_event_pk': str | None} """ if os.path.exists(STATE_FILE_PATH): try: with open(STATE_FILE_PATH, 'r') as f: state = json.load(f) if state.get('last_created_timestamp'): return { 'timestamp': state['last_created_timestamp'], 'last_event_pk': state.get('last_event_pk') } except Exception as e: log_error(WARNING, "Could not read state file, using default lookback", path=STATE_FILE_PATH, error=e) default_ts = (datetime.datetime.now(timezone.utc) - datetime.timedelta(minutes=AUTHENTIK_LOOKBACK_MINUTES)).isoformat().replace('+00:00', 'Z') log_error(INFO, "No valid state found, falling back to lookback window", lookback_minutes=AUTHENTIK_LOOKBACK_MINUTES, since=default_ts) return {'timestamp': default_ts, 'last_event_pk': None} def save_last_state(timestamp: str, event_pk: str): """Сохраняет timestamp и UUID последнего обработанного события.""" try: os.makedirs(os.path.dirname(STATE_FILE_PATH), exist_ok=True) with open(STATE_FILE_PATH, 'w') as f: state = {'last_created_timestamp': timestamp, 'last_event_pk': event_pk} json.dump(state, f) except Exception as e: log_error(ERROR, "Could not save state file", path=STATE_FILE_PATH, error=e) # --------------------------------------------------------------------------- # AUTHENTIK — получаем аудит-события с пагинацией # --------------------------------------------------------------------------- def fetch_authentik_logs(since_timestamp: str) -> list[dict]: """ Получает все события новее since_timestamp. Обрабатывает пагинацию и возвращает события от старых к новым. """ headers = { "Authorization": f"Bearer {AUTHENTIK_TOKEN}", "Accept": "application/json" } url = f"{AUTHENTIK_URL}/api/v3/events/events/" params = { "ordering": "-created", "page_size": AUTHENTIK_PAGE_SIZE, "created__gte": since_timestamp, } all_logs = [] while url: try: response = requests.get( url, headers=headers, params=params, verify=AUTHENTIK_VERIFY_SSL, timeout=15 ) if response.status_code == 404: break if response.status_code != 200: log_error(ERROR, "Authentik API returned unexpected status", status_code=response.status_code, body=response.text[:300]) break data = response.json() results = data.get('results', []) if not results: break all_logs.extend(results) next_value = data.get('next') if isinstance(next_value, str): url = next_value params = None else: url = None except Exception as e: log_error(ERROR, "Request to Authentik API failed", error=e) break # Разворачиваем, чтобы обрабатывать от старых к новым return all_logs[::-1] # --------------------------------------------------------------------------- # LOCAL FILE — пишем события как JSON Lines # --------------------------------------------------------------------------- def write_logs_to_file(logs: list[dict]): """ Дописывает события в EVENTS_FILE_PATH в формате newline-delimited JSON. Пропускается если WRITE_EVENTS_TO_FILE = False. """ if not logs or not WRITE_EVENTS_TO_FILE: return try: os.makedirs(os.path.dirname(EVENTS_FILE_PATH), exist_ok=True) with open(EVENTS_FILE_PATH, "a") as f: for event in logs: f.write(json.dumps(event, ensure_ascii=False) + "\n") except Exception as e: log_error(ERROR, "Could not write events to file", path=EVENTS_FILE_PATH, error=e) # --------------------------------------------------------------------------- # WAZUH — аутентификация и отправка событий # --------------------------------------------------------------------------- def wazuh_authenticate() -> dict | None: url_login = f"https://{WAZUH_URL}/security/user/authenticate" headers = { "Authorization": f'Basic {b64encode(f"{WAZUH_USER}:{WAZUH_PASSWORD}".encode()).decode()}', "Content-Type": "application/json" } try: response = requests.get( url_login, headers=headers, verify=WAZUH_VERIFY_SSL, timeout=10 ) if response.status_code == 200: return { "Authorization": f"Bearer {response.json()['data']['token']}", "Content-Type": "application/json" } log_error(ERROR, "Wazuh authentication failed", status_code=response.status_code, body=response.text[:300]) except Exception as e: log_error(ERROR, "Could not connect to Wazuh", url=url_login, error=e) return None def send_events_to_wazuh(logs: list[dict]): """ Отправляет события в Wazuh батчами по WAZUH_BATCH_SIZE штук. """ if not logs: return auth_headers = wazuh_authenticate() if not auth_headers: log_error(CRITICAL, "Aborting Wazuh send — authentication failed") return total = len(logs) for i in range(0, total, WAZUH_BATCH_SIZE): batch = logs[i:i + WAZUH_BATCH_SIZE] batch_num = i // WAZUH_BATCH_SIZE + 1 payload = {"events": [json.dumps(event, ensure_ascii=False) for event in batch]} try: response = requests.post( WAZUH_API_ENDPOINT, headers=auth_headers, json=payload, verify=WAZUH_VERIFY_SSL, timeout=15 ) if response.status_code == 200: print(f"[OK] Batch {batch_num}: {len(batch)} events sent to Wazuh.") else: log_error(ERROR, "Wazuh rejected batch", batch=batch_num, status_code=response.status_code, body=response.text[:300]) except Exception as e: log_error(ERROR, "Network error while sending batch to Wazuh", batch=batch_num, error=e) time.sleep(1) # не перегружаем API Wazuh # --------------------------------------------------------------------------- # MAIN # --------------------------------------------------------------------------- if __name__ == "__main__": started = datetime.datetime.now(timezone.utc) print(f"--- Run started: {started.strftime('%Y-%m-%dT%H:%M:%SZ')} ---") last_state = load_last_state() last_timestamp = last_state['timestamp'] last_event_pk = last_state['last_event_pk'] print(f"[INFO] Fetching events since: {last_timestamp} (last PK: {last_event_pk or 'None'})") fetched_logs = fetch_authentik_logs(last_timestamp) if not fetched_logs: print("[INFO] No new events found.") else: # Фильтруем события, которые уже были обработаны в прошлом запуске new_logs = [] if last_event_pk: last_pks_indices = [i for i, event in enumerate(fetched_logs) if event.get('pk') == last_event_pk] if last_pks_indices: start_index = last_pks_indices[-1] + 1 new_logs = fetched_logs[start_index:] else: new_logs = fetched_logs else: new_logs = fetched_logs if not new_logs: print("[INFO] No new events found after filtering.") else: print(f"[INFO] Events fetched: {len(fetched_logs)}, new events to process: {len(new_logs)}") # Тегируем каждое событие источником интеграции for event in new_logs: event['integration'] = 'authentik' write_logs_to_file(new_logs) if SEND_TO_WAZUH_API: send_events_to_wazuh(new_logs) # Сохраняем state по самому последнему из полученных событий newest_event = fetched_logs[-1] save_last_state(newest_event['created'], newest_event['pk']) print(f"[INFO] State updated. Last timestamp: {newest_event['created']}, PK: {newest_event['pk']}") elapsed = (datetime.datetime.now(timezone.utc) - started).total_seconds() print(f"--- Run finished in {elapsed:.2f}s ---")
Что нужно поправить перед запуском
В блоке конфигурации в начале файла:
Параметр | Что вставить |
|---|---|
| URL вашего Authentik, например |
| Токен, созданный в шаге 1 |
| FQDN вашего Wazuh API |
| Пользователь с доступом к |
| Путь,wazuh-agent будет читать события, если не хотите собирать через API, или для дублирования логов. |
| Куда писать ошибки скрипта |
| Куда сохранять состояние между запусками |
Запускаем скрипт по cron или systemd timer — раз в минуту или реже, в зависимости от интенсивности событий:
# Пример crontab: * * * * * /usr/bin/python3 /opt/scripts/authentik_audit_ver2.py >> /var/log/authentik/cron.log 2>&1
Шаг 4 — Правила Wazuh для событий Authentik
Это, пожалуй, самая трудоёмкая часть — написать правила, которые правильно детектируют каждый тип события. Я уже сделал это за вас. Правила покрывают 41 сценарий, разбиты на три секции и маппятся на MITRE ATT&CK, PCI DSS, GDPR, HIPAA и NIST.
Кладём файл в директорию правил (либо добавляем через GUI: Server management->Rules->Add new rules file):
cp authentik_custom.xml /var/ossec/etc/rules/ chown root:wazuh /var/ossec/etc/rules/authentik_custom.xml chmod 640 /var/ossec/etc/rules/authentik_custom.xml systemctl restart wazuh-manager
За основу правил взят action из документации: https://docs.goauthentik.io/sys-mgmt/events/event-actions/
Полный файл правил:
<!-- ============================================================================= Wazuh Detection Rules for Authentik Identity Provider ============================================================================= Author : pensecfort Channel : pensecfort Rule IDs: 110000 - 110040 Version : 1.3 ============================================================================= ID Map (sequential, no gaps): 110000-110024 Section 1 — Basic authentication events 110025-110036 Section 2 — Model: user accounts / apps / groups / flows / providers / outposts 110037-110040 Section 3 — Frequency-based and correlation rules --> <group name="authentik,"> <!-- ======================================================================= --> <!-- SECTION 1: BASIC AUTHENTICATION EVENTS (110000-110024) --> <!-- ======================================================================= --> <rule id="110000" level="3"> <field name="integration">authentik</field> <action>login</action> <description>Authentik: Successful user login</description> <mitre> <id>T1078</id> </mitre> <group>authentication_success,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.b,nist_800_53_AU.14.1,tsc_CC6.1,</group> </rule> <rule id="110001" level="5"> <field name="integration">authentik</field> <action>login_failed</action> <description>Authentik: Failed login attempt</description> <mitre> <id>T1078</id> <id>T1110</id> </mitre> <group>authentication_failures,pci_dss_10.2.4,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.b,hipaa_164.312.d,nist_800_53_AU.14.1,nist_800_53_AC.7,tsc_CC6.1,tsc_CC7.2,</group> </rule> <rule id="110002" level="3"> <field name="integration">authentik</field> <action>logout</action> <description>Authentik: User logged out of the system</description> <mitre> <id>T1078</id> </mitre> <group>pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.b,nist_800_53_AU.14.1,tsc_CC6.1,</group> </rule> <rule id="110003" level="5"> <field name="integration">authentik</field> <action>user_write</action> <description>Authentik: User data modified in flow</description> <mitre> <id>T1098</id> </mitre> <group>pci_dss_8.1.2,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.b,hipaa_164.312.a,nist_800_53_AC.2,nist_800_53_AU.2,tsc_CC6.3,</group> </rule> <rule id="110004" level="10"> <field name="integration">authentik</field> <action>suspicious_request</action> <description>Authentik: Suspicious request (e.g. revoked token)</description> <mitre> <id>T1550</id> <id>T1078</id> </mitre> <group>pci_dss_10.6.1,pci_dss_6.4.1,gdpr_IV_32.2,hipaa_164.308.a.1.ii,hipaa_164.312.b,nist_800_53_SI.4,nist_800_53_AU.6,tsc_CC7.2,tsc_CC7.3,</group> </rule> <rule id="110005" level="5"> <field name="integration">authentik</field> <action>password_set</action> <description>Authentik: User set a new password</description> <mitre> <id>T1098</id> </mitre> <group>pci_dss_8.3.6,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.d,hipaa_164.312.b,nist_800_53_IA.5,nist_800_53_AU.2,tsc_CC6.1,</group> </rule> <rule id="110006" level="8"> <field name="integration">authentik</field> <action>secret_view</action> <description>Authentik: Viewing of token or certificate</description> <mitre> <id>T1552</id> </mitre> <group>pci_dss_3.5.1,pci_dss_10.2.2,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.3,nist_800_53_AU.2,tsc_CC6.1,tsc_CC6.3,</group> </rule> <rule id="110007" level="3"> <field name="integration">authentik</field> <action>secret_rotate</action> <description>Authentik: Automatic token rotation</description> <mitre> <id>T1552</id> <id>T1098</id> </mitre> <group>pci_dss_8.6.1,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_IA.5,nist_800_53_AU.2,tsc_CC6.1,</group> </rule> <rule id="110008" level="5"> <field name="integration">authentik</field> <action>invitation_used</action> <description>Authentik: Invitation link used</description> <mitre> <id>T1078</id> </mitre> <group>pci_dss_8.2.1,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.2,nist_800_53_AU.2,tsc_CC6.1,</group> </rule> <rule id="110009" level="5"> <field name="integration">authentik</field> <action>authorize_application</action> <description>Authentik: User authorized access to the application</description> <mitre> <id>T1078</id> <id>T1550</id> </mitre> <group>pci_dss_7.2.1,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.3,nist_800_53_AU.2,tsc_CC6.3,tsc_CC6.8,</group> </rule> <rule id="110010" level="6"> <field name="integration">authentik</field> <action>source_linked</action> <description>Authentik: User linked an external identity source</description> <mitre> <id>T1098</id> </mitre> <group>pci_dss_8.2.1,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.2,nist_800_53_IA.8,tsc_CC6.3,</group> </rule> <rule id="110011" level="10"> <field name="integration">authentik</field> <action>impersonation_started</action> <description>Authentik: Administrator started user impersonation</description> <mitre> <id>T1078</id> <id>T1134</id> </mitre> <group>pci_dss_7.2.1,pci_dss_10.2.2,gdpr_IV_32.2,hipaa_164.308.a.1.ii,hipaa_164.312.b,nist_800_53_AC.3,nist_800_53_AU.2,tsc_CC6.3,tsc_CC7.2,</group> </rule> <rule id="110012" level="6"> <field name="integration">authentik</field> <action>impersonation_ended</action> <description>Authentik: Administrator ended user impersonation</description> <mitre> <id>T1078</id> <id>T1134</id> </mitre> <group>pci_dss_7.2.1,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.308.a.1.ii,hipaa_164.312.b,nist_800_53_AC.3,nist_800_53_AU.2,tsc_CC6.3,</group> </rule> <rule id="110013" level="3"> <field name="integration">authentik</field> <action>policy_execution</action> <description>Authentik: Policy execution (requires Execution logging enabled on policy)</description> <group>pci_dss_10.2.5,hipaa_164.312.b,nist_800_53_AU.2,tsc_CC7.2,</group> </rule> <rule id="110014" level="9"> <field name="integration">authentik</field> <action>policy_exception</action> <description>Authentik: Exception during policy execution</description> <group>pci_dss_6.4.1,gdpr_IV_32.2,hipaa_164.308.a.5,nist_800_53_SI.2,nist_800_53_AU.6,tsc_CC7.3,</group> </rule> <rule id="110015" level="7"> <field name="integration">authentik</field> <action>property_mapping_exception</action> <description>Authentik: Exception during property mapping execution</description> <group>pci_dss_6.4.1,gdpr_IV_32.2,hipaa_164.308.a.5,nist_800_53_SI.2,tsc_CC7.3,</group> </rule> <rule id="110016" level="9"> <field name="integration">authentik</field> <action>system_task_exception</action> <description>Authentik: Exception in system task</description> <group>pci_dss_6.4.1,gdpr_IV_32.2,hipaa_164.308.a.5,nist_800_53_SI.2,nist_800_53_IR.6,tsc_CC7.3,</group> </rule> <rule id="110017" level="9"> <field name="integration">authentik</field> <action>system_exception</action> <description>Authentik: General system exception</description> <group>pci_dss_6.4.1,gdpr_IV_32.2,hipaa_164.308.a.5,nist_800_53_SI.2,nist_800_53_IR.6,tsc_CC7.3,</group> </rule> <rule id="110018" level="9"> <field name="integration">authentik</field> <action>configuration_error</action> <description>Authentik: Configuration error (e.g. during application authorization)</description> <mitre> <id>T1562</id> </mitre> <group>pci_dss_6.4.1,pci_dss_2.2.1,gdpr_IV_32.2,hipaa_164.308.a.5,nist_800_53_CM.6,nist_800_53_SI.2,tsc_CC7.3,</group> </rule> <!-- Generic audit rules — refined in Section 2 below --> <rule id="110019" level="8"> <field name="integration">authentik</field> <action>model_created</action> <description>Authentik: Object creation in the system (audit)</description> <mitre> <id>T1098</id> <id>T1136</id> </mitre> <group>pci_dss_10.2.5,pci_dss_7.2.1,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.2,nist_800_53_AU.2,tsc_CC6.3,tsc_CC8.1,</group> </rule> <rule id="110020" level="8"> <field name="integration">authentik</field> <action>model_updated</action> <description>Authentik: Object modification in the system (audit)</description> <mitre> <id>T1098</id> </mitre> <group>pci_dss_10.2.5,pci_dss_10.6.1,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.2,nist_800_53_AU.2,tsc_CC6.3,tsc_CC8.1,</group> </rule> <rule id="110021" level="8"> <field name="integration">authentik</field> <action>model_deleted</action> <description>Authentik: Object deletion from the system (audit)</description> <mitre> <id>T1531</id> <id>T1070</id> </mitre> <group>pci_dss_10.2.5,pci_dss_10.6.1,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.2,nist_800_53_AU.2,tsc_CC6.3,tsc_CC8.1,</group> </rule> <rule id="110022" level="3"> <field name="integration">authentik</field> <action>email_sent</action> <description>Authentik: System email sent</description> <group>pci_dss_10.2.5,hipaa_164.312.b,nist_800_53_AU.2,tsc_CC6.1,</group> </rule> <rule id="110023" level="3"> <field name="integration">authentik</field> <action>update_available</action> <description>Authentik: System update available</description> <group>nist_800_53_SI.2,tsc_CC7.1,</group> </rule> <rule id="110024" level="8"> <field name="integration">authentik</field> <action>export_ready</action> <description>Authentik: Data export generated and ready</description> <mitre> <id>T1048</id> <id>T1567</id> </mitre> <group>pci_dss_10.2.5,pci_dss_3.5.1,gdpr_IV_32.2,hipaa_164.312.a,hipaa_164.312.b,nist_800_53_AC.3,nist_800_53_AU.2,tsc_CC6.3,</group> </rule> <!-- ======================================================================= --> <!-- SECTION 2: SPECIFIC MODEL EVENTS (110025-110036) --> <!-- Refine generic rules 110019-110021 for specific object types. --> <!-- Both generic and specific rules fire for the same event (expected). --> <!-- ======================================================================= --> <!-- User accounts (110025-110027) --> <rule id="110025" level="9"> <field name="integration">authentik</field> <action>model_created</action> <field name="context.model.model_name">user</field> <description>Authentik: New user account created</description> <mitre> <id>T1136</id> </mitre> <group>pci_dss_8.2.1,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.2,tsc_CC6.3,</group> </rule> <rule id="110026" level="9"> <field name="integration">authentik</field> <action>model_updated</action> <field name="context.model.model_name">user</field> <description>Authentik: User account modified (possible privilege change)</description> <mitre> <id>T1098</id> <id>T1078.003</id> </mitre> <group>pci_dss_7.2.1,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.2,tsc_CC6.3,</group> </rule> <rule id="110027" level="9"> <field name="integration">authentik</field> <action>model_deleted</action> <field name="context.model.model_name">user</field> <description>Authentik: User account deleted</description> <mitre> <id>T1531</id> </mitre> <group>pci_dss_8.1.4,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.2,tsc_CC6.3,</group> </rule> <!-- OAuth/OIDC applications (110028-110029) --> <rule id="110028" level="9"> <field name="integration">authentik</field> <action>model_created</action> <field name="context.model.model_name">application</field> <description>Authentik: New OAuth/OIDC application created</description> <mitre> <id>T1136</id> <id>T1550</id> </mitre> <group>pci_dss_7.2.1,pci_dss_10.2.5,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.2,tsc_CC6.3,</group> </rule> <rule id="110029" level="10"> <field name="integration">authentik</field> <action>model_updated</action> <field name="context.model.model_name">application</field> <description>Authentik: OAuth application configuration modified (verify redirect URIs)</description> <mitre> <id>T1550</id> <id>T1190</id> </mitre> <group>pci_dss_7.2.1,pci_dss_10.6.1,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.3,tsc_CC6.3,</group> </rule> <!-- Groups (110030) --> <rule id="110030" level="9"> <field name="integration">authentik</field> <action>model_updated</action> <field name="context.model.model_name">group</field> <description>Authentik: Group membership changed (possible privilege escalation)</description> <mitre> <id>T1098</id> <id>T1078.003</id> </mitre> <group>pci_dss_7.2.1,pci_dss_8.1.2,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.2,tsc_CC6.3,</group> </rule> <!-- Property mappings — token claims injection risk (110031) --> <rule id="110031" level="10"> <field name="integration">authentik</field> <action>model_updated</action> <field name="context.model.model_name">propertymapping</field> <description>Authentik: Property mapping modified (possible malicious claims injection)</description> <mitre> <id>T1098</id> <id>T1556</id> </mitre> <group>pci_dss_7.2.1,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.2,tsc_CC6.3,</group> </rule> <!-- Outpost lifecycle (110032-110033) --> <rule id="110032" level="8"> <field name="integration">authentik</field> <action>model_created</action> <field name="context.model.model_name">outpost</field> <description>Authentik: New outpost created/registered</description> <mitre> <id>T1136</id> </mitre> <group>pci_dss_10.2.5,nist_800_53_CM.2,tsc_CC8.1,</group> </rule> <rule id="110033" level="11"> <field name="integration">authentik</field> <action>model_deleted</action> <field name="context.model.model_name">outpost</field> <description>Authentik: Outpost deleted - authentication proxy may be unavailable</description> <mitre> <id>T1562</id> <id>T1531</id> </mitre> <group>pci_dss_10.6.1,nist_800_53_SI.4,tsc_CC7.2,</group> </rule> <!-- Authentication sources — LDAP, SAML, OAuth, etc. (110034) --> <rule id="110034" level="10"> <field name="integration">authentik</field> <action>model_deleted</action> <field name="context.model.model_name" type="pcre2">(ldap|saml|oauth2|plex|discord)</field> <description>Authentik: Authentication source deleted</description> <mitre> <id>T1562</id> <id>T1531</id> </mitre> <group>pci_dss_10.6.1,gdpr_IV_32.2,nist_800_53_CM.6,tsc_CC7.3,</group> </rule> <!-- Authentication/enrollment flows (110035) --> <rule id="110035" level="11"> <field name="integration">authentik</field> <action>model_updated</action> <field name="context.model.model_name">flow</field> <description>Authentik: Authentication/enrollment flow modified</description> <mitre> <id>T1556</id> <id>T1562</id> </mitre> <group>pci_dss_6.4.1,gdpr_IV_32.2,hipaa_164.308.a.5,nist_800_53_CM.6,tsc_CC7.3,</group> </rule> <!-- Authentication providers — LDAP, SAML, OAuth2, OIDC (110036) --> <rule id="110036" level="9"> <field name="integration">authentik</field> <action>model_updated</action> <field name="context.model.model_name" type="pcre2">(ldap|saml|oauth2|openidconnect)</field> <description>Authentik: Critical authentication provider configuration modified</description> <mitre> <id>T1562</id> <id>T1098</id> </mitre> <group>pci_dss_7.2.1,pci_dss_10.6.1,gdpr_IV_32.2,nist_800_53_CM.6,tsc_CC7.3,</group> </rule> <!-- ======================================================================= --> <!-- SECTION 3: FREQUENCY-BASED AND CORRELATION RULES (110037-110040) --> <!-- ======================================================================= --> <!-- 110037: Brute force — 5 failed logins from same IP within 60s --> <rule id="110037" level="10" frequency="5" timeframe="60"> <if_matched_sid>110001</if_matched_sid> <same_field>client_ip</same_field> <description>Authentik: Brute force detected (5 failed logins from same IP)</description> <mitre> <id>T1110.001</id> </mitre> <group>authentication_failures,pci_dss_10.2.4,nist_800_53_AC.7,tsc_CC7.2,</group> </rule> <!-- 110038: Password spraying — multiple users targeted from same IP --> <rule id="110038" level="12" frequency="5" timeframe="120"> <if_matched_sid>110001</if_matched_sid> <same_field>client_ip</same_field> <different_field>user.username</different_field> <description>Authentik: Password spraying detected (multiple users, same IP)</description> <mitre> <id>T1110.003</id> </mitre> <group>authentication_failures,pci_dss_10.2.4,nist_800_53_AC.7,tsc_CC7.2,</group> </rule> <!-- 110039: Token harvesting — 3+ secret views by same user within 60s --> <rule id="110039" level="12" frequency="3" timeframe="60"> <if_matched_sid>110006</if_matched_sid> <same_field>user.username</same_field> <description>Authentik: Possible token harvesting (multiple secret views by same user)</description> <mitre> <id>T1552</id> </mitre> <group>pci_dss_3.5.1,gdpr_IV_32.2,hipaa_164.312.a,nist_800_53_AC.3,tsc_CC6.1,</group> </rule> <!-- 110040: OAuth phishing — user authorizes 5+ different apps within 60s --> <rule id="110040" level="10" frequency="5" timeframe="60"> <if_matched_sid>110009</if_matched_sid> <same_field>user.username</same_field> <different_field>app</different_field> <description>Authentik: User authorized multiple different apps rapidly (possible OAuth phishing)</description> <mitre> <id>T1550.001</id> <id>T1566</id> </mitre> <group>pci_dss_7.2.1,nist_800_53_AC.3,tsc_CC6.8,</group> </rule> </group>
Всё аккуратно маппируется на MITRE ATT&CK, PCI DSS, GDPR и NIST — так что compliance-отчёты тоже будут красивыми.
🏁 Итог — что мы сделали
Давайте подведём итог.
Мы взяли Wazuh — мощный инструмент мониторинга безопасности — и интегрировали его в единую систему управления доступом на базе Authentik.
Теперь в вашей инфраструктуре:
✅ Единый вход. Один аккаунт в Authentik даёт доступ в Wazuh и все остальные подключённые сервисы. Никаких отдельных паролей, никакого хаоса с учётками.
✅ Обязательный 2FA. Каждый вход в дашборд требует второй фактор. Угнанный пароль сам по себе больше ничего не даёт.
✅ Централизованный контроль доступа. Новый сотрудник — один аккаунт в Authentik. Уволился — заблокировали одну учётку и он потерял доступ ко всему сразу.
✅ RBAC по проектам. Разработчики видят только свои логи, аналитики — только алерты, администраторы — всё. Wazuh очень гибок в этом плане, и если статья наберёт 100 лайков — сделаю отдельный подробный разбор: как настраивать доступ вплоть до конкретного агента.
✅ Мониторинг самой системы доступа. Логи Authentik теперь в Wazuh. Вы видите каждый вход, каждую попытку и каждое изменение в правах. Ваша система безопасности следит в том числе за собой.
До следующего материала! 🚀
