Skip to content

Latest commit

 

History

History
103 lines (72 loc) · 11.6 KB

File metadata and controls

103 lines (72 loc) · 11.6 KB

C4 Context: PKI-on-Box — система и её окружение

О чём эта диаграмма

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
Loading

Четыре актора и их истории

На диаграмме четыре сущности взаимодействуют с PKI-on-Box. Каждая из них пришла сюда своим путём, и у каждой — своя модель доверия.

PKI Admin — человек с ключами от королевства

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 — машина, которая просит сертификаты

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 — человек с SSH-ключом

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 TRNG — устройство без мнения

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-диаграмме.