Перейти к содержанию

Безопасность

Этот документ описывает модель безопасности Umbryx: что защищено и как. Это канонический справочник для операторов, разворачивающих платформу, и для всех, кто проверяет код.


1. Что защищает Umbryx

Umbryx хранит операционные данные активного пентеста или red-team engagement. Защищаемая поверхность:

Актив Почему это важно Где живёт
Креды (пароли, NTLM-хэши, Kerberos-тикеты, SSH-ключи) Прямой аутентификационный материал к системам заказчика таблица creds, зашифровано ключом уровня проекта
Заметки с конфиденциальными тегами Свободный текст с нераскрытыми находками, именами жертв, деталями операций notes.content при теге ∈ {confidential, secret, sensitive, opsec, restricted}, зашифровано
Текстовый loot (извлечённые хэши, секреты, фрагменты конфигов) Тот же класс риска, что и учётные данные loots.value для не-файловых артефактов, зашифровано
C2-материал оператора (токены Adaptix, apitoken Mythic, operator-config JSON Sliver) Кто владеет ими — управляет живой атакующей инфраструктурой global_settings.c2_integrations, зашифрованные поля token+password
Загруженные файлы-артефакты (дампы NTDS, ZIP-архивы, скриншоты) Часто содержат самый убойный сырой материал data/uploads/, Fernet-шифрование на файловой системе
Findings, инвентарь хостов, карты сетей Агрегированная картина engagement; ценна любому, кто его реконструирует PostgreSQL, plaintext
Аудит-журнал (timeline_events с entity="audit") Forensic-свидетельство «кто что видел» PostgreSQL + HMAC-отпечаток на строку + append-only JSONL-зеркало

2. От кого защищает Umbryx

Модель предполагает развёртывание в доверенной внутренней сети. Защита рассчитана на:

  • Любопытных или малопривилегированных пользователей платформы. viewer не видит паролей в открытом виде. auditor не видит учётных данных вообще. Права применяются и на REST-эндпоинтах, и на слое WebSocket-broadcast (фильтрация по получателю + редакция полей).
  • Межпроектные утечки. Участник проекта A, не состоящий в проекте B, не может листать, читать и искать данные проекта B. Тот же гейт покрывает WebSocket-подписки.
  • Поверхностный доступ к БД at rest. Снапшота volume PostgreSQL мало для паролей — они зашифрованы per-project ключами (см. §3).
  • Забытый audit-trail. Когда секреты расшифровываются для использования (reveal учётной записи, панель host actions, bulk exec, C2 exec, экспорт проекта), в timeline пишется событие audit/... с актором, id учётной записи и контекстом (secret_revealed, secret_used_bulk_exec, secret_used_c2_exec, export_with_secrets и смежные — см. §5).

Обязанности по развёртыванию, которые ваши, а не платформы: TLS и IP-ограничения на reverse proxy, дисковое шифрование уровня хоста, гигиена рабочих мест операторов.


3. Шифрование

Алгоритм

Симметричное аутентифицированное шифрование через cryptography.fernet.Fernet:

  • AES-128 в режиме CBC для конфиденциальности
  • HMAC-SHA256 по шифртексту для целостности
  • Случайный IV на каждое шифрование

Реализация: backend/app/core/crypto.py (глобальный ключ) и backend/app/core/key_management.py (per-project ключи).

Соглашения хранения шифртекстов: значения под глобальным ключом имеют префикс __enc__:; под per-project DEK — префикс __pdek__:. Legacy-plaintext проходит через расшифровку без изменений, а значение, выглядящее зашифрованным, но не расшифровывающееся, вызывает ошибку — а не молча возвращается как будто это plaintext.

Что шифруется

Поле Шифруется? Примечания
creds.secret ✅ пароль / NTLM / тикет / SSH-ключ; per-project DEK
notes.content ✅ условно только при теге из
loots.value ✅ для не-файловых текстовое поле типов «hash» / «secret» / «text»
Загруженные loots-файлы ✅ Fernet на диске (строки file_encrypted=TRUE)
global_settings.c2_integrations[].token и .password ✅ per-integration, глобальный ключ
Всё остальное (hosts, findings, активности, jobs, карта, timeline и т.д.) ❌ считается операционно необходимым в plaintext

Если ваша БД старше миграции шифрования loot-файлов — прогоните миграции и перезалейте/переимпортируйте затронутые артефакты, чтобы всё в data/uploads/ лежало зашифрованным.

Иерархия ключей

  • Глобальный ключ (KEK) разрешается в порядке:
    1. переменная ENCRYPTION_KEY (обязательна в production — бэкенд отказывается генерировать ключ автоматически при APP_ENV не dev/test, а невалидный формат ключа — ошибка старта, не молчаливая подмена)
    2. /data/secret.key на диске (chmod 0600), используется в dev/test
  • Per-project DEK оборачивают секреты учётных данных: у каждого проекта свой data-encryption key, зашифрованный глобальным (таблица project_encryption_keys). Ротация DEK проекта (POST /api/system/encryption/rotate-dek) переоборачивает учётные данные этого проекта, не трогая чужие.

Сильное управление ключами требует env-переменной: подключите ключ через secret-менеджер (Vault, sops, Kubernetes Secret) и не позволяйте ему осесть на data-volume.

JWT-секрет отделён от ключа шифрования:

  • JWT_SECRET env (ключ подписи HS256)
  • Если не задан или слаб — генерируется и сохраняется в /data/.jwt_secret
  • Если /data недоступен для записи, старт продолжается, но сгенерированный секрет не персистится; следующий ребилд/рестарт может инвалидировать сессии. Задавайте JWT_SECRET явно в постоянных деплоях.

4. RBAC

Два слоя: глобальная роль у каждого пользователя и проектная роль в каждом членстве. Полные таблицы ролей и матрица прав — в Контроле доступа.

  • Глобальные роли: platform_admin, user, viewer (legacy глобальная read-only роль — предпочитайте user + проектное членство viewer).
  • Проектные роли: 11 ролей от owner до auditor, ключом на ROLE_PERMISSIONS в backend/app/core/permissions.py.
  • Security-значимые различия: credentials.read_secret есть только у owner, admin, editor и operator; findings.approve — только у reviewer. Остальные роли видят версию с сокрытием секретов или read-only.

Точки enforcement

Один и тот же гейт применяется в трёх местах; обход одного не обходит остальные.

  1. Хендлеры REST-эндпоинтов вызывают check_pid_access(db, pid, user, "<permission>") (или check_object_access / user_has_permission). Отказ — 403, не-участник — 404.
  2. WebSocket-broadcast (backend/app/ws.py) фильтрует по получателю согласно _ENTITY_POLICY. Нет права read на сущность — событие отбрасывается. Есть read, но нет специального права (например, credentials.read_secret) — чувствительное поле заменяется на "" в payload конкретного получателя. Глобальные админы — мимо фильтра.
  3. Зависимость require_admin на уровне FastAPI для эндпоинтов без проектного контекста (CRUD пользователей, тумблеры модулей, глобальный CRUD C2).

Аудит доступа администратора

Когда platform_admin обращается к проектному эндпоинту без членства в проекте (полный bypass), в timeline_events проекта пишется событие audit/admin_bypass_access. Запись:

  • видна участникам проекта в timeline
  • коммитится немедленно, независимо от транзакции основного запроса
  • содержит: username актора, user ID и проверяемое право (permission)

Доступ через членство (админ, одновременно owner/editor) этим механизмом не аудируется — он следует обычным правилам ролей.

Проектно-скоупленные C2-интеграции

owner проекта может зарегистрировать свою C2-интеграцию (Adaptix / Mythic / Sliver), привязанную к своим проектам. Ограничения:

  • Не-админы обязаны указывать project_ids; интеграция без project_ids (глобальная) создаётся/редактируется/удаляется только админом.
  • Не-админ не может расширить project_ids чужим проектом или опустошить список (это повысило бы интеграцию до глобальной).
  • Видимость (list): админы видят все; участники проектов — интеграции своих проектов.

5. Аудит-журнал

Аудит-события живут в той же таблице timeline_events, что и обычные события проекта, с маркером entity = "audit". Покрытие:

Событие Когда
secret_revealed Пользователь с credentials.read_secret раскрывает секрет через GET /api/creds/{cid}/secret (rate-limited)
read_credential_secrets Панель host actions раскрывает учётные данные проекта, привязанные к хосту
secret_used_bulk_exec Кред привязывается к задаче bulk_exec
secret_used_validate Кред привязывается к задаче validate_cred
secret_used_c2_exec Кред подставляется в командную строку C2 exec
export_with_secrets Сгенерирован экспорт проекта, и у экспортёра есть credentials.read_secret
read_sensitive_loot Расшифрованы чувствительные значения loot (list-эндпоинты, включая глобальные admin/member-листинги)
read_confidential_notes Расшифровано конфиденциальное содержимое заметки
download_sensitive_file Скачан loot-файл или вложение заметки через /api/uploads/...
webhook_token_regenerated Ротирован webhook-токен проекта
admin_bypass_access Глобальный админ обратился к проектному эндпоинту без членства

Аудит-события видны в timeline проекта; они не удаляются при удалении сущностей — след переживает чистку.

Чего в аудите нет:

  • доставки WebSocket-событий
  • read-эндпоинтов, отдающих только метаданные (например, список хостов)
  • неудачных попыток доступа (403 / 404) — они только в логах приложения

Усиление аудита

Каждый вызов log_event() использует три независимых канала:

Канал Где Поддаётся tamper-детекту Immutable по умолчанию
Строка БД (timeline_events) PostgreSQL ✅ через поле integrity (HMAC) ❌ DB-админ может DELETE
JSONL-файл UPLOAD_ROOT/audit/timeline.jsonl (O_APPEND) — ❌ host-root может редактировать
Объект S3/Minio настроенный бакет, по объекту на событие — ✅ с Object Lock / WORM

HMAC-целостность: при заданном AUDIT_INTEGRITY_KEY каждое событие получает отпечаток sha256=<hex> по каноническим полям (id, pid, entity, action, label, ts). GET /api/admin/audit/verify проверяет все строки и отчитывается:

  • tampered — id событий, чей сохранённый хэш не сходится с пересчитанным
  • unverified — строки до введения отпечатков
  • file_only — id событий из JSONL, отсутствующие в БД (строка была удалена после записи)

Включение:

# добавить в .env:
AUDIT_INTEGRITY_KEY=$(openssl rand -hex 32)
AUDIT_LOG_DIR=/data/uploads/audit          # дефолт

# опционально WORM-форвард в S3/Minio:
AUDIT_S3_BUCKET=my-worm-bucket
AUDIT_S3_ENDPOINT_URL=https://minio.internal

6. AI-чат

В Umbryx есть опциональный AI-чат (POST /api/projects/{pid}/ai/chats/{cid}/messages и стриминговый вариант).

Какие данные уходят провайдеру LLM

При запросе чата системный промпт включает:

  • только счётчики сущностей проекта (хосты, учётные данные, находки, заметки, loot, scope, attack-шаги)
  • полную историю диалога, отправленную оператором в теле запроса

Чувствительные значения в системный промпт не входят — ни секреты учётных данных, ни содержимое заметок, ни файлы. AI-агент может вызывать инструменты (чтение хостов, находок, заметок, метаданных учётных данных) в рамках прав самого пользователя, поэтому в вопросах оператора и результатах вызовов могут появляться имена сущностей, hostname, IP и заголовки находок. Режим исходящей анонимизации (обратимая токенизация всего, что уходит провайдеру) может быть включена для чатов.

Приватность и opsec

  • Все сообщения и результаты инструментов передаются настроенному внешнему LLM-провайдеру (OpenAI-совместимый API). Провайдер может логировать/обучаться на них согласно своим условиям.
  • Не включайте имена заказчиков, нераскрытые уязвимости и прочие чувствительные детали в промпты, если договор engagement запрещает передачу третьим сторонам.
  • Для air-gapped и чувствительных проектов используйте локальную OpenAI-совместимую точку (Ollama, vLLM) в настройках провайдеров.

Kill switch

AI-модуль отключается админом через тумблер модуля (POST /api/modules/ai/disable).

Конфигурация

Провайдеры настраиваются в GET /api/ai/providers (ключи маскируются — видны последние 4 символа) и PUT /api/ai/providers/{id}. Считайте конфигурацию провайдеров чувствительной и ревьюйте текущую реализацию перед использованием внешних провайдеров на закрытых проектах.


7. HMAC-аутентификация вебхуков

Umbryx проверяет подписи HMAC-SHA256 входящих webhook-запросов (Sliver, Havoc, кастомные). Верификация fail-closed: если WEBHOOK_HMAC_SECRET не настроен на сервере, инжест отвергается целиком, а не принимается без подписи. Задайте секрет до включения вебхуков:

# сгенерировать 32-байтовый hex-секрет:
openssl rand -hex 32
# затем в .env:
WEBHOOK_HMAC_SECRET=<вывод>

С ним каждый входящий POST /api/webhooks/{token} обязан нести заголовок X-Hub-Signature-256: sha256=<hmac>, посчитанный по raw body. Запросы без валидного заголовка отвергаются с 403.

Эндпоинт GET /api/projects/{pid}/webhook возвращает "hmac_required": true/false — операторы могут проверить текущую конфигурацию.

Пункт чек-листа перед развёртыванием: WEBHOOK_HMAC_SECRET должен быть задан везде, где webhook-эндпоинт достижим с хостов, которыми вы не полностью управляете.


8. Ротация ENCRYPTION_KEY

Почему ротация опасна без инструмента миграции

Ключи Fernet не ротируются без перешифрования данных. Если обновить ENCRYPTION_KEY, не перешифровав все шифртексты, следующие данные станут навсегда нечитаемыми:

  • все секреты учётных данных
  • все конфиденциальные/ограниченные заметки
  • все чувствительные значения loot (хэши, секреты, тексты)
  • все загруженные loot-файлы с file_encrypted=True
  • токены и пароли C2-интеграций
  • per-project DEK (обёрнуты старым глобальным ключом)

Резервные копии со старым ключом также нечитаемы под новым без предварительной расшифровки.

Процедура ротации

Используйте CLI backend/scripts/rekey.py:

# 1. Сгенерировать новый Fernet-ключ
NEW_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
echo $NEW_KEY   # сохраните надёжно

# 2. Dry-run: посчитать затронутые строки без записи
OLD_KEY=<текущий> NEW_KEY=$NEW_KEY \
  python3 backend/scripts/rekey.py --dry-run

# 3. Если счётчики верны — сама миграция
#    (при остановленном API или в maintenance)
OLD_KEY=<текущий> NEW_KEY=$NEW_KEY \
  python3 backend/scripts/rekey.py

# 4. Обновите ENCRYPTION_KEY в окружении / secret-менеджере
# 5. Перезапустите контейнер бэкенда

Скрипт принимает и позиционные аргументы: python3 rekey.py <old_key> <new_key> [--dry-run]

Переменные окружения для подключения: DATABASE_URL (или DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD). UPLOAD_ROOT по умолчанию /data/uploads.

Что перешифровывает скрипт

Источник Колонка / поле
credentials secret (значения под глобальным ключом)
notes content (только строки с __enc__:)
loots value (только строки с __enc__:)
loots файлы на диске с file_encrypted=TRUE
global_settings c2_integrations[*].token и .password

Per-project DEK ротируются отдельно через POST /api/system/encryption/rotate-dek, который переоборачивает DEK проекта под (новый) глобальный ключ и перешифровывает учётные данные проекта. После глобального rekey ротируйте DEK каждого проекта (или включите этот шаг в план rekey) — иначе DEK-обёрнутые значения останутся нечитаемыми под новым глобальным ключом.

Сначала резервная копия

Всегда делайте дамп БД и копию data/uploads/ до rekey. Расшифровка Fernet неверным ключом кидает InvalidToken, и скрипт выйдет с ненулевым статусом — данные сохранятся, т.к. скрипт работает в одной транзакции.


9. Opsec уведомлений (Telegram / Slack)

Umbryx может отправлять алерты в Telegram и Slack через модуль уведомлений. Требуется исходящий HTTP/HTTPS к внешним хостам с машины бэкенд-контейнера.

Последствия для opsec

Риск Детали
Утечка трафика Содержимое алертов (заголовки находок, severity, имена хостов) уходит на api.telegram.org или hooks.slack.com. В air-gapped/закрытых сетях этот трафик виден мониторингу.
Логирование провайдером Внешние провайдеры могут логировать содержимое по своим условиям. Не включайте имена заказчиков и детали нераскрытых уязвимостей в заголовки, если договор запрещает передачу третьим лицам.
Утечка DNS Даже при HTTPS-прокси DNS-запрос api.telegram.org наблюдаем в локальной сети.
Конфликт с air-gap Без исходящего интернета доставка падает с ошибкой в логах — повторов в очереди нет.

Рекомендации

  • Для air-gapped и VPN-изолированных деплоев отключите модуль уведомлений (POST /api/modules/notifications/disable).
  • Используйте self-hosted relay (Mattermost, Matrix, локальный SMTP) и кастомную webhook-интеграцию Umbryx.
  • Считайте заголовки находок и имена хостов потенциально чувствительными — держите их opsec-безопасными.

10. Безопасность системы плагинов

Umbryx поддерживает серверные Python-плагины. Плагины исполняются in-process — загруженный плагин разделяет память бэкенда и может вызывать любой Python API. Контроли направлены против подмены supply-chain и случайных опасных паттернов, а не против решительно настроенного злоумышленника-админа.

AST-валидация при загрузке

Исходные модули, загружаемые через админ-интерфейс, сканируются на уровне AST перед сохранением:

Паттерн Решение
eval, exec, __import__, compile Жёсткий отказ 400 — загрузка запрещена
subprocess, socket, ctypes, multiprocessing Загрузка продолжается; warning в ответе и в логах

HMAC-подпись плагинов

При заданном PLUGIN_SIGNING_KEY:

  1. Админ вызывает POST /api/admin/modules/sign с исходником плагина. Эндпоинт гоняет ту же AST-валидацию и возвращает sha256=<hex> HMAC по содержимому.
  2. Админ загружает через POST /api/admin/modules/upload с заголовком X-Plugin-Signature.
  3. Эндпоинт загрузки проверяет заголовок перед сохранением.

При PLUGIN_REQUIRE_SIGNATURE=true (включено по умолчанию при наличии ключа) неподписанные загрузки отвергаются 403. Это исключает подмену файла между шагами подписи и загрузки.

Остаточный риск: эндпоинт подписи и БД на одном хосте. Host-root атакующий может подменить ключ. Подпись плагинов закрывает бытовую подмену supply-chain и гигиену CI/CD, а не админ-уровень противника.

Конфигурация

# .env
PLUGIN_SIGNING_KEY=$(openssl rand -hex 32)
PLUGIN_REQUIRE_SIGNATURE=true   # enforced (дефолт true)

11. Сообщение об уязвимости

Открывайте issue на GitHub для нечувствительных репортов. Для чувствительных раскрытий пишите мейнтейнеру лично (контакт — в README репозитория). Приложите:

  • версию с уязвимостью (git rev-parse HEAD или тег релиза)
  • минимальный воспроизводитель

Подтверждаем в течение 5 рабочих дней. Окно скоординированного раскрытия согласуется индивидуально.


12. Чек-лист перед развёртыванием

Прежде чем направлять реальные данные engagement на экземпляр Umbryx:

  • [ ] JWT_SECRET — длинное случайное значение через env / secret-менеджер
  • [ ] ENCRYPTION_KEY — так же (production не работает на автогенерированном или невалидном ключе)
  • [ ] DB_PASSWORD и ADMIN_PASSWORD ротированы со значений по умолчанию
  • [ ] Reverse proxy спереди (TLS, rate limit, IP allow-list)
  • [ ] /data/uploads/ на томе с шифрованием at rest (LUKS, облачное дисковое шифрование)
  • [ ] Стандартный admin отключён или его пароль ротирован после создания первого реального операторского аккаунта
  • [ ] Стратегия резервного копирования БД — включая ENCRYPTION_KEY, иначе резервные копии невосстановимы
  • [ ] Мониторинг timeline_events с entity="audit" на аномалии (массовые экспорты в нерабочее время, повторные чтения секретов)
  • [ ] WEBHOOK_HMAC_SECRET задан, если webhook-эндпоинт достижим с хостов вне вашего контроля (см. §7)
  • [ ] Процедура ротации ENCRYPTION_KEY документирована; backend/scripts/rekey.py доступен (см. §8)
  • [ ] Модуль уведомлений отключён или настроен на self-hosted relay в air-gapped/VPN-изоляции (см. §9)
  • [ ] AUDIT_INTEGRITY_KEY задан через env / secret-менеджер; после первого старта выполните GET /api/admin/audit/verify, чтобы убедиться, что HMAC применяется к новым событиям (см. §5)
  • [ ] AUDIT_S3_BUCKET с Object Lock, если нужна устойчивая к host-root неизменяемость аудита (см. §5)
  • [ ] PLUGIN_SIGNING_KEY и PLUGIN_REQUIRE_SIGNATURE=true при серверных плагинах в команде (см. §10)

2026-11-02: Вложения конфиденциальных заметок шифруются при хранении; их скачивание требует того же права credentials.read_secret, что и текст заметки.