Преглед на сигурността (издание 1.0)
Тази страница е записът от прегледа във вида, в който е в docs/security-review.md.
Повтаряйте прегледа при всяко минорно издание, както и при всяка промяна в
автентикацията, в изпълнителя на задачи в агента или в разгръщането.
Прегледът на сигурността от фаза 6 на цялата система: бекенда, портала, агента, конектора и продукционното разгръщане. Всяка точка е проверена спрямо кода, а не спрямо проектните документи. Доказателствата съдържат препратки към файлове и тестовете, които гарантират, че всяко свойство остава в сила. Корекциите, направени по време на прегледа, са отбелязани с коригирано в 1.0.
Прегледано на 2026-09-24 спрямо main (фаза 6). Автоматизираните проверки
са изброени в края. Повтаряйте този преглед при всяко минорно издание, както
и при всяка промяна в автентикацията, в изпълнителя на задачи в агента или в
разгръщането.
Моделът на заплахите в един абзац
Една инсталация обслужва много тенанти (клиенти на MSP). Нападателите, които разглеждаме:
- потребител от един тенант, който иска данни или контрол в друг тенант;
- по-ниска роля (наблюдател или техник), която иска повече, отколкото ролята ѝ дава;
- всеки в интернет, който може да достигне до портала, крайните точки за агенти и конектори и хоста за файлове;
- обикновен (без администраторски права) потребител на Windows на управляван компютър, който иска да получи SYSTEM чрез агента;
- някой, който се сдобие с копие на базата данни, резервно копие, файл с журнал или URL адрес.
Следните са доверени по замисъл:
- агентът работи като SYSTEM и изпълнява това, което сървърът изпраща;
- мениджърите на пакети и авторите на скриптове в даден тенант могат да изпълняват произволен код на устройствата на този тенант;
- глобалният администратор контролира всичко.
Контролен списък
| # | Точка | Състояние |
|---|---|---|
| 1 | Флагове на бисквитката на сесията | OK (в Hub от фаза 7) |
| 2 | CSP и заглавки за сигурност | OK, коригирано в 1.0 (хост за файлове) |
| 3 | CORS произходи от конфигурацията | OK, коригирано в 1.0 (валидиране) |
| 4 | Списък с разрешени типове/размери при качване | OK, коригирано в 1.0 |
| 5 | Path traversal в ключовете на обекти | OK |
| 6 | SSRF | OK |
| 7 | LDAP инжектиране | OK |
| 8 | Инжектиране на команди (агент/конектор) | OK, коригирано в 1.0 (3 находки) |
| 9 | Архивни/MSI бомби, ограничения на размера | OK, коригирано в 1.0 (хеш на MSI при push) |
| 10 | Тайни в журналите | OK, коригирано в 1.0 |
| 11 | Отмяна на ключа на агента при извеждане от експлоатация | OK, коригирано в 1.0 (други реплики, спиране) |
| 12 | Brute force атака срещу токени за регистриране | OK |
| 13 | Проверки на произхода при WebSocket | OK |
| 14 | Изтичане на SSE токени | OK, коригирано в 1.0 (POST /auth/sse-token) |
| 15 | Изолация на тенантите (authz + RLS) | OK |
| 16 | Идентификационни данни, токени, ротация на ключове | OK |
| 17 | Верига на доставки (скенери, фиксиране на версии, SBOM) | OK |
1. Бисквитка на сесията
- От фаза 7 единствената бисквитка на сесията е
platform_rtна Hub:HttpOnly,SameSite=Strict,Secureв продукционна среда, сPath=/api/platform/v1/auth(Hub API). Axis не задава бисквитки. - Продуктовите токени (5 мин) се държат в паметта на SPA приложението на Axis, никога в хранилището на браузъра.
2. CSP и заглавки
Порталът (deploy/Caddyfile) изпраща:
default-src 'self',script-src 'self'(SPA приложението няма вграден скрипт),style-src 'self' 'unsafe-inline'(вградени стилове на компонентите),img-src 'self' data:иconnect-src 'self' https://<files host>(качванията на пакети от браузъра отиват към предварително подписани URL адреси);frame-ancestors 'none',object-src 'none'иbase-uri 'self';- HSTS, nosniff,
X-Frame-Options: DENY, Referrer-Policy и Permissions-Policy.
Коригирано в 1.0: хостът за файлове (MinIO зад Caddy) вече изпраща
default-src 'none'; sandbox, а обектите винаги се съхраняват като
application/octet-stream (вижте т. 4). Качен файл никога не може да се
превърне в страница, която изпълнява скрипт.
Ако S3 не се обслужва на RMM_FILES_DOMAIN (външен S3), добавете тази
крайна точка в connect-src.
3. CORS
- Разрешените произходи идват само от
RMM_CORS_ORIGINSи идентификационни данни (credentials) се разрешават само за тях (middleware/cors.go). - Продукционният портал е на същия произход и не се нуждае от CORS.
- Коригирано в 1.0: конфигурацията се валидира при стартиране.
Заместващи символи (wildcards), пътища и стойности, различни от http(s),
се отказват (
config.validOrigin,TestLoadFromMap_Validation).
4. Качвания
Файлове на пакети:
- Разширенията са разрешени според вида:
.msi,.exe,.ps1. - Размерът е ограничен до 1 B–4 GiB.
- При финализиране съхраненият размер се сравнява с декларирания, след това
сървърът изчислява хеша на обекта и го сравнява със SHA-256 от клиента
(
deploy/packages.go).
Издания: ≤ 500 MiB, с размер, SHA-256 и подпис при публикуване. Качвания на изхода от скриптове: ограничени до 10 MiB.
Коригирано в 1.0:
- Предварително подписаните качвания винаги подписват
Content-Type: application/octet-stream, независимо какво декларира клиентът (storage/presign.go,TestPresignedPutRoundTrip). - Качване, което не премине проверката на размера или хеша, се изтрива
веднага (
discardUpload,TestPackageCRUDScopesAndValidation).
Известно ограничение:
- Предварително подписаният PUT не обвързва дължината. Прекалено голямо качване се отхвърля и изтрива при финализиране, но преди това се съхранява.
- Чернови, които никога не се финализират, запазват обекта си, докато пакетът не бъде изтрит.
5. Ключове на обекти
Ключовете се генерират от сървъра:
packages/<tenant|global>/<uuid>/<sanitized name>.SanitizeFileNameвзема базовото име след замяна на\с/, разрешава само[A-Za-z0-9._-]и премахва водещите точки.- Ключовете на изданията се получават от валидиран компонент и semver.
- Ключовете за изхода от скриптове са
OutputKey(tenant, run).
При финализиране ключът от клиента трябва да е равен на съхранения ключ. Изтеглянията винаги генерират предварително подписан адрес за съхранения ключ. Кешът на агента отхвърля невалидни имена и хешове, които не са в шестнадесетичен формат.
6. SSRF
Сървърът извлича само:
- индекса на winget от
RMM_WINGET_SOURCE_URL(конфигурация на оператора, с ограничен размер); - SMTP и S3 на конфигурираните крайни точки;
- собствения си
/readyz(проверка на състоянието).
Известията за аларми са само по имейл; няма webhooks. С LDAP хоста на конфигурацията за синхронизация се свързва локалният конектор, а не сървърът.
7. LDAP инжектиране
- Никаква потребителска стойност не се поставя в LDAP филтър. Филтърът за
компютри е
(&(objectCategory=computer)<computer_filter>). computer_filterпо замисъл се пише от администратора на тенанта. Той се проверява за балансирани скоби и дължина, така че да остане вътре във външния&(balancedFilterвentrosity-shared-go/proto/connector.go,entrosity-axis-connector/internal/ldap).- Ограничаването по OU се прилага от страна на клиента.
8. Инжектиране на команди
Налични мерки:
- Процесите се стартират с масиви от аргументи: winget, shutdown, обвивката за PowerShell скриптове (пътищата са в кавички, параметрите се подават като JSON и се разгъват чрез splatting) и свойствата на msiexec в конектора (метасимволите се отказват; командите за WinRM са константни).
- Аргументите за инсталиране и деинсталиране на пакети по замисъл са сурови командни редове. Те се пишат от мениджърите на пакети, които и без това могат да качат произволен изпълним файл.
Коригирано в 1.0:
- Деинсталиране от инвентаризацията (техник):
silent_argsсе добавяше към низа за деинсталиране и се изпълняваше чрезcmd.exe /cкато SYSTEM, така че/S & …изпълняваше произволна команда. Аргументи с& | < > ^ % ! "или нови редове вече се отказват (proto.CmdSafe,device.UninstallFromInventory,TestDeviceActions). - Деинсталиране на пакет по име от инвентаризацията (внедряване):
търсенето съвпадаше и със записи за деинсталиране на ниво потребител
(HKCU). Обикновен потребител можеше да постави такъв запис с име, което
имитира пакета, и с низ за деинсталиране, който изпълнява неговата
програма като SYSTEM. Сега се използват само записи за цялата машина
(
FindUninstallEntry,TestFindUninstallEntryIgnoresPerUserEntries). - cmd скриптове: низовите параметри стават
%RMM_PARAM_X%, които cmd разгъва, преди да анализира реда. Стойности с метасимволи на cmd вече се отказват заcmdскриптове (scripts.cmdSafeParams,TestCmdScriptParamsRejectMetacharacters). Параметрите за PowerShell са данни и не се ограничават.
Оставащ нисък риск: AddProvisionedAppx подава имена на файлове от
генерирани от сървъра ключове на обекти вътре в низ за -Command.
9. Ограничения на размера и цялост
Ограничения на размера:
- JSON тела в бекенда: 1 MiB.
- Инвентарни данни от агента: 32 MiB компресирани, 64 MiB декомпресирани.
- Изход от задачи: ограничен.
- AD порции от конектора: 16/32 MiB.
- Ограничение при четене от WebSocket: 1 MiB и от двете страни.
- Индекс на winget: ограничения за изтеглянето и за записите в zip архива.
Цялост:
- Изтеглянията на агента са с ограничен размер и хешът им се проверява, преди да бъдат преименувани или изпълнени. Кешът се използва само след маркер за успешна проверка.
- Самообновявания: подписан манифест (Ed25519), размер и SHA-256.
Коригирано в 1.0: MSI пакетът на агента, който конекторът инсталира
чрез push на компютрите в домейна (с идентификационни данни от домейна),
вече носи своя SHA-256, който конекторът проверява. Сървърът го изчислява
веднъж за всяка версия на обекта (adsync.msiHash, TestPushMSIHash). При
статичен RMM_AGENT_MSI_URL няма хеш; предпочитайте обектно хранилище.
Оставащ нисък риск: файловете за първоначално инсталиране на winget (пакетите на App Installer) се изтеглят без хеш. Windows проверява подписа на пакета им.
10. Тайни в журналите
- Журналът на заявките записва шаблона на маршрута в chi, никога суровия
път или низа на заявката (query string). Така токените в пътищата и
?sse_token=не попадат в журналите (TestLogger_NoSecretsFromURLs). - Коригирано в 1.0: журналът на процеса редактира атрибути, чийто ключ
назовава тайна (password, secret, token, authorization, cookie,
credential, keys). Идентификаторите и времената запазват стойностите си.
Стойностите
Bearer/Basicв грешки със свободен текст също се редактират (logging.redactAttr,TestSecretsAreRedacted). - Коригирано в 1.0: грешките при изтегляне в агента и конектора вече не
съдържат подписа на предварително подписания URL адрес
(
transport.RedactURL). Съобщение за задача, което не може да бъде доставено, се записва в журнала по вид и размер, а не със съдържанието си. - Одитните snapshot записи редактират ключовете на тайните (
audit.Redact).
11. Отмяна
- Извеждането на устройство от експлоатация отменя ключа на агента, отменя задачите му и затваря връзката му на която и да е реплика, която я държи.
- Модулите за автентикация на агенти и конектори отказват отменени ключове, изведени от експлоатация устройства и неактивни тенанти както при WebSocket, така и при HTTP polling.
Коригирано в 1.0:
- Всяка реплика изчиства кеширания си ключ при отмяна. Преди това другите реплики се доверяваха на кеширан ключ до 60 s.
- Спирането на тенант затваря всички негови връзки от агенти и конектори
на всички реплики и изчиства кешираните им ключове
(
Dispatcher.RevokeTenant,TestRevocationAcrossReplicas).
12. Токени за регистриране
- 32 случайни байта (base64url). Съхранява се само SHA-256, а търсенето е по хеш.
- Ограничение на честотата на заявките за IP адрес
(
RMM_ENROLL_RATE_PER_MINUTE). - Незадължителни срок на валидност, ограничение на използванията и отмяна.
- Brute force атака е неосъществима предвид ентропията.
13. Произход при WebSocket
- WebSocket връзките на агенти и конектори използват проверката на
произхода от библиотеката.
Originот браузър от друг сайт се отказва с 403; агентите не изпращат Origin (TestAgentWebSocketRejectsBrowserOrigins). - Порталът няма WebSocket; той използва SSE.
14. Токени за потоци от събития
Коригирано в 1.0: EventSource не може да изпраща заглавки и порталът
преди поставяше 15-минутния токен за достъп в ?access_token=. Сега той
извиква POST /auth/sse-token, който връща JWT със:
- собствена аудитория (
rmm-events); - 60-секунден живот;
- обвързване с един тенант и със сесията.
Токенът отваря само GET /tenants/{id}/events?sse_token= и само за този
тенант. Достъпът до тенанта се проверява отново при отваряне на потока.
?access_token= вече не се приема, а токенът за поток не е токен за достъп
(TestStreamTokens, events.stream.test.tsx).
15. Изолация на тенантите
Първи слой:
- Всеки маршрут в портала има правило за достъп (
portal/access.go). Маршрут без такова се отказва. - Маршрутите на тенанти разрешават
{tenantID}спрямо principal обекта. - 82 случая за достъп между тенанти подават идентификатори на B към
крайните точки на A в пътища, тела и филтри. Те очакват 404/422 и никаква
промяна в B (
TestAuthzFuzzCrossTenantIDs).
Втори слой: защита на ниво ред (row-level security) в PostgreSQL (миграция 0007).
- Сървърът се свързва като
rmm_app(NOBYPASSRLS). Всяка заявка се изпълнява с тенанта вapp.tenant_id. - Съставните външни ключове държат препратките в рамките на един тенант.
- Одитният журнал позволява само добавяне.
Бележка за дизайна: пътищата в кода без обхват на тенант (worker процеси, маршрути за глобални администратори, протоколът на агента преди автентикация) се изпълняват със заобикаляне на RLS. Те са покрити от първия слой и fuzz теста. Превръщането на „без обхват“ в отказ по подразбиране е кандидат за 1.x.
16. Идентификационни данни и ключове
- Паролите, TOTP тайните, ограниченията при влизане и сесиите от фаза 7 се
намират само в Entrosity Hub (миграция 0013 ги премахна от Axis): Hub
хешира паролите с argon2id (m=64 MiB, t=3, p=2), пази ограниченията при
влизане в PostgreSQL за всички реплики, шифрова TOTP тайните с
PLATFORM_MASTER_KEYи хешира кодовете за възстановяване и бисквитките на сесиите. - Собствените JWT на Axis са само токените за потоци от събития: HS256, една минута, с проверка на издател, аудитория, изтичане и iat.
- Ротация (ново в 1.0):
RMM_JWT_SECRET_OLDпроверява старите токени за потоци по време на ротация на JWT тайната.rmm-server rotate-master-keyшифрова наново всички тайни с новия главен ключ (TestRotateMasterKey).
- Сървърът отказва да стартира в продукционна среда с примерните тайни за разработка.
17. Верига на доставки
security.ymlсе изпълнява при всеки push и веднъж седмично:- govulncheck (компилации за Linux и Windows);
- gosec (изключенията са прегледани по-долу);
pnpm audit;- gitleaks (цялата история);
- Trivy върху образите (backend, web, Entrosity Hub) и върху
deploy/; - SPDX SBOM.
- Изданията сканират образите преди качването им и прилагат SBOM.
- Фиксиране на версии:
go.sum,pnpm-lock.yamlи тагове на образите. Caddy се компилира наново отdeploy/caddyс актуални зависимости, защото официалните образи изоставаха с корекциите на Go и gRPC. Go използва коригиранияtoolchain go1.26.8. - Dependabot покрива Go, npm, actions и Docker.
- Образите работят без root права: distroless
nonrootза бекенда; uid 10001 само сCAP_NET_BIND_SERVICEза Caddy.
18. Влизане в Entrosity Hub (фаза 7)
- Продуктови токени: само EdDSA (HS256 и
alg: noneсе отказват), познатkid, издателRMM_PLATFORM_URL, аудиторияrmm, безpurpose, изтичане с допуск от 30 s (entrosity-axis.backend/internal/platformauth,TestParseAccess). - Ролите никога не идват от токена; RBAC матрицата и fuzz тестът за достъп
между тенанти се изпълняват с продуктови токени от Hub (
TestRBACMatrix,TestAuthzFuzzCrossTenantIDs). В Axis не са останали маршрути за влизане (миграция 0013). - Step-up токени: обвързани с потребителя и сесията,
jtiсе записва вplatform_step_ups_used, така че всеки работи само веднъж на която и да е реплика (TestHubStepUp,TestVerifyStepUp). - Маршрутите с бисквитка на Hub отказват заявки от други сайтове
(
TestCookieRoutesRefuseCrossSiteRequests,TestProductTokenAndStepUp); повторната употреба при опресняване след гратисния прозорец прекратява сесията (TestSessionLifecycle). - Вътрешният API слуша отделно, отказва се на ръба на мрежата
(
/api/platform/internal/*→ 404 на всеки хост) и сравнява продуктовите токени за константно време. - Отмяна: сесиите, прекратени в Hub, достигат до Axis със следващия
snapshot (
TestSyncUnchangedAndRevokedSessions); деактивираните и премахнатите потребители и организации следват същия път (TestSyncLifecycle). Закъснение: интервалът на синхронизация плюс 30 s кеш на principal обектите. - Копието никога не изтрива окончателно (потребители →
deleted, тенанти →suspended), така че погрешна промяна в Hub не води до загуба на данни в Axis. - Hub има собствена матрица за достъп за всеки маршрут
(
entrosity-hub.backend/internal/http/rbac_matrix_integration_test.go).
Изключения в gosec:
- G115: преобразувания на цели числа за стойности, които са валидирани и ограничени.
- G204: работата на агента е да стартира програми; командните редове са покрити от т. 8.
- G304: агентът чете собствените си файлове със състояние и кеш.
- Симулаторите и тестовите fixtures (
agentsim,connectorsim,wingetfixture,testutil) са изключени; те не се доставят. - Отделните анотации
#nosecсъдържат причината си на същия ред.
Автоматизирани резултати от този преглед (2026-09-24)
| Проверка | Резултат |
|---|---|
| govulncheck, и четирите модула (toolchain go1.26.8) | Няма извиквани уязвимости (moby/go-archive е надграден до v0.3.0) |
| gosec с изключенията по-горе | 0 проблема |
pnpm audit | Няма известни уязвимости |
| gitleaks, цялата история | Няма изтичания (заместващите стойности за разработка са в списъка с разрешени в .gitleaks.toml) |
| Trivy, образ на бекенда | 0 HIGH/CRITICAL |
| Trivy, web образ | 0 HIGH/CRITICAL (след повторното компилиране на Caddy; стандартните образи caddy:2.10/2.11 имаха 59/17) |
Trivy config, deploy/ | 0 HIGH/CRITICAL (след стартиране на Caddy без root права) |
| syft SBOM | образ на бекенда – 90 пакета, портал – 554 компонента |
Одобрение
| Роля | Име | Дата | Резултат |
|---|---|---|---|
| Преглед и корекции | Claude (AI партньор по програмиране), с независима одитна проверка | 2026-09-24 | Всички точки OK; 2 високи и 3 средни находки са коригирани; оставащите ниски рискове са изброени по-горе |
| Одобрение от поддържащия | предстои |