C4 Context — это взгляд с высоты птичьего полёта. Мы не видим классы, не видим модули — мы видим систему как чёрный ящик и задаём вопрос: кто с ней разговаривает и зачем?
%%{init: {'theme': 'neutral'}}%%
graph TB
admin["PKI Admin<br/><i>CLI: pki.py, ceremony scripts</i>"]
operator["Deploy Operator<br/><i>deploy.py → SSH/SCP</i>"]
client["PKI Client<br/><i>REST API consumer</i>"]
stm32["STM32 TRNG Device<br/><i>G431/G474/H750, USB HID</i>"]
subgraph box ["PKI-on-Box (RK3328 ARM64)"]
pki_core["PKI Core Service<br/><i>Python 3.6, Flask :5000<br/>CA, Certificates, CRL, OCSP</i>"]
hsm_svc["HSM Service<br/><i>TRNG→DRBG→CryptoEngine→KeyStorage<br/>DeviceAllow=/dev/hidraw0</i>"]
pki_core --- hsm_svc
end
admin -->|"CLI commands"| pki_core
client -->|"REST API :5000"| pki_core
operator -->|"SSH/SCP"| box
stm32 -->|"USB HID /dev/hidraw0"| hsm_svc
На диаграмме четыре сущности взаимодействуют с PKI-on-Box. Каждая из них пришла сюда своим путём, и у каждой — своя модель доверия.
PKI Admin — это единственный человек, который имеет физический доступ к устройству. Он работает через CLI (pki.py), и это осознанный выбор: церемония создания Root CA — слишком критичная операция для REST API. Один неправильный HTTP-запрос, один перехваченный ответ с приватным ключом — и вся PKI скомпрометирована.
CLI работает локально, через stdin/stdout. Никакого сетевого стека, никаких сериализаторов, никаких парсеров HTTP. Команда ca create-root --name "PKI-Box Root CA" --validity 20 — это 67 символов, которые создают корень доверия на 20 лет. Церемония (firstceremony2.py) добавляет к этому проверку здоровья TRNG, верификацию сертификата после создания и JSON-отчёт — но суть та же: человек, терминал, прямой доступ.
PKI Client — это любая система, которой нужен сертификат: веб-сервер, IoT-устройство, другой сервис. Он общается через REST API на порту 5000, и видит 12 endpoints. Для него PKI-on-Box — это просто HTTP-сервис, который принимает JSON и возвращает PEM.
Но за этой простотой скрывается важное архитектурное решение: REST API не имеет собственной аутентификации. Нет ни API-ключей, ни OAuth, ни mTLS на уровне приложения. Почему? Потому что аутентификация реализована на уровне ниже — SELinux ограничивает сетевой доступ к порту 5000, eBPF network filter пропускает только whitelist IP-адресов. Это defense in depth наоборот: вместо того чтобы добавлять слои аутентификации в приложение (где каждый слой — это код, а код — это баги), мы убираем сетевой доступ на уровне ядра.
Для PKI Client это прозрачно — он просто делает POST /api/v1/certs/server и получает сертификат. Он не знает, что его TCP-пакеты прошли через eBPF фильтр и что процесс, обрабатывающий его запрос, работает в SELinux домене pki_core_t.
Deploy Operator обновляет код на устройстве. Он не взаимодействует с PKI напрямую — он работает с deploy.py, который по SSH/SCP загружает новую версию приложения, перезапускает сервис и проверяет health check.
Почему отдельный актор, а не часть PKI Admin? Потому что разные модели угроз. PKI Admin имеет доступ к криптографическим операциям — он может создать CA, выпустить сертификат. Deploy Operator имеет доступ к файловой системе — он может заменить код. Это разные привилегии, и в идеале это разные люди (или как минимум разные SSH-ключи).
Deploy flow намеренно простой: backup → upload → restart → health check. Если health check не проходит — автоматический rollback. Никакого blue-green, никакого canary — на одноплатнике с 2GB RAM это было бы overengineering.
STM32 — единственный актор, который не принимает решений. Он не знает, что является частью PKI. Он просто генерирует случайные числа из теплового шума и отправляет их по USB HID. 64 байта каждые ~65 мс, бесконечно, пока есть питание.
Это самый важный актор в системе — и одновременно самый простой. Firmware на bare metal, без ОС, без файловой системы, без сетевого стека. Три платы поддерживаются (G474, G431, H750), но бинарь один и тот же для каждого семейства. Watchdog (IWDG) перезагружает MCU если main loop зависнет. При ошибке RNG — SOS-мигание светодиодом и halt. Никаких попыток восстановления, никаких fallback — если аппаратный генератор сломался, единственное честное действие — остановиться.
Связь с хостом — USB 2.0 Full Speed, Custom HID. Почему HID, а не CDC (виртуальный COM-порт)? Потому что HID не требует драйверов — ни на Linux, ни на Windows, ни на macOS. Подключил — /dev/hidraw0 появился — работает. Для embedded-устройства, которое должно работать годами без обслуживания, отсутствие зависимости от драйверов — это не удобство, это требование надёжности.
На Context-диаграмме есть одна граница — System_Boundary(box). Внутри неё два сервиса: PKI Core и HSM Service. Но настоящих границ доверия — три:
┌─────────────────────────────────────────────────────────┐
│ Граница 1: Физическая │
│ RK3328 + STM32 в одном корпусе, USB кабель внутри │
│ Атакующий без физического доступа не может: │
│ - подменить TRNG устройство │
│ - прочитать ключи с диска │
│ - подключить отладчик к STM32 │
├─────────────────────────────────────────────────────────┤
│ Граница 2: Сетевая │
│ eBPF network filter + SELinux TCP bind │
│ Только whitelist IP → порт 5000 │
│ Атакующий из сети не может: │
│ - обратиться к HSM Service напрямую │
│ - открыть произвольный порт │
│ - обойти фильтр из userspace │
├─────────────────────────────────────────────────────────┤
│ Граница 3: Процессная │
│ SELinux domains + systemd sandboxing │
│ pki_core_t ≠ pki_hsm_t ≠ unconfined_t │
│ Атакующий с RCE в REST API не может: │
│ - читать /dev/hidraw0 (только pki_hsm_t) │
│ - писать в /etc (ProtectSystem=strict) │
│ - повысить привилегии (NoNewPrivileges) │
│ - создать исполняемую память (MemoryDenyWriteExecute) │
└─────────────────────────────────────────────────────────┘
Три границы — три уровня компрометации. Чтобы получить полный контроль над PKI, атакующему нужно преодолеть все три. Каждая следующая граница предполагает, что предыдущая уже пробита.
Context-диаграмма намеренно скрывает внутреннюю структуру. Она не показывает, что PKI Core — это Flask с 12 endpoints, что HSM Service — это цепочка из четырёх классов (TRNG → DRBG → CryptoEngine → KeyStorage), что между ними SQLite и файловая система.
Это не упрощение — это правильный уровень абстракции. Для PKI Client неважно, как устроен CryptoEngine. Для Deploy Operator неважно, сколько таблиц в SQLite. Для STM32 вообще неважно, что происходит после USB HID report.
Context отвечает на один вопрос: «кто с кем разговаривает?» Ответ: четыре актора, два сервиса, пять связей. Всё остальное — на следующем уровне, в Container-диаграмме.