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.
This commit is contained in:
Zimin A.N.
2026-05-27 19:02:34 +03:00
parent d031743159
commit 7abff8f73f
8 changed files with 78 additions and 14 deletions
+10
View File
@@ -23,3 +23,13 @@ HELP.md
*.key *.key
.gstack/ .gstack/
.claude/ .claude/
# Local env files — CSO finding #2. dotenv pattern предполагает не-committed
# .env с реальными creds для local dev. Без этого правила `git add .` засосёт
# secrets. Whitelist .env.example / .env.template / .env.sample — шаблоны.
.env
.env.local
.env.*.local
*.env
!.env.example
!.env.template
!.env.sample
+8 -1
View File
@@ -30,7 +30,14 @@ RUN mvn -B -pl ordinis-app -am package -DskipTests \
-Dgit.commit.time="$GIT_TIME" -Dgit.commit.time="$GIT_TIME"
FROM repo.nstart.cloud/library/eclipse-temurin:21-jre FROM repo.nstart.cloud/library/eclipse-temurin:21-jre
# Non-root user — security hardening (CSO report 2026-05-27 finding #1).
# Eclipse-temurin runtime image не задаёт USER by default → JVM от root.
# Compromise через Spring Boot CVE / Jackson deserialization без USER
# становится root-в-контейнере → escalation surface на ноду. UID 10001
# не конфликтует с system users (0-999) и distribution-reserved (1000-9999).
RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app -s /sbin/nologin app
WORKDIR /app WORKDIR /app
COPY --from=build /build/ordinis-app/target/*.jar /app/app.jar COPY --from=build --chown=app:app /build/ordinis-app/target/*.jar /app/app.jar
USER app
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"] ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]
+15 -8
View File
@@ -12,14 +12,21 @@ management:
endpoints: endpoints:
web: web:
exposure: exposure:
# `conditions`, `beans`, `configprops`, `loggers` — diagnostic четвёрка # CSO finding #3 — defense-in-depth. Default exposure narrowed к трём
# для autoconfig debug (например: почему MailSenderAutoConfiguration # безопасным: health (k8s probes), info (UI banner), prometheus
# не fired при наличии SPRING_MAIL_HOST env var и starter-mail на # (ServiceMonitor scrape). Sensitive endpoints (env, configprops,
# classpath). `loggers` runtime-toggle позволяет крутить # beans, mappings, conditions, loggers) выключены by default —
# `org.springframework.boot.autoconfigure` в DEBUG без redeploy. # утечка ORDINIS_KEYCLOAK_ADMIN_CLIENT_SECRET через /actuator/env
# Endpoints sensitive — exposed только на staging behind cluster-internal # потенциальна на любой pod-to-pod атаке.
# Ingress (auth.required=true в prod). #
include: health,info,prometheus,refresh,metrics,env,mappings,conditions,beans,configprops,loggers # 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 base-path: /actuator
endpoint: endpoint:
health: health:
@@ -40,7 +40,35 @@ public class SecurityConfig {
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> { .authorizeHttpRequests(auth -> {
auth.requestMatchers("/actuator/**").permitAll(); // Actuator split — CSO finding #3.
// Раньше /actuator/** = permitAll → /actuator/env публично отдавал
// env vars (включая ORDINIS_KEYCLOAK_ADMIN_CLIENT_SECRET) любому
// pod'у в кластере. Сейчас защищены только три безопасных:
//
// - /actuator/health/** — k8s liveness/readiness probes (бессрочно
// open, иначе pod не Ready).
// - /actuator/info — build info (commit, tag) для UI banner.
// - /actuator/prometheus — Prometheus scrape (cluster-internal
// ServiceMonitor; в принципе можно гейтить по network policy,
// но scrape token convention'а не было).
//
// Остальные (env, configprops, beans, mappings, conditions,
// loggers, refresh, metrics) — denyAll. Если оператор хочет
// diagnose в проде — port-forward к pod и `curl localhost:8080`
// (внутри pod нет network policy), либо temporarily exposed
// через kubectl exec.
//
// Defense-in-depth: дополнительно values-prod.yaml ограничивает
// management.endpoints.web.exposure.include = health,info,prometheus
// на уровне Spring → даже если SecurityFilterChain поменяется,
// endpoint'ов просто не существует.
auth.requestMatchers(
"/actuator/health",
"/actuator/health/**",
"/actuator/info",
"/actuator/prometheus"
).permitAll();
auth.requestMatchers("/actuator/**").denyAll();
auth.requestMatchers("/error").permitAll(); auth.requestMatchers("/error").permitAll();
// Build-info endpoint — открыт без auth: UI polling с update-banner // Build-info endpoint — открыт без auth: UI polling с update-banner
// должен работать ДО login (anonymous landing → "новая версия" // должен работать ДО login (anonymous landing → "новая версия"
+5 -1
View File
@@ -7,7 +7,11 @@ COPY . ./
RUN mvn -B -pl ordinis-projection-writer -am package -DskipTests RUN mvn -B -pl ordinis-projection-writer -am package -DskipTests
FROM repo.nstart.cloud/library/eclipse-temurin:21-jre FROM repo.nstart.cloud/library/eclipse-temurin:21-jre
# Non-root user — security hardening (CSO report 2026-05-27 finding #1).
# См. ordinis-app/Dockerfile для rationale.
RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app -s /sbin/nologin app
WORKDIR /app WORKDIR /app
COPY --from=build /build/ordinis-projection-writer/target/*.jar /app/app.jar COPY --from=build --chown=app:app /build/ordinis-projection-writer/target/*.jar /app/app.jar
USER app
EXPOSE 8082 EXPOSE 8082
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"] ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]
@@ -14,7 +14,8 @@ management:
endpoints: endpoints:
web: web:
exposure: exposure:
include: health,info,prometheus,refresh,metrics # CSO finding #3 — defense-in-depth. См. ordinis-app/application.yml.
include: ${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE:health,info,prometheus}
endpoint: endpoint:
health: health:
probes: probes:
+6 -1
View File
@@ -7,7 +7,12 @@ COPY . ./
RUN mvn -B -pl ordinis-read-api -am package -DskipTests RUN mvn -B -pl ordinis-read-api -am package -DskipTests
FROM repo.nstart.cloud/library/eclipse-temurin:21-jre FROM repo.nstart.cloud/library/eclipse-temurin:21-jre
# Non-root user — security hardening (CSO report 2026-05-27 finding #1).
# См. ordinis-app/Dockerfile для rationale (compromise → root в контейнере
# = escalation surface на ноду). UID 10001 outside system+distro reserved.
RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app -s /sbin/nologin app
WORKDIR /app WORKDIR /app
COPY --from=build /build/ordinis-read-api/target/*.jar /app/app.jar COPY --from=build --chown=app:app /build/ordinis-read-api/target/*.jar /app/app.jar
USER app
EXPOSE 8081 EXPOSE 8081
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"] ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]
@@ -12,7 +12,9 @@ management:
endpoints: endpoints:
web: web:
exposure: exposure:
include: health,info,prometheus,refresh,metrics,env,mappings # CSO finding #3 — defense-in-depth. См. ordinis-app/application.yml
# для rationale (narrow default, env-override для staging diagnostic).
include: ${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE:health,info,prometheus}
endpoint: endpoint:
health: health:
probes: probes: