19 Commits

Author SHA1 Message Date
Александр Зимин 664590660b fix(ci): use /dockerhub/ prefix для Nexus proxy путей 2026-06-20 21:45:56 +00:00
Zimin A.N. 7abff8f73f fix(security): CSO findings — non-root containers + .env gitignore + actuator hardening
Три прямых фикса по /cso security audit на v2.45.2:

## Finding #1 (HIGH): Backend containers run as root

ordinis-app / ordinis-read-api / ordinis-projection-writer Dockerfile'ы
запускали JVM от root (eclipse-temurin:21-jre default'но не задаёт USER).
Compromise через Spring CVE / Jackson deserialization → root-в-контейнере
= escalation surface на ноду через любой pod misconfig.

Добавлено: `RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app
-s /sbin/nologin app` + `USER app`. UID 10001 — outside system (0-999)
и distribution reserved (1000-9999). `--chown=app:app` на COPY чтобы
JVM могла прочитать jar.

ordinis-migrations (USER liquibase) и docs (nginx default) уже non-root.

## Finding #2 (HIGH): .env / .env.local not in .gitignore

`.gitignore` блокировал *.pem, *.key, .gstack/, .claude/ — но не .env.
Развработчик с realный dotenv для local dev мог случайно `git add .` и
закоммитить creds. Сейчас нет committed .env (verified) но защита нулевая.

Добавлено: `.env`, `.env.local`, `.env.*.local`, `*.env` + whitelist
`!.env.example`, `!.env.template`, `!.env.sample` для шаблонов.

Существующий tracked `ordinis-admin-ui/.env.example` остался tracked
(verified `git ls-files | grep .env.example`).

## Finding #3 (MEDIUM): Spring Actuator permitAll() — defense-in-depth gap

SecurityConfig had `requestMatchers("/actuator/**").permitAll()` →
любой pod в кластере мог `curl ordinis-app:8080/actuator/env` и
получить ORDINIS_KEYCLOAK_ADMIN_CLIENT_SECRET, DB creds и пр.

VERIFIED: nginx ingress на проде НЕ проксирует /actuator/* (SPA fallback
отдаёт index.html), так что не exposed на интернет. Но pod-to-pod в
кластере = открыто.

Defense in two layers:

1. SecurityConfig: split actuator routes —
     /actuator/health/**, /actuator/info, /actuator/prometheus → permitAll
     /actuator/**                                              → denyAll
   Probes/Prometheus scrape работают, остальное блок.

2. application.yml для всех трёх Spring apps: narrowed default exposure
   to "health,info,prometheus". Поддерживается env override
   MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE для staging diagnostic
   (включить env/configprops/loggers только когда нужно дебажить
   autoconfig).

## Тесты

- mvn package — все 15 модулей SUCCESS
- ordinis-rest-api + ordinis-auth tests — SUCCESS

## Manual verify post-deploy

После пророллится v2.45.3:
- staging+prod liveness/readiness probes должны остаться зелёными
  (/actuator/health/{liveness,readiness} в permitAll list).
- Prometheus scrape /actuator/prometheus → 200.
- ssh pod && curl localhost:8080/actuator/env → должен вернуть
  401/403 (или 404 если endpoint вообще выключен через exposure).

## Откат

Если probes сломаются (маловероятно — health endpoints whitelisted) —
revert этого MR. Image rollback к v2.45.2 через helm.
2026-05-27 19:02:34 +03:00
Александр Зимин 51d6709fb3 ci(docker): future-proof backend Dockerfiles (fix root cause from Phase A break) 2026-05-10 16:23:03 +00:00
Александр Зимин 3d5304b308 fix(docker): include ordinis-notifications in 3 Dockerfiles (unblock main pipeline) 2026-05-10 15:57:52 +00:00
Zimin A.N. 0a8275ef4c feat(redis-projection): per-dict feature flag (CEO plan E2 tiered perf)
Per-dictionary opt-in для Redis projection materialization. Default false
— projection-writer пропускает event'ы того dict'а, никаких dead writes
в Redis. Включается админ'ом через UI checkbox в Metadata tab.

Migration 0017:
- ALTER TABLE dictionary_definitions ADD COLUMN redis_projection_enabled
  BOOLEAN NOT NULL DEFAULT false.
- Index idx_dict_def_redis_proj для быстрого filtering в list queries.

Backend:
- DictionaryDefinition entity: new field + getter/setter.
- DictionaryResponse DTO: returns flag в response.
- CreateDictionaryRequest DTO: optional Boolean (null/missing = no change).
- DictionaryDefinitionService.create + updateSchema: применяет flag из request.
- ProjectionWriter (projection-writer service):
  * upsert() — early-return + skip counter если flag=false на dict.
  * delete() — symmetric: skip если flag=false (защита от stale keys
    после flag-flip; полный cleanup TODO Phase 2).
  * New metric: ordinis_projection_records_skipped_total.

Admin UI:
- DictionaryEditorDialog Metadata tab: checkbox "Включить Redis-проекцию"
  с описанием. Apply через CreateDictionaryRequest payload.
- DictionaryDefinition + CreateDictionaryRequest types updated.
- i18n RU/EN: schema.redisProjection.{label,hint}.

Что НЕ делается (deferred Phase 2):
- read-api Redis routing — read-api сейчас всегда идёт в PG read replica.
  Когда dict опт-инится в флаг, projection материализуется, но read-api
  ещё не использует. Read routing — отдельная feature с benchmark proof
  (через JMH + k6 SLO test).
- One-shot cleanup job для stale keys после flag-flip from true→false.

Behavior change в production:
- До patch: projection-writer пишет в Redis для ВСЕХ dict events (40
  dictionaries). Default flag=false после migration → projection-writer
  пропускает всё → Redis keys для existing dicts становятся stale.
  Это OK потому что read-api НИКОГДА не читал из Redis (writes были
  dead). Stale keys eventually evicted Redis maxmemory policy.
- После patch: только dicts с redis_projection_enabled=true получают
  материализацию. По default — нет. Admin opt-ins per-dict через UI.

Verify:
- mvn test (full reactor): SUCCESS, all green.
- mvn -P e2e -pl ordinis-app -am test: 20/20 PASS (migration 0017 applied).
- pnpm tsc --noEmit: clean.
- pnpm test (vitest): 89/89 PASS.
- pnpm build: clean.
2026-05-08 12:34:58 +03:00
Zimin A.N. 3a28d741e9 feat(metrics): включить percentiles-histogram для http.server.requests
Spring Boot Micrometer по дефолту эмитит только _count/_sum/_max
для http.server.requests — без _bucket нельзя посчитать
histogram_quantile() в Prometheus, p50/p95/p99 latency дашборды
получают "No data".

Включён percentiles-histogram + кастомные SLO bucket boundaries
от 5ms до 5s — покрывают realistic range read-api JSON endpoints.
Server-side histograms точнее client-side percentiles (по которым
нельзя aggregate across instances).

Применить: build + push image + helm upgrade. После рестарта
ordinis-app/read-api/projection-writer Grafana дашборды покажут
latency графики.
2026-05-06 11:10:33 +03:00
Zimin A.N. 2b70c0692d fix(projection-writer): отключить spring-cloud-vault kv source
projection-writer крашится в bootstrap на 403 при чтении
secret/data/ordinis/cuod/k8s — путь которого нет ни в одной из vault-policies
(ordinis-app/read-api/projection-writer). Сами секреты приходят через Vault
Agent injector в env-переменные ещё до старта JVM, поэтому spring-cloud-vault
как config source избыточен.

writer и read-api ту же 403 получают, но Spring Cloud Vault лог-warning'ом
переваривает. У projection-writer почему-то фатально (возможно из-за
ROTATE-mode на secret lease + redis-зависимости в bootstrap chain). Не лезу
в эту разницу — просто отключаю kv совсем.

Если когда-то понадобится использовать vault как config source — добавить
читать secret/data/ordinis/cuod/k8s в политики или сменить application-name
на ordinis-projection-writer + создать отдельный путь.
2026-05-06 00:24:29 +03:00
Александр Зимин a969c50072 fix(docker): COPY ordinis-auth в Dockerfile'ах app/read-api/projection-writer
Multi-module Maven build падал на 'Child module ordinis-auth does not exist'
потому что Dockerfile'ы копировали только конкретные используемые модули.
Добавлен COPY ordinis-auth ./ordinis-auth перед COPY ordinis-validation.
2026-05-04 00:43:29 +03:00
Александр Зимин 519c397a29 fix(projection-writer): add spring-boot-starter-web for actuator HTTP server
Without spring-boot-starter-web, Spring Boot не запускает Tomcat, и
actuator endpoints (/health, /metrics) недоступны. Probes в чарте не
могут пройти. Добавляем web starter — Tomcat поднимется на server.port
(8082), actuator/health/{liveness,readiness} ответят, probes работают.
2026-05-03 23:13:47 +03:00
Александр Зимин 5ed360a979 fix: misc tech debt
- ci: resolve UPSTREAM_IMAGE_TAG via dotenv artifact (resolve-tag job)
  GitLab quirk — $IMAGE_TAG в trigger.variables не интерполируется
  напрямую от upstream global vars; dotenv делает значение job-local,
  тогда interpolation работает.
- projection-writer: management.server.port=8082 + web-application-type=none
  Возврат actuator/health probes — Spring Boot non-web app не имеет
  HTTP сервера; вынесли actuator на отдельный management server.
- docker-compose.yml: локальный dev-стек (PG + Kafka + Redis + 3 services)
  через repo.nstart.cloud/terravault images. Включается профилем 'dev'.
2026-05-03 23:06:24 +03:00
Александр Зимин d49dd6be06 fix(k8s): add datasource/kafka/redis config to k8s profile
K8s profile relied on shared/dev defaults for spring.datasource.url, but those
live inside the dev profile block — invisible when k8s profile is active.
Result: 'APPLICATION FAILED TO START — Failed to configure a DataSource'.

Move full datasource (and kafka/redis for projection-writer) wiring into the
k8s profile section. ENV vars come from Vault Agent injector
(ORDINIS_PG_USER/PASSWORD, ORDINIS_KAFKA_USER/PASSWORD, ORDINIS_REDIS_PASSWORD)
and chart-supplied ENV (ORDINIS_PG_HOST, ORDINIS_KAFKA_BOOTSTRAP, etc).
2026-05-03 20:24:53 +03:00
Александр Зимин 413bd3449d chore: update default config-server fallback to ordinis-staging
application.yml fallback URL was http://config-server.ordinis-dev:8888 — namespace
removed. Default now points to staging; SPRING_CLOUD_CONFIG_URI env still overrides
via Helm chart for any deployment.
2026-05-03 20:11:51 +03:00
Zimin A.N. 8eb0ec086e ci: pull base images через repo.nstart.cloud (DH official → /library/, vendor → /<vendor>/) 2026-05-03 18:40:29 +03:00
Zimin A.N. 9a79b53eb0 ci: align с altum pattern — runner mirror'ит Docker Hub
- Убраны префиксы repo.nstart.cloud/ — runner config (DOCKER_AUTH_CONFIG)
  обрабатывает pull из Docker Hub mirror автоматически
- DinD сервис: alias=docker, --tls=false, DOCKER_HOST=tcp://docker:2375
  (proven pattern из altum/.gitlab/ci/docker.yml)
- IMAGE_TAG = ref-slug + sha (как у altum)
- BUILDKIT_INLINE_CACHE=1 для cache-from optimization
- Dockerfiles: FROM maven/eclipse-temurin/liquibase без префиксов
2026-05-03 18:16:21 +03:00
Zimin A.N. 7c06cf7f71 ci: pull images через repo.nstart.cloud (corp Docker Hub mirror)
Runners в кластере не имеют network access к Docker Hub.
Заменили все base images:
- maven:3.9.9-eclipse-temurin-21 → repo.nstart.cloud/maven:...
- eclipse-temurin:21-jre → repo.nstart.cloud/eclipse-temurin:21-jre
- liquibase/liquibase:4.27.0 → repo.nstart.cloud/liquibase/...
- docker:27 / docker:27-dind → repo.nstart.cloud/docker:...
2026-05-03 18:02:46 +03:00
Zimin A.N. 4723af5bca feat(docker): Dockerfile для read-api и projection-writer
Multi-stage build (Maven 3.9.9 + Eclipse Temurin 21 → eclipse-temurin:21-jre):
- ordinis-read-api/Dockerfile (port 8081)
- ordinis-projection-writer/Dockerfile (port 8082)

JVM args: -XX:+UseZGC -XX:MaxRAMPercentage=75 (low-pause GC + container-aware
memory). MaxRAMPercentage важен потому что Helm задаёт container memory
limit через resources.limits.memory; JVM должна respect'ить это.

Helm chart (terravault/ordinis-infra/charts/ordinis/) теперь имеет полный
набор images для deploy:
  registry.k8s.265.nstart.local/terravault/ordinis-app:<tag>
  registry.k8s.265.nstart.local/terravault/ordinis-read-api:<tag>
  registry.k8s.265.nstart.local/terravault/ordinis-projection-writer:<tag>
  registry.k8s.265.nstart.local/terravault/ordinis-migrations:<tag>
2026-05-03 16:10:21 +03:00
Zimin A.N. 6a6067e70b feat(projection-writer): Kafka consumer + Redis materialization per-locale
ordinis-projection-writer полностью реализован, last module:
- OrdinisProjectionWriterApplication: Spring Boot main + JpaRepositories
- RedisKeys: convention 'ord:<bundle>:<dictionary>:<bk>:<locale>' для flat
  плюс ':_raw' для multi-locale, плюс 'ord:idx:<bundle>:<dictionary>' set index
- ProjectionWriter: для каждой supported locale справочника flatten data и
  записать в Redis. Также пишет _raw multi-locale ключ. На delete стирает
  все варианты + удаляет из index. Метрики ordinis_projection_records_*
- RecordEventListener: @KafkaListener на 3 scope-топика, group
  ordinis-projection-cuod. Парсит EventEnvelope, разбирает payload.type
  (RecordCreated/Updated/Deleted), вызывает upsert или delete.
- application.yml profile-based: dev (port-forward), k8s (Vault role
  ordinis-projection-writer)
- logback-spring.xml как у других сервисов

ordinis-events-api: добавлен явный 'type' accessor в каждом record
(@JsonProperty type). Без него Jackson не сериализовал discriminator
из @JsonTypeInfo (при generic T payload Jackson не знает declared type).

ordinis-infra: Redis StatefulSet (redis:7.4-alpine, AOF, maxmemory 256mb
LRU, secret password) в namespace ordinis-dev на :6379.

E2E (3 сервиса параллельно):
  POST /spacecraft/records {AQUA-1, name:{ru-RU:'Аква',en-US:'Aqua'}, ...}
  → outbox row id=7 published_at=true
  → ordinis_outbox_published_total +1
  → projection log: 'Projection upserted: spacecraft:AQUA-1 (2 locales)'
  → Redis ord:cuod:spacecraft:AQUA-1:ru-RU = {name:'Аква',...}
  → Redis ord:cuod:spacecraft:AQUA-1:en-US = {name:'Aqua',...}
  → Redis ord:cuod:spacecraft:AQUA-1:_raw  = {name:{ru-RU,en-US},...}
  → Redis ord:idx:cuod:spacecraft set member 'AQUA-1'

10 of 10 модулей реализованы. Last module done.
2026-05-03 15:59:40 +03:00
Zimin A.N. d1c8d5755e fix(scaffolding): make ordinis-app boot in dev profile
- Remove spring-cloud-starter-bootstrap (deprecated в SC 2024.x; используем
  spring.config.import вместо bootstrap context)
- Profile-based config: dev отключает Cloud Config + Vault + OTel + JPA
  auto-config для standalone smoke testing; k8s profile подключает реальную
  инфру (vault + config server)
- Dockerfile: COPY всех модулей parent pom их видит при reactor
- Уберём spring-boot-maven-plugin из read-api/projection-writer пока нет main

Validated:
- mvn install all 11 modules: BUILD SUCCESS
- Spring Boot стартует за 2.7s в dev profile
- /actuator/health UP, /actuator/prometheus отдаёт http_server_requests + jvm
- HelloController отвечает на GET /
2026-05-03 01:03:03 +03:00
Zimin A.N. a28fd0b352 feat: Maven multimodule scaffolding for Ordinis MDM service
- 10 модулей (domain, validation, events-api, outbox, rest-api, app, read-api, projection-writer, cuod-bundle, migrations)
- Parent pom + BOM с Spring Boot 3.4.2, Spring Cloud 2024.0.0, Vault Config 4.2.0
- ordinis-app: Spring Boot main с Actuator/Prometheus/Logstash JSON encoder/OpenTelemetry/Cloud Config/Vault skeleton
- ordinis-migrations: Liquibase changelogs (dictionary_definitions/records, audit_log, outbox_events, idempotency_keys, EXCLUSION CONSTRAINT через btree_gist)
- Multi-stage Dockerfiles для app и migrations
2026-05-03 00:41:53 +03:00