ТЗ: Secure Distributed File Processing System
Версия: 0.3 (MVP)
Компоненты: Rust Client, Go Server, Go Worker, C++ Core, PostgreSQL, Docker/rootless Podman
1. Назначение
Клиент загружает файл, Server создаёт задачу (Job), Worker забирает её, запускает существующий C++ Core (обфускация) в изолированном контейнере и возвращает результат. Клиент скачивает результат.
1.1. Цели
- Связать Rust Client и C++ Core через серверную инфраструктуру.
- Безопасная загрузка, обработка и скачивание файлов.
- Изоляция C++ Core от Server и от хоста Worker.
- Управление задачами, статусами и ошибками.
- Возможность добавления нескольких Worker.
- Код понятный, тестируемый, пригодный для развития.
1.2. Не входит в MVP
- Переписывание C++ Core.
- Лицензирование и биллинг, квоты сверх простого лимита активных задач.
- Несколько типов обработки.
- Админка, роли и права пользователей.
- Отмена задач (статуса
cancelled в MVP нет).
- WebSocket/SSE, push-уведомления.
- Kubernetes, Redis/RabbitMQ/Kafka, S3/MinIO.
- Продуктовая аналитика и метрики.
- JWT, refresh token rotation, MFA.
2. Архитектура
Rust Client --HTTPS (Bearer token)--> Go Server --> PostgreSQL
|--> Private Storage (локальная ФС)
^
| Internal HTTPS, mTLS (Worker сам опрашивает Server)
Go Worker --> Docker/Podman --> C++ Core
2.1. Ответственность компонентов
Client: аутентифицируется, загружает файл, запрашивает статус, скачивает результат. Прямого доступа к PostgreSQL, Storage и Worker нет.
Server: публичный и внутренний API, аутентификация и авторизация, проверка лимитов, создание и хранение Job, единственный владелец статусов, доступ к файлам, метаданные в PostgreSQL. Docker и C++ Core не запускает.
Worker: сам инициирует соединение с Server (pull), получает назначенную задачу, скачивает вход, создаёт отдельную рабочую директорию, запускает Core в sandbox, проверяет результат, передаёт его Server. Доступа к PostgreSQL нет. Не доверяет пользовательским именам файлов и путям.
C++ Core: только обработка файла. Не знает о пользователях, задачах и HTTP, не получает произвольных команд и параметров.
PostgreSQL: пользователи, токены, Job и метаданные; на MVP также очередь задач.
Storage: исходные файлы и результаты на локальной ФС. Публичного прямого доступа нет, доступ только через Server и авторизованные внутренние операции Worker.
2.2. Принципы взаимодействия
- Client общается только с Server.
- Worker общается только с Server по внутреннему API и сам забирает задачи.
- Server единственный владелец состояния Job; Worker не меняет БД напрямую.
- Core запускается только Worker и только внутри изолированной среды.
- Server не зависит от деталей запуска Core.
- Архитектура допускает добавление Worker.
3. Технологический стек
| Компонент |
Технология |
| Client |
Rust |
| Server, Worker |
Go |
| Core |
C++ |
| Database |
PostgreSQL |
| API |
REST over HTTPS |
| Контейнеризация |
Docker или rootless Podman |
| Storage MVP |
Локальная файловая система |
4. Основной сценарий
- Client:
POST /api/v1/jobs (multipart, потоково).
- Server проверяет токен, размер, лимит активных задач пользователя, доступное место.
- Server потоково сохраняет файл под случайным внутренним именем, создаёт Job
pending, возвращает job_id и статус.
- Worker:
POST /internal/v1/jobs/claim. Server атомарно назначает задачу, ставит processing, выдаёт lease и fencing_token.
- Worker скачивает вход, создаёт рабочую директорию, запускает Core в sandbox, шлёт heartbeat.
- Worker проверяет exit code и выходной файл, отправляет результат (
PUT .../result) или ошибку (POST .../fail).
- Server сохраняет результат, считает SHA-256, фиксирует
completed.
- Client запрашивает статус; после
completed скачивает результат.
5. Жизненный цикл Job
Статусы: pending, processing, completed, failed.
pending -> processing -> completed
| ^
| +-- retry (attempt < max_attempts) -> pending
+----> failed
5.1. Правила
- Статусами управляет только Server; Client не может менять статус.
- Worker сообщает о результате или ошибке, Server проверяет допустимость перехода.
- Переходы выполняются условными обновлениями в БД (
UPDATE ... WHERE status = ... AND fencing_token = ...).
completed только после того, как результат сохранён.
- Результат принимается только от Worker, которому задача назначена, и только с актуальным
fencing_token.
- Повторный запрос не приводит к повторному переходу и не создаёт дубликатов.
5.2. Инварианты
- Job принадлежит ровно одному пользователю.
completed означает, что результат существует и доступен для скачивания.
processing означает, что задача назначена Worker и имеет актуальный lease.
- Нельзя скачать результат чужой или незавершённой задачи.
5.3. Lease, retry, fencing
Поля Job: worker_id, lease_expires_at, heartbeat_at, attempt, fencing_token, max_attempts (по умолчанию 3), error_code, error_retryable.
- Каждый claim увеличивает
attempt и fencing_token на 1.
- Heartbeat продлевает lease. Если lease потерян (истёк или задача переназначена), Server отвечает
409, Worker обязан прервать работу и удалить workspace.
- Фоновая проверка Server:
processing с истёкшим lease возвращается в pending при attempt < max_attempts, иначе failed. Работает и при одном Worker.
fail содержит code и retryable:
retryable = false (некорректный вход, Core вернул ошибку на этом входе): сразу failed;
retryable = true (таймаут, сбой контейнера, нехватка ресурсов): повтор, пока attempt < max_attempts.
- Каждая попытка использует новую рабочую директорию.
6. API
Публичные endpoints: /api/v1, внутренние: /internal/v1. Единый формат ошибок { "error": { "code": "...", "message": "..." } }, без внутренних путей, stack trace и сведений об инфраструктуре.
6.1. Публичный API (Client)
| Метод |
Endpoint |
Назначение |
| POST |
/api/v1/auth/token |
Получить токен (логин + пароль) |
| GET |
/api/v1/auth/me |
Данные текущего пользователя |
| POST |
/api/v1/jobs |
Создать задачу и загрузить файл |
| GET |
/api/v1/jobs/{id} |
Статус и сведения о задаче |
| GET |
/api/v1/jobs/{id}/result |
Скачать результат |
| GET |
/health |
Проверка живости |
- Создание:
multipart/form-data, потоково, Idempotency-Key (опционально; уникален в рамках пользователя, хранится 24 часа; тот же ключ с другим телом даёт 422).
- Статус:
id, status, created_at, updated_at, error_code при ошибке, sha256 и размер при completed. Внутренние пути и данные Worker не раскрываются.
- Результат: только владельцу, только при
completed, через Server.
GET /api/v1/jobs (список) отложен.
6.2. Внутренний API (Worker)
| Метод |
Endpoint |
Назначение |
| POST |
/internal/v1/jobs/claim |
Получить следующую задачу (204, если пусто) |
| GET |
/internal/v1/jobs/{id}/input |
Скачать исходный файл |
| POST |
/internal/v1/jobs/{id}/heartbeat |
Продлить lease |
| PUT |
/internal/v1/jobs/{id}/result |
Передать результат |
| POST |
/internal/v1/jobs/{id}/fail |
Сообщить об ошибке (code, retryable) |
Запросы Worker содержат fencing_token. Server проверяет worker_id, статус, lease и токен.
6.3. HTTP-требования
- Только HTTPS (TLS 1.2+, предпочтительно 1.3).
- Read/write/idle timeouts, лимит размера HTTP body.
- Валидация входных DTO, корректные HTTP status codes.
- Идемпотентность повторяемых операций.
7. Требования безопасности
7.1. Аутентификация и авторизация пользователей
- Все защищённые endpoints требуют аутентификации.
- Пароли: argon2id (или bcrypt), с солью.
- API-токен: opaque, 32 байта из
crypto/rand, передаётся как Bearer.
- В БД хранится только
HMAC-SHA256(secret, token), сам токен не хранится. Поиск по хешу, сравнение в константное время.
- Таблица
tokens: token_hash (unique), user_id, created_at, expires_at (по умолчанию 30 дней), revoked_at. Revoke действует немедленно.
- HMAC-секрет: из переменной окружения или файла вне репозитория, наличие и длина (не менее 32 байт) проверяются при старте. Смена секрета инвалидирует все токены (допустимо для MVP).
- Rate limit и защита от перебора на
/auth/token; одинаковый ответ при неверном логине и неверном пароле.
- Для каждой операции с Job владелец проверяется на стороне Server: поиск по
job_id AND user_id. UUID не считается защитой от IDOR.
user_id из тела запроса не принимается. Endpoint для произвольной смены статуса у Client отсутствует.
7.2. Аутентификация Worker (mTLS)
- Внутренний API слушает отдельный порт/listener с
tls.RequireAndVerifyClientCert и собственным внутренним CA. Порт не выставляется в публичную сеть.
- Публичный и внутренний API разнесены: Worker-сертификат не даёт доступа к
/api/v1, пользовательский токен не даёт доступа к /internal/v1.
worker_id определяется по сертификату (CN/SAN), а не по данным запроса.
- Список разрешённых
worker_id хранится на Server; отзыв Worker = удаление из списка (и/или короткий срок жизни сертификата). Ротация сертификатов описывается в README.
- Worker не имеет DB credentials, не может получить произвольную задачу по чужому ID и не может отправить результат для не назначенной или переназначенной задачи.
- mTLS подтверждает, кто подключился, но не доказывает корректность результата при полной компрометации Worker. Для этого служат lease, fencing token и изоляция.
7.3. Защита от устаревших Worker и повторной отправки
- При claim Server увеличивает
fencing_token и выдаёт его Worker.
- Heartbeat продлевает lease; при истечении Server может переназначить задачу.
- После переназначения старый Worker не может ни завершить задачу, ни сообщить об ошибке.
- При отправке результата Server проверяет Worker, статус, lease и fencing token.
7.4. Защита загрузки файлов
- Максимальный размер входа и результата задаётся в конфиге; HTTP body ограничивается на уровне сервера.
- Потоковое сохранение (
io.Copy), без io.ReadAll.
- Лимит одновременных загрузок и активных задач на пользователя.
- Проверка свободного места, корректная обработка его нехватки.
- Внутренние имена файлов случайные; оригинальное имя только как metadata, никогда не путь.
- Расширению и MIME-типу от клиента не доверять.
- Нет Path Traversal и выхода из рабочей директории через symlink.
- Storage не публичный; временные файлы удаляются при ошибках и по политике хранения.
- Если появится распаковка архивов, отдельно закрыть Zip Slip и архивные бомбы.
7.5. Изоляция C++ Core
Входной файл недоверенный. Core может содержать уязвимости (buffer/integer overflow, use-after-free, OOB read/write, алгоритмический DoS, возможное выполнение кода). Поэтому Core запускается только в изолированной среде.
Минимальные настройки контейнера:
- не root;
--network none;
- read-only root filesystem;
- запись только в выделенную output/tmpfs директорию, вход монтируется read-only;
- лимиты CPU, RAM, PIDs, времени выполнения, размера output и дискового пространства, где возможно;
cap-drop ALL, no-new-privileges;
- seccomp (минимум default-профиль), по возможности AppArmor/SELinux;
- без
--privileged, без Docker socket внутри контейнера, без host PID/network namespaces, без широких mount.
Запуск фиксированного бинарника через exec.Command; sh -c с пользовательскими данными запрещён. Пользователь не влияет на команду, путь и аргументы.
Доступ к Docker socket на хосте равен root-доступу. Поэтому: Worker на отдельной машине/VM, предпочтительно rootless Podman, либо узкий supervisor с заранее заданными операциями запуска. При повышенных требованиях рассмотреть gVisor или Firecracker (после MVP).
7.6. Контракт запуска Core (зафиксировать до реализации Worker)
- Команда и аргументы (фиксированные).
- Пути входа и выхода внутри контейнера.
- Exit codes и их смысл (успех, некорректный вход, внутренняя ошибка).
- Формат и верхняя граница размера результата.
- Таймаут по умолчанию.
7.7. PostgreSQL
- Только parameterized queries; динамическая сортировка/имена колонок через allowlist.
- Отдельный DB-пользователь с минимальными привилегиями; БД не в публичной сети; TLS при сетевом соединении.
- Резервное копирование и проверка восстановления из backup.
- Worker DB credentials не получает.
7.8. Storage
- Файлы доступны только через авторизованные операции Server/Worker.
- Владелец Job проверяется перед выдачей результата; незавершённая Job результат не отдаёт.
- Файлы лежат в изолированных директориях, пользовательские пути не используются.
- SHA-256 результата сохраняется для проверки целостности хранения и передачи. Он не доказывает честность обработки.
7.9. Бизнес-логика и гонки
- Переходы статусов выполняются с проверкой текущего состояния.
- Повторное создание Job по одному
Idempotency-Key не создаёт дубликат.
- Завершение и переназначение разрешаются атомарно на стороне Server.
- Загрузка результата и переход в
completed образуют одну логическую операцию (см. раздел 8).
7.10. Что пока не требуется
- CORS не механизм аутентификации; для native Rust Client не нужен.
- CSRF не основной риск для Bearer-API без cookies; пересмотреть, если появятся cookie-сессии.
- SSRF неактуален, пока Server не загружает файлы по URL от пользователя.
- Сложная JWT-инфраструктура не нужна.
8. Атомарное завершение
При PUT /result Server:
- Проверяет
worker_id (из сертификата), статус processing, lease, fencing_token.
- Пишет результат во временный файл, проверяет размер, считает SHA-256 потоково.
- Делает
rename в финальное место.
- В одной транзакции с проверкой статуса и токена ставит
completed, сохраняет sha256 и размер.
Если шаг 4 не удался, Job остаётся processing, а файл без записи в БД считается осиротевшим. Фоновая очистка удаляет такие файлы.
9. Надёжность, хранение, нефункциональные требования
- Claim: транзакция и
SELECT ... FOR UPDATE SKIP LOCKED.
- Retention: входные файлы и результаты хранятся N дней (по умолчанию 7, настраивается), затем удаляются фоновой задачей.
- Большие файлы обрабатываются в пределах лимита без загрузки целиком в RAM.
- Server остаётся отзывчивым во время обработки.
- Ошибки логируются с correlation/job ID; логи не содержат токенов, секретов и содержимого файлов.
- Конфигурация (лимиты, таймауты, пути, параметры Worker и контейнера) валидируется при старте.
- Секреты не хранятся в репозитории.
- Миграции БД воспроизводимы.
10. Структура
10.1. Server
server/
├── cmd/server/main.go
├── internal/
│ ├── jobs/
│ │ ├── domain/ (job.go, status.go, errors.go)
│ │ ├── ports.go
│ │ ├── service.go
│ │ └── adapters/
│ │ ├── http/ (client/, worker/)
│ │ └── postgres/
│ ├── auth/
│ ├── users/
│ ├── storage/
│ └── platform/ (config, logging, tls)
├── migrations/
└── go.mod
Правила: Domain не зависит от HTTP, БД, ФС, Docker. Интерфейсы объявляются у потребителя. Client- и Worker-handlers используют один jobs.Service, но дают разные операции. Очередь это механизм назначения, а не отдельный бизнес-модуль. HTTP DTO не в Domain. Пакеты common/utils/helpers не создавать. Каталоги создавать по мере необходимости.
10.2. Worker
worker/
├── cmd/worker/main.go
└── internal/
├── jobs/ (клиент внутреннего API, цикл claim/heartbeat)
├── executor/ (запуск и контроль обработки)
├── container/ (запуск sandbox)
└── platform/
Worker не содержит логики пользователей и не управляет статусами в БД.
11. Тесты
Domain: допустимые и недопустимые переходы, инварианты, повторное завершение, retry-логика.
Server (безопасность и корректность):
- запрос без токена, с истёкшим и отозванным токеном отклоняется;
- пользователь не получает чужую Job и чужой результат (IDOR);
- результат до
completed не отдаётся;
- превышение размера отклоняется;
- повтор с тем же
Idempotency-Key не создаёт дубликат;
- Worker без валидного сертификата или с неразрешённым
worker_id отклоняется;
- результат от не назначенного Worker и со старым
fencing_token отклоняется;
- пользовательский токен не работает на
/internal/v1, сертификат Worker не работает на /api/v1;
- ошибка сохранения результата не приводит к
completed;
- Path Traversal и symlink-сценарии не выводят за пределы Storage;
- зависший
processing после истечения lease возвращается в pending.
Worker и sandbox:
- некорректный вход даёт контролируемую ошибку (
retryable = false);
- таймаут Core обрабатывается;
- негативные проверки лимитов: бесконечный цикл, аллокация памяти сверх лимита, fork-бомба, попытка сетевого доступа, запись вне output, запись в read-only rootfs;
- рабочие директории изолированы между задачами; результат проверяется до отправки.
Integration: upload -> claim -> обработка в sandbox -> result -> completed -> download; а также отказ Worker во время обработки и восстановление после истечения lease.
12. Критерии готовности MVP
- Пользователь аутентифицируется и загружает файл в пределах лимита.
- Server создаёт Job
pending; Worker (по mTLS) забирает её, скачивает вход, обрабатывает Core в sandbox, отправляет результат.
completed ставится только после успешного сохранения результата.
- Пользователь видит статус и скачивает результат только своей завершённой задачи; чужие данные недоступны.
- Worker не может завершить чужую, не назначенную или переназначенную задачу.
- Некорректный вход, сбой Core и таймаут обрабатываются контролируемо.
- Зависшая задача обнаруживается и восстанавливается.
- Входные файлы не обрабатываются вне sandbox; лимиты sandbox подтверждены негативными тестами.
- Токены хранятся только в виде HMAC-хеша, revoke работает; секреты вне репозитория; логи без секретов.
13. Порядок реализации
- Контракт Core (7.6), конфиг, TLS/mTLS-инфраструктура (CA, сертификаты Server и Worker).
- Domain Job (статусы, переходы, ошибки).
- Persistence: миграции, хранилище Job, атомарный claim, lease, fencing; таблицы users и tokens.
- Storage: потоковое сохранение и выдача, безопасные пути, лимиты.
- HTTP API: публичный и внутренний listener, auth и mTLS middleware, валидация, лимиты body, единый формат ошибок.
- Worker: claim, скачивание, workspace, sandbox, heartbeat, отправка результата и ошибки.
- Интеграция Core: фиксированный запуск, проверка exit code, выходного файла, таймаута и лимитов.
- Security и reliability review: авторизация и владение, IDOR, Path Traversal/symlink, изоляция контейнера, таймауты и лимиты, гонки завершения и переназначения, восстановление зависших задач, логи без секретов, backup/restore БД.
14. Документация
docs/
├── ARCHITECTURE.md компоненты, границы, взаимодействие
├── API.md публичный и внутренний API, ошибки
└── THREAT_MODEL.md активы, границы доверия, угрозы и меры защиты