Три прямых фикса по /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.
Implements the Phase 2 hook from the previous commit. With this in
place, UserDisplayService.find chains:
hot cache → DB (user_display_cache) → Keycloak Admin API
so any realm user resolves on first display, even if they never made
an authenticated request to ordinis. The resolved entry is persisted
via UserDisplayService.upsert with source=KEYCLOAK_ON_DEMAND, so the
next render of the same sub hits the DB tier (~ms) instead of going
back to Keycloak.
Implementation notes:
- Spring RestClient (modern blocking HTTP, replaces RestTemplate).
- client_credentials grant against
{issuer}/realms/{realm}/protocol/openid-connect/token. Token cached
in-memory minus a 30s safety margin from `expires_in`.
- 401 on user lookup wipes the cached token and surfaces a
RuntimeException, so the next resolve refetches.
- 404 on user lookup → Optional.empty (definitive miss, sub not in
realm). UserDisplayService logs and the caller falls back to short
UUID. Negative cache TTL not implemented — added complexity not
justified for the current realm size (~50 users).
- Bean activated only when ordinis.keycloak.admin.enabled=true AND
the env vars are set. Default disabled — UserDisplayService treats
KeycloakUserResolver as @Autowired(required = false), so a
deployment without Keycloak admin creds keeps the previous behavior
(DB cache only) instead of crashing on startup.
Application config:
- ORDINIS_KEYCLOAK_ADMIN_ENABLED (false by default)
- ORDINIS_KEYCLOAK_ADMIN_URL e.g. https://auth.nstart.space
- ORDINIS_KEYCLOAK_ADMIN_REALM defaults to nstart
- ORDINIS_KEYCLOAK_ADMIN_CLIENT_ID via vault (see docs-internal/keycloak-admin-setup.md)
- ORDINIS_KEYCLOAK_ADMIN_CLIENT_SECRET via vault
Setup runbook (docs-internal/keycloak-admin-setup.md) covers:
- creating the Keycloak service-account client + assigning view-users
- writing the secret into both vault instances (different per env)
- wiring vault.hashicorp.com agent-inject annotations into the chart
- verification steps + common failure modes
Phase 3 (scheduled bulk sync) is documented at the end of the runbook
but deferred — depends on observed cache miss rate after this lands.
OpenApiConfig:
- Добавлена OAuth2 security scheme (authorization_code flow с Keycloak
realm `nstart`). Auth/token URLs из ordinis.openapi.oauth.issuer-uri
configuration property (default: auth.nstart.space/realms/nstart).
- BearerAuth scheme сохранён secondary — для pre-issued tokens из CI/scripts.
- Servers list переупорядочен — prod первый, staging второй.
- Description обновлён с pointer на /docs/ для full integration guide.
application.yml:
- springdoc.swagger-ui.oauth: client-id (env override), use-pkce-with-
authorization-code-grant=true. Public client `ordinis-swagger`
настраивается отдельно в Keycloak — see keycloak-setup-swagger-client.md
в ordinis-infra repo.
- ORDINIS_OPENAPI_OAUTH_ISSUER_URI env var для override realm на разных
окружениях.
docs/integration/api/read-endpoints.md:
- Swagger UI section обновлён: акцент на Authorize button + Keycloak
PKCE login (no client_secret needed), tip про bearerAuth fallback.
docs/index.md:
- Добавлен tab "Я хочу пощупать API" с link на Swagger UI.
После deploy:
- https://ordinis.k8s.264.nstart.cloud/swagger-ui.html — Swagger UI
- https://ordinis.k8s.264.nstart.cloud/v3/api-docs — OpenAPI spec
Authorize → выбрать oauth2 → Keycloak login (PKCE) → Try it out с JWT.
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 графики.
Tempo на staging (tempo.monitoring:4318) — single-process, принимает
только traces. Spring Boot OTel autoconfig по умолчанию отправляет всё
по тому же endpoint → 404 на каждой попытке (раз в секунду в логах
writer/read-api/projection-writer).
Override через ENV — можно переключить когда поднимем полный stack
(Mimir для metrics, Loki для logs).
Метрики Prometheus продолжают работать через actuator/prometheus
(scrape снаружи), это отдельный path.
Новый модуль ordinis-auth:
- OrdinisAuthProperties — public-key, issuer, requireAuthentication, allowQueryScope
- ScopeContext — резолвит current scopes из JWT claim 'scopes' (приоритет),
потом legacy ?as_scope=, потом anonymous=PUBLIC
- SecurityConfig — Spring Security OAuth2 Resource Server + RSA public-key
decoder. Если public-key пуст — JWT validation отключён (permissive
fallback); rollout без auth-сервера не ломает API.
Read controller теперь делегирует scope resolution в ScopeContext вместо
прямого парсинга ?as_scope=. Backward compat: legacy query параметр
работает пока ordinis.auth.allow-query-scope=true (по умолчанию true в
dev/staging, false в prod после интеграции с @nstart/auth).
Vault: public-key читается из secret/ordinis/cuod/jwt-public-key (key=key).
Spring Cloud Vault flatten'ит в property 'key', resolved в
ordinis.auth.public-key=${key:}.
Stack trace showed io.opentelemetry.exporter.internal.http.HttpExporter
sending to :4317 — Tempo on 4317 expects gRPC frames, gets HTTP, sends
RST. Switch endpoint to :4318 (Tempo OTLP HTTP receiver) and protocol to
http/protobuf so the SDK and Tempo speak the same protocol.
The gRPC config never took effect — Spring Boot SDK on micrometer-tracing-
otlp pulls in only the HTTP exporter by default; gRPC needs the explicit
opentelemetry-exporter-otlp jar.
Hibernate validate mode threw 'wrong column type [supported_locales]:
found [_text (Types#ARRAY)] but expecting [text[] (Types#OTHER)]' against
the Liquibase-managed schema. This is a well-known hypersistence-utils +
PostgreSQL TEXT[] friction with Hibernate validate.
Schema is owned by Liquibase migrations; validate adds no safety here. Set
to 'none' (matches read-api and projection-writer).
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).
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.
- 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 /