баг-фикс 30/34

This commit is contained in:
Zuev
2026-07-19 14:40:43 +03:00
parent 3d798c13e3
commit bc0e1ab1b4
172 changed files with 13431 additions and 2910 deletions

View File

@@ -6,18 +6,20 @@
```yaml
services:
backend: # Spring Boot (Java 17), порт 8080
frontend: # Apache httpd:alpine, порт 80
db: # PostgreSQL alpine3.23, порт 5432
backend: # Spring Boot (Java 17), внутренний порт 8080
frontend: # Apache httpd, публикует HTTP_PORT (по умолчанию 80)
db: # PostgreSQL 16.3, внутренний порт 5432
```
### Сеть
Все сервисы работают в Docker-сети `proxy` (external). Перед первым запуском:
Compose сам создаёт изолированную bridge-сеть `magistr`. Внешняя сеть или отдельно
установленный reverse proxy для локального запуска не нужны. Apache раздаёт frontend и
проксирует same-origin пути `/api` и `/actuator/health` в backend; backend и PostgreSQL не
публикуют порты на хосте.
```bash
docker network create proxy
```
Данные PostgreSQL сохраняются в именованном томе `postgres_data`. Обычный
`docker compose down` не удаляет их; явный `docker compose down -v` выполняет полный сброс.
### Переменные окружения
@@ -30,9 +32,29 @@ POSTGRES_DB=app_db
JWT_SECRET=replace-with-random-jwt-secret-minimum-32-bytes
JWT_ACCESS_TOKEN_TTL=15m
JWT_REFRESH_TOKEN_TTL=7d
JWT_REFRESH_CLEANUP_RETENTION=30d
LOGIN_RATE_MAX_FAILURES=5
LOGIN_RATE_ATTEMPT_WINDOW=15m
LOGIN_RATE_BASE_BLOCK_DURATION=1m
LOGIN_RATE_MAX_BLOCK_DURATION=15m
LOGIN_AUDIT_RETENTION=90d
TRUSTED_PROXY_CIDRS=172.16.0.0/12
OTEL_SDK_DISABLED=true
HTTP_PORT=80
```
`POSTGRES_PASSWORD` обязателен для `docker compose up`: пароль не хранится в `compose.yaml`. `JWT_SECRET` должен быть случайным секретом длиной минимум 32 байта. В продакшене секреты задаются через Kubernetes Secret, а не через коммитимые файлы.
Начальный шаблон находится в `.env.example`: скопируйте его в игнорируемый Git файл `.env`
и заполните пустые секреты. `POSTGRES_PASSWORD` и `JWT_SECRET` обязательны для
`docker compose up` и не хранятся в `compose.yaml`; JWT должен быть случайным секретом длиной
минимум 32 байта (`openssl rand -base64 48`). `POSTGRES_DB` одновременно задаёт создаваемую
БД и входит в `SPRING_DATASOURCE_URL`, поэтому произвольное локальное имя остаётся
согласованным. Все JWT TTL/cleanup-переменные и параметры защиты входа передаются
backend-контейнеру явно. `TRUSTED_PROXY_CIDRS` должен содержать только сеть фактического
reverse proxy: заголовок `X-Forwarded-For` от остальных источников backend игнорирует.
Поскольку локальный Compose не запускает OpenTelemetry Collector, SDK по умолчанию отключён;
при подключённом Collector задайте `OTEL_SDK_DISABLED=false` и его OTLP endpoint.
В продакшене секреты задаются через Kubernetes Secret, а не через коммитимые файлы.
Встроенного JWT fallback в приложении нет. Отсутствующее, короткое или шаблонное значение
останавливает запуск; профиль `production` также требует Secure refresh-cookie.
@@ -43,25 +65,41 @@ JWT_REFRESH_TOKEN_TTL=7d
Backend собирается через multi-stage сборку Maven:
1. Этап сборки: `maven:3.9-eclipse-temurin-17``mvn package`
2. Этап запуска: `eclipse-temurin:17-jre-alpine``java -jar app.jar`
2. OpenTelemetry Java Agent `2.28.1` загружается как фиксированный Maven-артефакт и
проверяется по закреплённому SHA-256
3. Этап запуска: `eclipse-temurin:17-jre-alpine``java -jar app.jar`
### Dockerfile (Frontend)
```dockerfile
FROM httpd:alpine
COPY . /usr/local/apache2/htdocs/
RUN chown -R www-data:www-data /usr/local/apache2/htdocs/
FROM node:22-alpine3.23@sha256:8516dce... AS frontend-assets
RUN npm ci
RUN npm run build:vendor
FROM httpd:alpine3.23@sha256:4a15e9c...
COPY --from=frontend-assets /build/dist/vendor/ /usr/local/apache2/htdocs/vendor/
COPY security.conf /usr/local/apache2/conf/extra/magistr-security.conf
COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf
```
Оба базовых образа зафиксированы tag и manifest digest. Первый этап собирает зафиксированный
OpenTelemetry bundle; второй раздаёт только runtime-
файлы, без `node_modules`, тестов и build-исходников. Apache подключает `mod_headers`,
`mod_proxy` и `mod_proxy_http`; proxy сохраняет исходный `Host`, чтобы `localhost` корректно
маршрутизировался в tenant `default`. Сервер
возвращает CSP с `script-src 'self'`, `script-src-attr 'none'`, `style-src 'self'` и точными
SHA-256 для оставшихся статических style-атрибутов. `unsafe-inline` и `unsafe-eval` не
используются. Дополнительно выставляются `nosniff`, `DENY`, строгий referrer policy и
ограниченная Permissions Policy.
---
## Kubernetes (продакшн)
### Расположение конфигурации
Ожидаемое расположение production-манифестов: `../k8s/`. В текущей рабочей копии этот
внешний каталог недоступен, поэтому приведённые ниже изменения являются обязательным
runbook для оператора, а не утверждением о применённом или проверенном состоянии кластера.
Production-манифесты находятся в `../k8s/`. Каталог расположен вне Git-корня `magistr`,
поэтому его изменения проверяются и перечисляются отдельно от `git diff` репозитория.
### Ключевые ресурсы
@@ -78,6 +116,37 @@ runbook для оператора, а не утверждением о прим
- `JWT_ACCESS_TOKEN_TTL=15m`
- `JWT_REFRESH_TOKEN_TTL=7d`
- `JWT_REFRESH_COOKIE_SECURE=true`
- `JWT_REFRESH_CLEANUP_ENABLED=true`
- `JWT_REFRESH_CLEANUP_RETENTION=30d`
- `JWT_REFRESH_CLEANUP_BATCH_SIZE=500`
- `JWT_REFRESH_CLEANUP_MAX_BATCHES=20`
- `JWT_REFRESH_CLEANUP_INTERVAL_MS=3600000`
- `JWT_REFRESH_CLEANUP_INITIAL_DELAY_MS=60000`
Cleanup проходит отдельно по каждой активной tenant-БД. `RETENTION` задаёт срок хранения
свежих истёкших и отозванных audit-записей, `BATCH_SIZE` и `MAX_BATCHES` ограничивают объём
одного прохода, а interval/initial delay управляют безопасным расписанием.
### Защита входа и доверенные proxy
`app-config` задаёт общий PostgreSQL rate limit для всех backend-pod:
- `LOGIN_RATE_MAX_FAILURES=5`
- `LOGIN_RATE_ATTEMPT_WINDOW=15m`
- `LOGIN_RATE_BASE_BLOCK_DURATION=1m`
- `LOGIN_RATE_MAX_BLOCK_DURATION=15m`
- `LOGIN_AUDIT_RETENTION=90d`
- `TRUSTED_PROXY_CIDRS=10.42.0.0/16`
Последнее значение соответствует стандартной pod-сети K3s, из которой Traefik обращается
к backend. Если кластер запущен с другим `cluster-cidr`, перед rollout нужно указать
фактическую сеть ingress proxy. Не следует добавлять публичные сети клиентов: backend
доверяет `X-Forwarded-For` только от непосредственного источника из этого списка.
Счётчики и audit-события находятся в каждой tenant-БД, поэтому два pod используют одно
атомарное состояние. Старая история очищается раз в сутки ограниченными `SKIP LOCKED`
пачками; параметры `LOGIN_AUDIT_CLEANUP_*` из `.env.example` позволяют изменить расписание
и максимальный объём одного прохода.
`app-secret` задаёт `JWT_SECRET`. Этот Secret не создаётся файлами `../k8s/`: его заранее
предоставляет внешний secret manager или оператор. Deployment явно включает профиль
@@ -194,8 +263,7 @@ namespace `magistr`. Secret должен существовать заранее
### Liveness и readiness probes backend
TCP probe подтверждает только открытый порт и не отражает готовность обязательных tenant-БД.
В production Deployment необходимо использовать раздельные HTTP probes:
Production Deployment использует раздельные HTTP probes:
```yaml
livenessProbe:
@@ -214,29 +282,20 @@ Liveness зависит только от состояния процесса. R
исключается из приёма трафика без перезапуска. Actuator не публикует components, домены,
JDBC URL или credentials.
Параметры `initialDelaySeconds`, `periodSeconds`, `timeoutSeconds` и `failureThreshold` нужно
согласовать с текущим Deployment; замена механизма probe не подтверждает автоматически
корректность этих порогов.
`../k8s/backend.yaml` монтирует каталог `/config` без `subPath`, а `app-config` задаёт
`TENANTS_CONFIG_PATH=/config/tenants.json` и `TENANTS_CONFIG_REQUIRED=true`. Отсутствующий
или невалидный обязательный Secret поэтому снимает readiness, не включая H2 fallback.
### Внешние действия для production Kubernetes
Каталог `../k8s/` не изменялся и не проверялся в рамках текущей рабочей копии. Оператору после
получения доступа к нему необходимо:
1. обновить backend Deployment: directory mount без `subPath`,
`TENANTS_CONFIG_REQUIRED=true` и две HTTP probes;
2. добавить либо сузить Role до `get`/`update` одного `tenants-secret` и проверить RoleBinding
фактического ServiceAccount;
3. убедиться, что внешний `tenants-secret` существует, содержит непустой ключ `tenants.json`
и допускает обновление;
4. отрендерить манифесты и выполнить серверную проверку без применения:
Перед production rollout оператор должен убедиться, что внешний `tenants-secret` существует,
содержит непустой ключ `tenants.json`, допускает обновление, и выполнить проверки без
применения:
```bash
kubectl kustomize ../k8s >/dev/null
kubectl apply --dry-run=server -k ../k8s
```
5. отдельно проверить полномочия фактического ServiceAccount:
Отдельно проверьте полномочия фактического ServiceAccount:
```bash
BACKEND_SERVICE_ACCOUNT='укажите-фактический-service-account'
@@ -246,15 +305,21 @@ kubectl auth can-i update secret/tenants-secret \
--as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr
```
Применение манифестов, rollout и проверка реальных pod являются отдельными внешними
операциями и без разрешения автоматически не выполняются.
Production rollout и проверка реальных pod являются внешними операциями и без отдельного
разрешения из этой рабочей копии не выполнялись.
### Обновление backend
### Ручное обновление backend и frontend
```bash
kubectl rollout restart deployment backend -n magistr
export BACKEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-backend@sha256:<64 hex>'
export FRONTEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-frontend@sha256:<64 hex>'
bash ../k8s/deploy.sh apply
```
Манифесты содержат нулевой digest-sentinel. `deploy.sh` требует два реальных digest,
локально подставляет их до `kubectl apply` и строго ждёт оба rollout; прямое применение
Deployment-файлов запрещено.
---
## Caddy (реверс-прокси)
@@ -275,10 +340,16 @@ kubectl rollout restart deployment backend -n magistr
Расположение: `.gitea/workflows/docker-build.yaml`
Основные шаги:
1. Checkout кода
2. Login в Docker Registry
3. Build + Push образов (`backend`, `frontend`)
4. Генерация меток через `docker/metadata-action`
1. Backend unit/component tests, frontend static/unit tests, Compose validation и shell-тест
immutable rollout/rollback.
2. Только после успешных gates — login, параллельная сборка и push backend/frontend с
SHA-tag и release tag без `latest`.
3. Build jobs публикуют digest обоих образов; deploy job устанавливает фиксированный
`kubectl v1.33.12` после SHA-256 проверки.
4. `scripts/deploy-images.sh` принимает только `image@sha256:...`, сохраняет предыдущие
ссылки, применяет оба digest и ждёт rollout. При отказе автоматически возвращает оба
предыдущих образа и повторно проверяет их готовность.
5. Workflow-wide concurrency lock не допускает одновременные production deployment.
---
@@ -289,7 +360,7 @@ kubectl rollout restart deployment backend -n magistr
```mermaid
graph LR
Backend["Spring Boot"] -->|OTLP gRPC| Collector["OTel Collector"]
Frontend["JS (otel.js)"] -->|OTLP HTTP| Collector
Frontend["JS (локальный OTel bundle)"] -->|OTLP HTTP| Collector
Collector --> SigNoz["SigNoz"]
Collector -->|"Метрики PostgreSQL"| PgExporter["pg_exporter"]
@@ -308,10 +379,13 @@ Tenant ID добавляется в:
### Интеграция Frontend
Файл `admin/js/otel.js` — клиентская телеметрия:
Собранный `/vendor/otel.js` — клиентская телеметрия:
- Метрики производительности страниц
- Трейсы пользовательских действий
Исходные npm-пакеты имеют exact-версии и lockfile, собираются внутри frontend-образа и не
загружаются браузером со стороннего CDN.
PostgreSQL receivers collector получают endpoint и credentials только из
`otel-postgres-secret`; ConfigMap collector содержит лишь `${env:...}` ссылки.