Преминете към основното съдържание

Преглед на сигурността (издание 1.0)

Информация

Тази страница е записът от прегледа във вида, в който е в docs/security-review.md. Повтаряйте прегледа при всяко минорно издание, както и при всяка промяна в автентикацията, в изпълнителя на задачи в агента или в разгръщането.

Прегледът на сигурността от фаза 6 на цялата система: бекенда, портала, агента, конектора и продукционното разгръщане. Всяка точка е проверена спрямо кода, а не спрямо проектните документи. Доказателствата съдържат препратки към файлове и тестовете, които гарантират, че всяко свойство остава в сила. Корекциите, направени по време на прегледа, са отбелязани с коригирано в 1.0.

Прегледано на 2026-09-24 спрямо main (фаза 6). Автоматизираните проверки са изброени в края. Повтаряйте този преглед при всяко минорно издание, както и при всяка промяна в автентикацията, в изпълнителя на задачи в агента или в разгръщането.

Моделът на заплахите в един абзац​

Една инсталация обслужва много тенанти (клиенти на MSP). Нападателите, които разглеждаме:

  • потребител от един тенант, който иска данни или контрол в друг тенант;
  • по-ниска роля (наблюдател или техник), която иска повече, отколкото ролята ѝ дава;
  • всеки в интернет, който може да достигне до портала, крайните точки за агенти и конектори и хоста за файлове;
  • обикновен (без администраторски права) потребител на Windows на управляван компютър, който иска да получи SYSTEM чрез агента;
  • някой, който се сдобие с копие на базата данни, резервно копие, файл с журнал или URL адрес.

Следните са доверени по замисъл:

  • агентът работи като SYSTEM и изпълнява това, което сървърът изпраща;
  • мениджърите на пакети и авторите на скриптове в даден тенант могат да изпълняват произволен код на устройствата на този тенант;
  • глобалният администратор контролира всичко.

Контролен списък​

#ТочкаСъстояние
1Флагове на бисквитката на сесиятаOK (в Hub от фаза 7)
2CSP и заглавки за сигурностOK, коригирано в 1.0 (хост за файлове)
3CORS произходи от конфигурациятаOK, коригирано в 1.0 (валидиране)
4Списък с разрешени типове/размери при качванеOK, коригирано в 1.0
5Path traversal в ключовете на обектиOK
6SSRFOK
7LDAP инжектиранеOK
8Инжектиране на команди (агент/конектор)OK, коригирано в 1.0 (3 находки)
9Архивни/MSI бомби, ограничения на размераOK, коригирано в 1.0 (хеш на MSI при push)
10Тайни в журналитеOK, коригирано в 1.0
11Отмяна на ключа на агента при извеждане от експлоатацияOK, коригирано в 1.0 (други реплики, спиране)
12Brute force атака срещу токени за регистриранеOK
13Проверки на произхода при WebSocketOK
14Изтичане на SSE токениOK, коригирано в 1.0 (POST /auth/sse-token)
15Изолация на тенантите (authz + RLS)OK
16Идентификационни данни, токени, ротация на ключовеOK
17Верига на доставки (скенери, фиксиране на версии, SBOM)OK
  • От фаза 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 средни находки са коригирани; оставащите ниски рискове са изброени по-горе
Одобрение от поддържащияпредстои