CVE 패치 일괄 작업 — Netty 4.1.119 → 4.1.133 을 BOM 한 줄로 잡은 이야기
Trivy CI 가 매일 SARIF 를 뱉으면서 4 개 백엔드 서비스에 Netty CVE 12 개 가 빨간 줄로 뜨고 있었습니다. 단순한 패치 같았는데 첫 시도(PR #1) 가 실패했고, 두 번째(PR #2) 에서 BOM 패턴으로 잡았습니다. 그 과정 정리.
이 글에서 다루는 것
- CVE 무엇이었나 — Netty 4.1.119 의 알려진 12 개 취약점
- PR #1 —
constraints { ... }만으로는 왜 부족했나 (transitive 못 잡음)- PR #2 —
dependencyManagement { mavenBom("io.netty:netty-bom:...") }로 일괄 처리- Trivy CI 결과로 검증
- GitHub Actions 의 함정 (대소문자, SARIF 업로드, 푸시 순서)
1. 문제 — 4 개 서비스의 동일 CVE
api-gateway, user-service, catalog-service, media-service — 모두 Spring Boot 3.4.1 + Spring Cloud Gateway. Trivy 결과:
HIGH/CRITICAL CVE in netty-codec-4.1.119.Final.jar
- CVE-2025-XXXXX (HTTP/2 부분 응답 처리 결함)
- CVE-2025-XXXXX (Bzip2 디컴프레서 메모리 누수)
- CVE-2025-XXXXX (DoS via malformed multipart)
- ... 9 more
Netty 4.1.133 (당시 최신 stable) 에서 다 패치되어 있었습니다. 단순히 4 개 서비스의 의존성 버전을 올리면 끝.
2. PR #1 — constraints { } 만으로 시도 (실패)
처음 시도한 패치:
// services/api-gateway/build.gradle.kts
dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
constraints {
implementation("io.netty:netty-codec:4.1.133.Final")
implementation("io.netty:netty-codec-http:4.1.133.Final")
implementation("io.netty:netty-codec-http2:4.1.133.Final")
implementation("io.netty:netty-handler:4.1.133.Final")
}
}
빌드 → 푸시 → Trivy 재스캔. 결과:
HIGH CVE in netty-buffer-4.1.119.Final.jar ← 안 잡힘
HIGH CVE in netty-common-4.1.119.Final.jar ← 안 잡힘
HIGH CVE in netty-resolver-4.1.119.Final.jar ← 안 잡힘
constraints 에 적은 4 개는 4.1.133 으로 잡혔는데, transitive 로 깔려오는 다른 netty 모듈 (netty-buffer, netty-common, netty-resolver, netty-transport, …) 은 여전히 4.1.119. Spring Cloud Gateway 가 끌고 오는 의존성 트리가 깊어서 일일이 다 못 잡았습니다.
⚠️ 교훈:
constraints는 “내가 적은 모듈” 만 강제. transitive 의 다른 모듈은 BOM 같은 도구가 필요.
3. PR #2 — Netty BOM 으로 일괄 (성공)
수정:
// services/api-gateway/build.gradle.kts
plugins {
id("io.spring.dependency-management") version "1.1.7"
}
dependencyManagement {
imports {
mavenBom("io.netty:netty-bom:4.1.133.Final") // ★ 한 줄
}
}
dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
// constraints 다 지움
}
netty-bom 이 모든 netty-* 모듈의 버전을 4.1.133 으로 일괄 고정합니다. transitive 든 직접이든 무조건 그 버전. PR #1 처럼 모듈명을 일일이 적을 필요 없음.
같은 작업을 4 개 서비스 build.gradle.kts 에 동일하게 적용 → 푸시.
곁다리 — AWS SDK 도 같이
같은 PR 에서 software.amazon.awssdk:bom:2.29.20 → 2.31.7 도 올렸습니다 (Trivy 가 같이 빨갰음).
dependencyManagement {
imports {
mavenBom("io.netty:netty-bom:4.1.133.Final")
mavenBom("software.amazon.awssdk:bom:2.31.7")
}
}
BOM 두 개로 30+ 모듈 한 방에 정리.
4. Trivy 검증
이미지 빌드 후 ghcr 에 푸시 → Actions 에서 Trivy 자동 재스캔:
✅ api-gateway:latest No HIGH/CRITICAL netty CVE
✅ user-service:latest No HIGH/CRITICAL netty CVE
✅ catalog-service:latest No HIGH/CRITICAL netty CVE
✅ media-service:latest No HIGH/CRITICAL netty CVE
12 개 CVE × 4 서비스 = 48 개 빨간 줄이 0 으로 ✅
5. GitHub Actions 의 함정 — 빠진 것들
CI 자동화 과정에서 4 가지 사고를 만났습니다.
5.1 ghcr 태그 대소문자
# 처음 (실패)
docker push ghcr.io/$/api-gateway:latest
# → ghcr.io/MyoungSoo7/api-gateway:latest
# Error: invalid reference format: repository name must be lowercase
해결: 소문자 하드코딩.
docker push ghcr.io/myoungsoo7/api-gateway:latest
(또는 $ 같은 expression 도 가능 — 환경에 따라 다름)
5.2 Trivy SARIF 업로드 실패 (private repo)
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: trivy-results.sarif }
private repo 는 GitHub Code Scanning 이 유료 → SARIF 업로드 거절됨. 해결: SARIF 업로드 제거, 그냥 Trivy 표 형태 출력만 남김.
- uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/myoungsoo7/api-gateway:latest
format: table # sarif 대신 table
severity: HIGH,CRITICAL
5.3 Push 순서 — 스캔이 푸시를 막으면 안 됨
처음 워크플로우:
- name: Trivy scan
run: trivy image ... # 실패하면 다음 스텝 안 감
- name: Push image
run: docker push ... # ← Trivy 결과로 막힘
이러면 CVE 가 뜬 이미지를 못 올림 → 패치 검증 자체가 안 됨. 순서 뒤집어서:
- name: Push image
run: docker push ...
- name: Trivy scan
continue-on-error: true # 결과는 보지만 빌드는 안 막음
run: trivy image ...
Trivy 는 모니터링 이지 게이트가 아닙니다. 게이트로 쓰려면 별도 PR 단계에서.
5.4 gradlew 가 repo 에 없음
- run: ./gradlew build
# Error: ./gradlew: No such file or directory
해결: gradle/actions/setup-gradle@v4 로 gradle 자체를 받아서 직접 호출.
- uses: gradle/actions/setup-gradle@v4
- run: gradle build --no-daemon
6. 최종 워크플로우 (요약)
name: build-and-push
on:
push:
branches: [main]
jobs:
backend:
strategy:
matrix:
service: [api-gateway, user-service, catalog-service, media-service]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/actions/setup-gradle@v4
- run: gradle :services:$:bootJar --no-daemon
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: myoungsoo7
password: $
- run: |
docker build -t ghcr.io/myoungsoo7/$:latest \
-t ghcr.io/myoungsoo7/$:$ \
services/$
docker push --all-tags ghcr.io/myoungsoo7/$
- uses: aquasecurity/trivy-action@master
continue-on-error: true
with:
image-ref: ghcr.io/myoungsoo7/$:latest
format: table
severity: HIGH,CRITICAL
4 개 서비스가 matrix 로 병렬 빌드. PR 한 번 = 4 이미지 동시 푸시 + 4 스캔. 약 6 분.
7. 회고
- PR #1 가 실패한 게 사실 더 좋은 학습 이었습니다. constraints 와 BOM 의 차이를 몸으로 배움.
- BOM 패턴은 Spring Boot starter parent 가 이미 쓰는 방식. Spring 진영에서 의존성 관리하는 방법을 그대로 따라가는 게 가장 안전합니다.
- Trivy 는 “현재 떠 있는 이미지” 가 아니라 “ghcr 에 올라간 이미지” 를 검사합니다. 운영 K3s 이미지가 정말 패치된 버전인지는 Image rollout 이 별도로 필요합니다 (다음 글).
다음 글
- 운영 K3s 에 새 이미지 롤링 적용 — kubectl rollout 과 ArgoCD image-updater
- BOM 으로 패치한 이미지를 실제 Pod 에 반영시키는 단계.