Модель безопасности
Платформа исполняет недоверенные flake разработчиков и открывает окружения в сеть, поэтому границы ниже — это конструктивные ограничения, а не дополнительные меры по желанию. Каждую границу закрепляет интеграционный тест.
Модель доверия
Отправленный flake — это запрос, а не удостоверение. Оператор опознаётся по SSH-ключу, которым он подключается; содержимое flake не может выдать себе доступ. Значение имеют только ключи из реестра платформы (/etc/mev/sshkeys.json).
Границы безопасности
Вычисление недоверенного flake
mevd вычисляет входящий flake в изоляции:
nix eval --json .#mev \
--no-write-lock-file \
--pure-eval --restrict-eval \
--option allow-import-from-derivation false \
--option allow-unsafe-native-code-during-evaluation falseСверху — таймаут по часам (SIGKILL), RLIMIT_CPU и RLIMIT_AS как непривилегированные ограничения ресурсов. Нарушение любого из них завершается кодом 65 — до привилегированного согласования управление не доходит. Тест: checks.eval-cap.
Разделение привилегий
mev, mevd и mevd-apply работают без привилегий от пользователя mev: вычисление, валидация, авторизация и весь учёт (выделение id, реестры, фрагменты Traefik).
Root достигается только через два узких помощника sudo, повторно проверяющих свои входные данные:
mev-container—extra-container create/destroy. Пиннит<nixpkgs>при сборке и сам строит выражение вычисления из перепроверенного дескриптора, поэтому root никогда не вычисляет выбранный вызывающим nixpkgs или строку Nix.mev-data— ZFS-датасет / каталог данных. Leaf повторно проверяется доenv<id>, путь не может выйти заrpool/environments.
Есть и помощники только для чтения: mev-logs (фиксированный journalctl) и mev-stat (использование ZFS; только чтение, отдельно от mev-data, чтобы путь чтения не мог изменить том).
Активатор работает без привилегий от пользователя wake. Его единственная привилегированная потребность — systemctl start/stop юнитов окружений — закрыта узким правилом polkit: только субъект wake, только container@env<цифры>.service / microvm@env<цифры>.service, только команды start/stop. Тесты: checks.sudo-scope, checks.wake.
SSH-диспетчер
Единственный аккаунт proxy принимает все SSH-подключения к окружениям. sshd сопоставляет предложенный ключ с именем пользователя через AuthorizedKeysCommand по реестру и запускает env-ssh <user> как forced command: маршрут → авторизация → пробуждение → канал к sshd окружения.
Команда зарегистрирована как файл, принадлежащий root, вне /nix/store (sshd отклоняет AuthorizedKeysCommand, путь которой проходит через каталог, доступный на запись группе или прочим, а у /nix/store режим 1775). Тест: checks.env-ssh.
Внешняя сессия ограничена (restrict); проброс портов (-L/-R) и PTY согласуются уже во внутренней сессии по каналу.
Авторизация
База членства хранится на хосте (SQLite, /var/lib/mev/mev.db), а не берётся из входящего users[]. Менять список участников и удалять окружение вправе только владелец или администратор.
Создание разрешено только владельцу (или администратору). Обновление разрешено владельцу, администратору или любому из users — кроме изменения самого users[].
Опциональная защита requireSignedCommit отклоняет пуши без действительной SSH-подписи HEAD-коммита, ключ которой зарегистрирован в signingKeys оператора. Тесты: checks.authz, checks.signed-commit.
Сетевая изоляция окружений
Каждое окружение получает собственный /30 (RFC 6598). Правила nftables блокируют транзитный трафик между интерфейсами окружений (ve-* ↔ ve-*, mvm-env* ↔ mvm-env* и между бэкендами), оставляя открытым только исходящий NAT. Тест: checks.inter-env-isolation.
Дополнительная изоляция: backend = "microvm" даёт окружению собственное ядро.
Контроль допуска активатора
Объединение одинаковых запросов, общий лимит холодных стартов, бюджет свободной RAM и ограничение частоты по источнику сдерживают радиус поражения при наплыве запросов. Тест: checks.wake-admission.
Секреты
Никаких секретов в открытом виде в отслеживаемых файлах. Хостовые секреты зашифрованы через sops-nix и доходят до сервисов через шаблоны времени активации или чтение во время работы из config.sops.secrets.<name>.path — но никогда через Nix store.
Секреты окружений описаны в описании окружения. Кратко: каждое окружение владеет своей парой age-ключей на постоянном томе данных; закрытый ключ никогда не покидает хост. Открытый текст существует только в tmpfs работающего окружения (/run/mev-secrets) — не в store и не на диске в покое. Тест: checks.env-secrets.
Сообщения об уязвимостях
Сообщайте об уязвимостях на почту support@maximaster.ru. Не заводите публичные issue по проблемам безопасности. Укажите затронутую версию или коммит и способ воспроизведения.
Подробнее об архитектуре — в разделе Архитектура.