Безопасность¶
Этот документ описывает модель безопасности 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) разрешается в порядке:
- переменная
ENCRYPTION_KEY(обязательна в production — бэкенд отказывается генерировать ключ автоматически приAPP_ENVне dev/test, а невалидный формат ключа — ошибка старта, не молчаливая подмена) /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_SECRETenv (ключ подписи 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¶
Один и тот же гейт применяется в трёх местах; обход одного не обходит остальные.
- Хендлеры REST-эндпоинтов вызывают
check_pid_access(db, pid, user, "<permission>")(илиcheck_object_access/user_has_permission). Отказ — 403, не-участник — 404. - WebSocket-broadcast (
backend/app/ws.py) фильтрует по получателю согласно_ENTITY_POLICY. Нет праваreadна сущность — событие отбрасывается. Естьread, но нет специального права (например,credentials.read_secret) — чувствительное поле заменяется на""в payload конкретного получателя. Глобальные админы — мимо фильтра. - Зависимость
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:
- Админ вызывает
POST /api/admin/modules/signс исходником плагина. Эндпоинт гоняет ту же AST-валидацию и возвращаетsha256=<hex>HMAC по содержимому. - Админ загружает через
POST /api/admin/modules/uploadс заголовкомX-Plugin-Signature. - Эндпоинт загрузки проверяет заголовок перед сохранением.
При 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, что и текст заметки.