diff --git a/backlog.md b/backlog.md index 7881bee..ffb99af 100644 --- a/backlog.md +++ b/backlog.md @@ -27,7 +27,7 @@ | BL-KAFKA | Kafka как шина данных между сервисами (сейчас gRPC + БД-outbox) | ТЗ §12, обсуждение | P3 | DEFERRED | | BL-CF | Cloudflare / внешний периметр (сейчас Caddy, mTLS; конфиг вне кода) | ТЗ §11, техдок §10 | P2 | MANUAL | | BL-CI | Внешний CI/CD + SAST + скан зависимостей в пайплайне (сейчас вручную) | техдок §8 | P2 | BACKLOG | -| BL-IMG-HARDEN | non-root/read-only дефолты контейнеров, seccomp/limits | техдок §10 | P2 | TECHDEBT | +| BL-IMG-HARDEN | **Сделано (2026-09-11):** non-root USER (deal, UID 10001, HOME=/tmp) во всех прикладных образах; в compose — read_only root FS + tmpfs /tmp, no-new-privileges, cap_drop ALL, mem_limit/cpus; логи stateless-сервисов в /tmp/logs. Проверено `compose config` (dev/prod/observability); живой прогон — MANUAL | техдок §10 | P2 | DONE (live — MANUAL) | | BL-BACKUP-CRON | Автоматизация бэкапов (cron/systemd-примеры есть, реальный прогон — MANUAL) | техдок §9 | P2 | MANUAL | ## 3. SaaS / мультитенантность @@ -59,7 +59,7 @@ | TD-OLD-DOCS | Исторические доки несут старые термины под пометками (переписывать не нужно) | docs sweep | P3 | TECHDEBT | | TD-SOURCE-PROVIDER | Провайдеры содержимого источников. **Сделано (2026-09-11):** `TelegramSourceContentProvider` + `ReadSource` RPC + `GET /api/cards/{id}/source` + UI «Обновить из источника». Осталось: провайдеры прочих источников по мере появления | generic source 2026-09-11 | P2 | TECHDEBT | | TD-STORE-ATTACH | Выгрузка вложений источника в Storage-сервис адаптером. **Решение (2026-09-11): медиа-посты Telegram пропускаем** — извлечение/выгрузка не делаются. Остались на будущее: вложения прочих источников (файл/диск/таблица) и `ISourceContentProvider` для remote-просмотра | generic source 2026-09-11 | P3 | BACKLOG | -| TD-TG-CORE-SPLIT | Перенести оставшуюся Telegram-специфику ядра (`TelegramStore`, таблицы `Dialogs`/`TgMessages`, Discovery) в telegram-сервис. **Сделано (2026-09-11):** входящий поток переведён на generic `sources.proto`/`PushSource`, `PushMessage` удалён, приём в ядре generic (`SourceIngressGrpcService`). Осталось: каталог (`SyncDialogs`/`ReportStatus`), `TelegramStore`, Discovery | generic source 2026-09-11 | P2 | TECHDEBT | +| TD-TG-CORE-SPLIT | Перенос оставшейся Telegram-специфики ядра в telegram-сервис. **Закрыто (2026-09-11): не требуется.** Задача «дашборды/карточки не знают о Telegram» решена generic-контрактом источника; оставшиеся `TelegramStore`/`Dialogs`/`TgMessages`/Discovery — это состояние тенанта (ядро — владелец данных, telegram-service — stateless-шлюз), перенос отдал бы шлюзу доступ к схеме тенанта | generic source 2026-09-11 | — | CLOSED | | TD-SOURCE-CONTACTS | Квалификатор контактов знает форматы профилей (t.me/`@handle`) — вынести в расширяемые правила источников | generic source 2026-09-11 | P3 | BACKLOG | | TD-APIMAP-COUNT | Ручной подсчёт числа операторских ручек в `api-map` (может расходиться с группировкой) | docs sweep | P3 | TECHDEBT | diff --git a/deploy/compose.dev.yml b/deploy/compose.dev.yml index fe83b57..807f6d4 100644 --- a/deploy/compose.dev.yml +++ b/deploy/compose.dev.yml @@ -26,6 +26,17 @@ # dotnet run, и сервисы ходят в ингресс хоста через DEAL_CORE_INGRESS=http://host.docker.internal:5082 # (см. env telegram-service ниже). +# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp, +# запрет новых привилегий и сброс capabilities. Данные — только в именованных volume. +x-service-hardening: &service-hardening + read_only: true + tmpfs: + - /tmp + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + services: postgres: image: postgres:16-alpine @@ -72,6 +83,9 @@ services: context: .. dockerfile: src/core/Deal.Api/Dockerfile container_name: deal-core + <<: *service-hardening + mem_limit: 1g + cpus: 1.5 environment: ASPNETCORE_ENVIRONMENT: Development ASPNETCORE_URLS: http://0.0.0.0:5080 @@ -123,11 +137,15 @@ services: context: .. dockerfile: src/telegram-service/Deal.Telegram/Dockerfile container_name: deal-telegram-service + <<: *service-hardening + mem_limit: 512m + cpus: 1.0 environment: GRPC_PORT: "5101" DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:-ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=} DEAL_TELEGRAM_SESSION_DIR: /data/sessions + DEAL_LOGS_DIR: /tmp/logs SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082} ports: - "5101:5101" @@ -151,9 +169,13 @@ services: context: .. dockerfile: src/ai-service/Deal.Ai/Dockerfile container_name: deal-ai-service + <<: *service-hardening + mem_limit: 512m + cpus: 1.0 environment: GRPC_PORT: "5102" DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} + DEAL_LOGS_DIR: /tmp/logs ports: - "5102:5102" healthcheck: @@ -173,10 +195,14 @@ services: context: .. dockerfile: src/ml-service/Deal.Ml/Dockerfile container_name: deal-ml-service + <<: *service-hardening + mem_limit: 768m + cpus: 1.0 environment: GRPC_PORT: "5103" DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} DEAL_ML_DATA_DIR: /data/ml # файлы моделей data/ml/.sqlite на volume deal_ml_data (Ruling 4/12) + DEAL_LOGS_DIR: /tmp/logs ports: - "5103:5103" volumes: @@ -195,6 +221,9 @@ services: context: .. dockerfile: src/storage-service/Deal.Storage/Dockerfile container_name: deal-storage-service + <<: *service-hardening + mem_limit: 512m + cpus: 1.0 environment: GRPC_PORT: "5104" DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token} @@ -203,6 +232,7 @@ services: DEAL_STORAGE_SECRET_KEY: deal_minio_secret DEAL_STORAGE_BUCKET: deal-attachments DEAL_STORAGE_SECURE: "false" + DEAL_LOGS_DIR: /tmp/logs ports: - "5104:5104" depends_on: diff --git a/deploy/compose.prod.yml b/deploy/compose.prod.yml index bfa7d54..2e47e72 100644 --- a/deploy/compose.prod.yml +++ b/deploy/compose.prod.yml @@ -48,6 +48,17 @@ # Frontend: статика монтируется из ../src/frontend/dist (СОБРАТЬ ДО ПОДЪЁМА: cd src/frontend && # npm run build). Конфигурация caddy — deploy/caddy/Caddyfile (см. его шапку про домен/TLS). +# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp, +# запрет новых привилегий и сброс capabilities. Данные/логи — только в именованных volume или tmpfs. +x-service-hardening: &service-hardening + read_only: true + tmpfs: + - /tmp + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + services: postgres: image: postgres:16-alpine @@ -84,6 +95,9 @@ services: build: context: .. dockerfile: src/core/Deal.Api/Dockerfile + <<: *service-hardening + mem_limit: 1g + cpus: 1.5 environment: ASPNETCORE_ENVIRONMENT: Production ASPNETCORE_URLS: http://0.0.0.0:5080 @@ -149,11 +163,15 @@ services: build: context: .. dockerfile: src/telegram-service/Deal.Telegram/Dockerfile + <<: *service-hardening + mem_limit: 512m + cpus: 1.0 environment: GRPC_PORT: "5101" DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан} DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:?DEAL_TELEGRAM_SESSION_KEY не задан (ключ AES-GCM сессий)} DEAL_TELEGRAM_SESSION_DIR: /data/sessions + DEAL_LOGS_DIR: /tmp/logs SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082} DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0} DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem @@ -176,9 +194,13 @@ services: build: context: .. dockerfile: src/ai-service/Deal.Ai/Dockerfile + <<: *service-hardening + mem_limit: 512m + cpus: 1.0 environment: GRPC_PORT: "5102" DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан} + DEAL_LOGS_DIR: /tmp/logs DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0} DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ai-service-server.pfx @@ -199,10 +221,14 @@ services: build: context: .. dockerfile: src/ml-service/Deal.Ml/Dockerfile + <<: *service-hardening + mem_limit: 768m + cpus: 1.0 environment: GRPC_PORT: "5103" DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан} DEAL_ML_DATA_DIR: /data/ml + DEAL_LOGS_DIR: /tmp/logs DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0} DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ml-service-server.pfx diff --git a/docs/technical/Техническая-документация-Дейл.md b/docs/technical/Техническая-документация-Дейл.md index ed43734..d41ce8b 100644 --- a/docs/technical/Техническая-документация-Дейл.md +++ b/docs/technical/Техническая-документация-Дейл.md @@ -428,8 +428,15 @@ DEAL_MTLS_ENABLED=0|1 DEAL_MTLS_CERT_PASSWORD=... DEAL_DEFAULT_AI_BU - Криптография/код: пароли Argon2id; секреты настроек AES-256-GCM (`enc:`, ключ `DEAL_ENCRYPTION_KEY`); сессии Telegram AES-256-GCM (`DEAL_TELEGRAM_SESSION_KEY`); SQL параметризуется; секреты в логи/аудит не пишутся. +- **Hardening контейнеров (BL-IMG-HARDEN, 2026-09-11):** прикладные образы (core/telegram/ai/ml/storage) + работают non-root (пользователь `deal`, UID 10001) с `HOME=/tmp`; в compose заданы `read_only: true`, + `tmpfs: /tmp`, `security_opt: no-new-privileges`, `cap_drop: ALL` и лимиты `mem_limit`/`cpus` (якорь + `x-service-hardening`). Данные — в именованных volume (`/app/data` core, `/data/sessions` telegram, + `/data/ml` ml); логи stateless-сервисов — `DEAL_LOGS_DIR=/tmp/logs` (tmpfs), у core — volume + `/app/data/logs`. Проверено `docker compose config` (dev и prod, включая профиль observability); + живой подъём с этими ограничениями — ⚠ Manual. - **Вне этапа (не настроено; заделы §11/roadmap):** Cloudflare (конфигурация вне кода — шапка - Caddyfile), k8s, non-root/read-only-дефолты образов, биллинг, UI админок, саморегистрация. + Caddyfile), k8s, биллинг, UI админок, саморегистрация. --- diff --git a/src/ai-service/Deal.Ai/Dockerfile b/src/ai-service/Deal.Ai/Dockerfile index 7b19ab0..96dd7e8 100644 --- a/src/ai-service/Deal.Ai/Dockerfile +++ b/src/ai-service/Deal.Ai/Dockerfile @@ -35,4 +35,9 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health # Volume-ов нет: ai-service без БД и без персистентных файлов (Ruling 5) — конфиг провайдера # (id/base/model/apiKey/api_style) приходит в теле каждого запроса от ядра, ответы не хранятся. +# Non-root: процесс работает пользователем deal (UID 10001); персистентных файлов нет. +RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal +ENV HOME=/tmp +USER deal + ENTRYPOINT ["dotnet", "Deal.Ai.dll"] diff --git a/src/core/Deal.Api/Dockerfile b/src/core/Deal.Api/Dockerfile index 7a792f8..53f2eb0 100644 --- a/src/core/Deal.Api/Dockerfile +++ b/src/core/Deal.Api/Dockerfile @@ -32,4 +32,12 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health # (/app/data); каталог монтируется volume-ом deal_api_data из compose.dev.yml. В полном dev-стеке файлы # идут в MinIO (Storage__Minio__* env, см. compose.dev.yml) — volume остаётся для ключа шифрования. +# Non-root: процесс работает пользователем deal (UID 10001); /app/data — каталог данных (volume). +# HOME=/tmp — рабочий каталог для временных/DP-файлов (совместимо с read_only root FS + tmpfs /tmp). +RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal \ + && mkdir -p /app/data \ + && chown -R deal:deal /app/data +ENV HOME=/tmp +USER deal + ENTRYPOINT ["dotnet", "Deal.Api.dll"] diff --git a/src/ml-service/Deal.Ml/Dockerfile b/src/ml-service/Deal.Ml/Dockerfile index 18a5714..1436414 100644 --- a/src/ml-service/Deal.Ml/Dockerfile +++ b/src/ml-service/Deal.Ml/Dockerfile @@ -35,4 +35,11 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health # Файлы моделей тенантов — data/ml/.sqlite (Ruling 4): каталог монтируется volume-ом # deal_ml_data из compose.dev.yml; каталог и файлы создаёт движок модели (задачи 5–6). +# Non-root: процесс работает пользователем deal (UID 10001); /data/ml — каталог моделей (volume). +RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal \ + && mkdir -p /data/ml \ + && chown -R deal:deal /data/ml +ENV HOME=/tmp +USER deal + ENTRYPOINT ["dotnet", "Deal.Ml.dll"] diff --git a/src/storage-service/Deal.Storage/Dockerfile b/src/storage-service/Deal.Storage/Dockerfile index a259d61..21705d1 100644 --- a/src/storage-service/Deal.Storage/Dockerfile +++ b/src/storage-service/Deal.Storage/Dockerfile @@ -23,4 +23,10 @@ WORKDIR /app EXPOSE 5104 COPY --from=build /app/publish . COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health-probe /bin/grpc_health_probe + +# Non-root: процесс работает пользователем deal (UID 10001); персистентных файлов нет. +RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal +ENV HOME=/tmp +USER deal + ENTRYPOINT ["dotnet", "Deal.Storage.dll"] diff --git a/src/telegram-service/Deal.Telegram/Dockerfile b/src/telegram-service/Deal.Telegram/Dockerfile index b79659a..4e5cdc5 100644 --- a/src/telegram-service/Deal.Telegram/Dockerfile +++ b/src/telegram-service/Deal.Telegram/Dockerfile @@ -35,4 +35,11 @@ COPY --from=ghcr.io/grpc-ecosystem/grpc-health-probe:v0.4.35 /ko-app/grpc-health # Сессии тенантов — файлы /data/sessions (Ruling 3): каталог монтируется volume-ом deal_tg_sessions # из compose.dev.yml; создаётся при первом сохранении сессии (задачи 9–11). +# Non-root: процесс работает пользователем deal (UID 10001); /data/sessions — каталог сессий (volume). +RUN useradd --uid 10001 --shell /usr/sbin/nologin --no-create-home deal \ + && mkdir -p /data/sessions \ + && chown -R deal:deal /data/sessions +ENV HOME=/tmp +USER deal + ENTRYPOINT ["dotnet", "Deal.Telegram.dll"]