Files
mdm-ordinis/ordinis-app/src/main/resources/application.yml
T
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

279 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
spring:
application:
name: ordinis-app
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
server:
port: 8080
shutdown: graceful
management:
endpoints:
web:
exposure:
# CSO finding #3 — defense-in-depth. Default exposure narrowed к трём
# безопасным: health (k8s probes), info (UI banner), prometheus
# (ServiceMonitor scrape). Sensitive endpoints (env, configprops,
# beans, mappings, conditions, loggers) выключены by default —
# утечка ORDINIS_KEYCLOAK_ADMIN_CLIENT_SECRET через /actuator/env
# потенциальна на любой pod-to-pod атаке.
#
# Diagnostic четвёрка (conditions/beans/configprops/loggers) +
# debug-set (env/mappings/refresh/metrics) включаются через env
# override `MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE` на staging
# — для autoconfig debug (например: почему
# MailSenderAutoConfiguration не fired при наличии SPRING_MAIL_HOST
# env var). На prod env override не ставим → defense вместе с
# SecurityConfig denyAll на /actuator/{env,beans,...}.
include: ${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE:health,info,prometheus}
base-path: /actuator
endpoint:
health:
probes:
enabled: true
show-details: when-authorized
group:
liveness:
include: livenessState
readiness:
include: readinessState,db
health:
# Redis health indicator отключён в writer pod. Writer не использует Redis
# напрямую (нет @Cacheable, нет RedisTemplate), но depends на
# ordinis-notifications module который включает spring-boot-starter-data-redis
# transitive'но (для будущего @EnableCaching). Spring autoconfig пытается
# connect к localhost:6379 → fail → /actuator/health = DOWN, забивает
# readiness alerts.
# Если writer когда-либо понадобится реальный Redis — wire env vars
# (см. projection-writer.yaml образец) и убрать этот override.
redis:
enabled: false
prometheus:
metrics:
export:
enabled: true
metrics:
tags:
application: ${spring.application.name}
environment: ${ENVIRONMENT:dev}
distribution:
# Server-side histogram buckets для http.server.requests — нужны
# histogram_quantile() в Grafana p50/p95/p99 dashboards.
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 5ms,10ms,25ms,50ms,100ms,250ms,500ms,1s,2s,5s
otel:
exporter:
otlp:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://tempo.monitoring:4318}
protocol: http/protobuf
# Tempo (single-process на staging) принимает только traces. Если включить
# экспорт metrics/logs — каждую секунду 404 в логах ("Failed to export logs").
# Когда поднимем полный observability stack (Tempo + Mimir + Loki) — снимем
# эти overrides либо переопределим через ENV.
metrics:
exporter: ${OTEL_METRICS_EXPORTER:none}
logs:
exporter: ${OTEL_LOGS_EXPORTER:none}
resource:
attributes:
service.name: ${spring.application.name}
service.namespace: ordinis
deployment.environment: ${ENVIRONMENT:dev}
traces:
sampler: parentbased_traceidratio
sampler.arg: ${ORDINIS_TRACING_SAMPLING:0.1}
logging:
level:
root: INFO
cloud.nstart.terravault.ordinis: DEBUG
ordinis:
outbox:
enabled: ${ORDINIS_OUTBOX_ENABLED:true}
poll-interval-ms: ${ORDINIS_OUTBOX_POLL_INTERVAL_MS:1000}
batch-size: ${ORDINIS_OUTBOX_BATCH_SIZE:100}
# Cap на ретраи: после N попыток событие → DLQ (dlq_at), poller его пропускает.
# 1000 при poll=1s ≈ ~17 минут активных ошибок (или быстрее на fail-fast топиках).
attempt-max: ${ORDINIS_OUTBOX_ATTEMPT_MAX:1000}
springdoc:
# Per env policy:
# prod → swagger UI disabled (real-user environment, не для exploration);
# api-docs JSON остаётся для SDK generation / machine consumers
# staging/dev → оба enabled (interactive playground для интеграторов)
# Controlled via ORDINIS_SWAGGER_UI_ENABLED env var (helm values-prod.yaml:
# springdoc.swaggerUi=false). Default true для backward compat.
api-docs:
path: /v3/api-docs
enabled: ${ORDINIS_SWAGGER_API_DOCS_ENABLED:true}
swagger-ui:
path: /swagger-ui.html
enabled: ${ORDINIS_SWAGGER_UI_ENABLED:true}
operations-sorter: method
tags-sorter: alpha
display-request-duration: true
# OAuth2 / Keycloak interactive login (PKCE flow). См. OpenApiConfig.
# Public client `ordinis-swagger` создаётся в Keycloak realm `nstart` с
# redirect URI = https://<host>/swagger-ui/oauth2-redirect.html
oauth:
client-id: ${ORDINIS_SWAGGER_CLIENT_ID:ordinis-swagger}
use-pkce-with-authorization-code-grant: true
use-basic-authentication-with-access-code-grant: false
scope-separator: " "
show-actuator: false
packages-to-scan: cloud.nstart.terravault.ordinis.restapi.web
---
# DEV profile: подключается к cluster PG+Kafka через kubectl port-forward.
# Перед запуском: kubectl port-forward -n ordinis-staging svc/ordinis-postgres-rw 5432:5432 &
# kubectl port-forward -n ordinis-staging svc/ordinis-kafka-kafka-bootstrap 9092:9092 &
spring:
config:
activate:
on-profile: dev
cloud:
config:
enabled: false
vault:
enabled: false
autoconfigure:
exclude:
- org.springframework.cloud.vault.config.VaultAutoConfiguration
- org.springframework.cloud.vault.config.VaultObservationAutoConfiguration
- org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration
datasource:
url: jdbc:postgresql://${ORDINIS_PG_HOST:localhost}:${ORDINIS_PG_PORT:5432}/${ORDINIS_PG_DB:ordinis}
username: ${ORDINIS_PG_USER:ordinis_app}
password: ${ORDINIS_PG_PASSWORD:}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate.format_sql: true
hibernate.jdbc.lob.non_contextual_creation: true
open-in-view: false
kafka:
bootstrap-servers: ${ORDINIS_KAFKA_BOOTSTRAP:localhost:9092}
properties:
security.protocol: ${ORDINIS_KAFKA_SECURITY:SASL_PLAINTEXT}
sasl.mechanism: ${ORDINIS_KAFKA_SASL_MECHANISM:SCRAM-SHA-512}
sasl.jaas.config: >-
org.apache.kafka.common.security.scram.ScramLoginModule required
username="${ORDINIS_KAFKA_USER:ordinis-app}"
password="${ORDINIS_KAFKA_PASSWORD:}";
producer:
acks: all
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
properties:
enable.idempotence: true
compression.type: lz4
otel:
sdk:
disabled: true
---
# K8s profile: реальная инфра в кластере. Cloud Config + Vault Kubernetes auth.
spring:
config:
activate:
on-profile: k8s
import:
- "configserver:${SPRING_CLOUD_CONFIG_URI:http://config-server.ordinis-staging:8888}"
- "vault://"
cloud:
config:
enabled: true
vault:
enabled: true
uri: ${VAULT_URI:http://vault.config:8200}
authentication: KUBERNETES
kubernetes:
role: ordinis-app
service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
kv:
enabled: true
backend: secret
application-name: ordinis/cuod
datasource:
url: jdbc:postgresql://${ORDINIS_PG_HOST}:${ORDINIS_PG_PORT:5432}/${ORDINIS_PG_DB:ordinis}
username: ${ORDINIS_PG_USER}
password: ${ORDINIS_PG_PASSWORD}
driver-class-name: org.postgresql.Driver
kafka:
bootstrap-servers: ${ORDINIS_KAFKA_BOOTSTRAP}
properties:
security.protocol: ${ORDINIS_KAFKA_SECURITY:SASL_PLAINTEXT}
sasl.mechanism: ${ORDINIS_KAFKA_SASL_MECHANISM:SCRAM-SHA-512}
sasl.jaas.config: >-
org.apache.kafka.common.security.scram.ScramLoginModule required
username="${ORDINIS_KAFKA_USER}"
password="${ORDINIS_KAFKA_PASSWORD}";
producer:
acks: all
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
properties:
enable.idempotence: true
compression.type: lz4
jpa:
hibernate:
ddl-auto: none
open-in-view: false
# Auth (см. ordinis-auth: SecurityConfig). public-key из Vault.
ordinis:
auth:
public-key: ${key:}
issuer: ${jwt-issuer:}
require-authentication: ${ORDINIS_AUTH_REQUIRED:false}
allow-query-scope: ${ORDINIS_AUTH_ALLOW_QUERY_SCOPE:true}
# AI Schema Assist (Phase 1 step 3-6). Off by default — controllers/services
# bean-conditionally registered, frontend hides AI button when 404.
# Enable per-env через helm values (см. charts/ordinis/values-staging.yaml).
# endpoint: OpenAI-compatible /v1/chat/completions root (без /v1 suffix —
# adapter добавит сам). bearer-token optional (Ollama не требует, OpenAI да).
ai:
enabled: ${ORDINIS_AI_ENABLED:false}
endpoint: ${ORDINIS_AI_ENDPOINT:}
model: ${ORDINIS_AI_MODEL:qwen2.5:7b}
bearer-token: ${ORDINIS_AI_BEARER_TOKEN:}
timeout-seconds: ${ORDINIS_AI_TIMEOUT_SECONDS:15}
# Keycloak Admin REST integration for UserDisplayService Phase 2 — on-demand
# sub→user lookup when JWT capture cache + DB cache both miss. See
# KeycloakAdminUserResolver.java + 0023-user-display-cache.xml migration.
# Disabled by default; flip on per-environment when the service-account
# client + vault secret are provisioned.
keycloak:
admin:
enabled: ${ORDINIS_KEYCLOAK_ADMIN_ENABLED:false}
url: ${ORDINIS_KEYCLOAK_ADMIN_URL:}
realm: ${ORDINIS_KEYCLOAK_ADMIN_REALM:nstart}
client-id: ${ORDINIS_KEYCLOAK_ADMIN_CLIENT_ID:}
client-secret: ${ORDINIS_KEYCLOAK_ADMIN_CLIENT_SECRET:}
# Notifications module — push email for draft lifecycle events.
# Activated via ordinis.notifications.enabled=true (writer pod only).
# SMTP creds: spring.mail.* (standard Spring Boot mail properties).
# See ordinis-notifications module + migration 0021/0024.
notifications:
enabled: ${ORDINIS_NOTIFICATIONS_ENABLED:true}
poll-interval-ms: ${ORDINIS_NOTIFICATIONS_POLL_INTERVAL_MS:30000}
batch-size: ${ORDINIS_NOTIFICATIONS_BATCH_SIZE:50}
# Reviewer pool email — receives RecordDraftSubmitted / RecordDraftWithdrawn pings.
# Leave blank to disable reviewer-pool notifications (maker-only notifications still work).
reviewer-pool-email: ${ORDINIS_NOTIFICATIONS_REVIEWER_POOL_EMAIL:}
email:
from-address: ${ORDINIS_NOTIFICATIONS_FROM:}
base-url: ${ORDINIS_NOTIFICATIONS_BASE_URL:}