add-keycloakx: keycloak 자체 빌드 하드닝 이미지 추가 (차단 CVE 17건 → 0건)
PR #18 이 카탈로그에 넣는 quay.io/keycloak/keycloak:26.6.4 가 게이트에서 차단 17건(실효 HIGH 17 / CRITICAL 0)이었다. sbom.yml 이 warn-only 라 PR 은 통과했지만 실제로는 게이트 실패 상태로 카탈로그에 들어간다. ## 상위 태그·베이스 OS 교체를 먼저 검토한 결과 차단 17건 중 12건이 배포본에 함께 실린 jar 다. keycloak 26.6.4 와 최신 26.7.1 의 quarkus.version 이 둘 다 3.33.2.1 이고 그 BOM 이 netty 4.1.135.Final / jackson-bom 2.21.2 를 고정한다(keycloak pom.xml 두 태그 + quarkus BOM 실측). 필요한 수정 버전은 netty 4.1.136.Final, jackson 2.21.4 라 **상위 태그로도 풀리지 않고**, CVE 가 OS 패키지가 아니라 jar 자체라 **베이스 OS 교체도 통하지 않는다.** jar 를 직접 교체하는 자체 빌드가 유일한 수단이다 — etcd 이미지의 `go.work replace golang.org/x/text` 와 같은 성격의 의존성 override. ## images/keycloak/ 업스트림 quarkus/container/Dockerfile 을 기준으로 하되 셋이 다르다. 1. 런타임 rootfs 가 SUSE BCI. bci-micro 파일시스템을 **씨앗으로 깔고** 그 위에 zypper --installroot 로 설치한다. 업스트림 ubi-null.sh 처럼 별도 installroot 를 micro 위에 덮으면 micro 의 rpmdb 가 가려져 micro 자체 패키지가 SBOM 에서 통째로 사라진다 — CVE 가 주는 게 아니라 스캔 사각지대가 생긴다. 씨앗 방식으로 OS 패키지 65종이 정상적으로 잡히는 것을 SBOM 으로 확인했다. 2. 취약 jar 오버레이(overlay-jars.sh). netty 17종 → 4.1.136.Final, jackson core/databind → 2.21.4, pgjdbc → 42.7.12. Quarkus fast-jar 의 클래스패스가 파일명을 그대로 참조하므로 **파일명은 유지하고 내용만** 바꾸고 sha1 로 검증한다. trivy 는 jar 내부 메타데이터를 읽으므로 SBOM 에 새 버전이 정확히 잡힌다. 3. bin/client 제거. keycloak-admin-cli 가 jackson 을 shade 로 품은 uber-jar 라 교체가 불가능하다. 서버 JVM 이 로드하지 않는 독립 CLI 라 제거했다 — 업스트림 대비 유일한 기능적 차이이며 CUSTOM-README 에 대안을 적었다. 버전은 26.7.1 로 올렸다. 26.6.4 는 26.7.1(및 26.6.5)에서만 패치된 keycloak-services HIGH 5건(CVE-2026-16102/16442/16443/15572/15573)에 취약하다. 차트(keycloakx 7.2.2)는 최신이고 그대로 둔다 — appVersion 26.6.4 는 codecentric 의 릴리스 캐던스 지연이다. ## 베이스 OS 정책 확정 (image-authoring.md 원칙 2 미결 해소) SUSE BCI 로 통일하되 **버전은 이미지마다 실측해서 고른다.** BCI 16.0 이 나와 있지만 SLE_BCI 의 java-21-openjdk-headless 가 15.7 은 21.0.12, 16.0 은 21.0.11 이라 최신 베이스로 가면 CVE-2026-41254·CVE-2026-47063 이 오히려 남는다. bci-micro 에 sed·grep·find 가 셋 다 없다는 것과 SLE 패키지명 차이(tzdata→timezone 등)도 함께 기록했다. ## 실측 결과 로컬 빌드(linux/amd64) → verify.sh → SBOM → 전 심각도 스캔 → 게이트: 차단 17건 → **0건** (커버리지 자가진단 ok, OS=sles 15.7) 남은 1건 CVE-2025-59250 은 예외 등록했다 — 트리비가 같은 mssql-jdbc jar 하나로 컴포넌트를 둘 만들어(pom.properties 의 13.2.1.jre11 / 파일명의 13.2.1) 접미사가 잘린 쪽이 매칭된 파싱 오탐이다. 설치본은 FixedVersion 목록에 있는 13.2.1.jre11 이다. dev 클러스터 격리 네임스페이스(kc-test-build)에 cnpg-cluster + keycloakx 로 실배포 검증: Pod Running, jdbc-postgresql 연결, liquibase 스키마 생성, admin 부트스트랩 (KC-SERVICES0077), apisix ingress 경유 OIDC discovery 200 / admin 토큰 발급 / realm·client 생성(201) 까지 확인. 정리 시 Longhorn Volume 까지 삭제했다. ## custom-values.yaml ingress 수정 path 가 exact "/" 였다. apisix 에서는 루트만 매치되어 /realms/*, /admin/* 이 전부 404 가 난다 — airflow·superset·mlflow·lakekeeper 에서 이미 실측된 문제로 카탈로그가 regex 방식으로 통일돼 있다. path: /.* + k8s.apisix.apache.org/use-regex 로 맞췄고, 배포 검증에서 이 경로들이 실제로 뜨는 것을 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+79
-11
@@ -44,23 +44,91 @@ security-catalog 레포에서 검증한 자체 빌드 프레임워크를 포팅
|
||||
실행할 수 있다 — 호스트 실행이 상위 호환이다. 마지막 줄에 `VERIFY-OK` 를 출력해야
|
||||
통과로 판정된다.
|
||||
|
||||
## 원칙 2 — 최종 런타임 베이스 OS 는 아직 미결
|
||||
## 원칙 2 — 최종 런타임 베이스 OS 는 SUSE BCI
|
||||
|
||||
security-catalog 는 자체 빌드 이미지의 최종 런타임 베이스로 SUSE BCI 하나만 쓰기로
|
||||
결정했지만, 그 결정은 이 레포에 이식하지 않았다. **dip-catalog 는 베이스 OS 정책이
|
||||
아직 없다** — 처음 자체 빌드 이미지를 추가할 때 정하고, 결정 배경을 남긴다(카탈로그
|
||||
전용 `doc/decisions/` 관례는 아직 없으므로 우선 해당 PR 설명과 `MEMORY.md`에 기록).
|
||||
**배포판은 결정됐다(2026-08-07, `keycloak` 이미지 추가 PR).** 그전까지는
|
||||
"security-catalog 의 SUSE BCI 단일화 결정을 이식하지 않았다" 는 미결 상태였다.
|
||||
`keycloak` 은 업스트림이 UBI9 기반이라 "업스트림과 최대한 동일하게" 와 정면으로
|
||||
충돌했고, **카탈로그 내 일관성을 우선**했다 — 기존 3종이 전부 BCI 이고 trivy 의
|
||||
SLES 15.7 커버리지가 양성 대조로 실측 확인돼 있다
|
||||
(`doc/analysis/sles-oval-measurement.md`).
|
||||
|
||||
- "업스트림과 최대한 동일하게" 라는 기본 원칙과 특정 베이스 OS 채택이 충돌할 수 있다
|
||||
(예: 정적 링크 바이너리에 어떤 최소 이미지를 쓸지). 그 경우 무엇을 우선했는지와 왜인지
|
||||
기록한다.
|
||||
- 최소 이미지(distroless 류, BCI micro 류 등)는 `sed`/`grep` 같은 흔한 도구가 없을 수
|
||||
있다 — `verify.sh` 게스트 스크립트는 그런 도구에 의존하지 말고 순수 셸 루프
|
||||
(`while IFS= read -r line; do ...; done`)로 작성하는 편이 안전하다.
|
||||
- 이 정책과 "업스트림과 최대한 동일하게" 가 충돌하면 **무엇을 우선했는지와 왜인지를
|
||||
해당 이미지 README 에 남긴다.** 정책이 있다고 기록을 생략하지 않는다.
|
||||
- 빌더 스테이지(컴파일용, 최종 이미지에 남지 않는 스테이지)는 이 정책 대상이 아니다 —
|
||||
공식 언어 이미지(`golang` 등)를 그대로 써도 된다. 정책이 적용되는 것은 **스캔·배포
|
||||
대상인 최종 스테이지**뿐이다.
|
||||
|
||||
### 어느 BCI 버전을 쓸지는 이미지마다 실측해서 정한다
|
||||
|
||||
**"최신 BCI 를 쓴다" 는 규칙을 두지 않는다.** 새 SLE 메이저의 SLE_BCI 저장소가 특정
|
||||
패키지에서 구버전에 뒤처져 있을 수 있고, 그러면 최신 베이스가 오히려 CVE 를 남긴다.
|
||||
|
||||
실례 — `keycloak` 이미지에서 15.7 을 고른 근거(2026-08-07 실측):
|
||||
|
||||
| BCI | `java-21-openjdk-headless` |
|
||||
| --- | --- |
|
||||
| 15.7 | `21.0.12.0-150600.3.29.1` |
|
||||
| 16.0 | `21.0.11.0-160000.2.1` |
|
||||
|
||||
`21.0.12` 가 CVE-2026-41254·CVE-2026-47063 의 수정 버전이라 16.0 으로 갔으면 차단
|
||||
CVE 2건이 그대로 남았다. **이 이미지가 실제로 필요로 하는 패키지의 버전을 후보 태그마다
|
||||
직접 재고 결과를 `build.env` 주석에 남긴다.**
|
||||
|
||||
```sh
|
||||
docker run --rm registry.suse.com/bci/bci-base:<태그> \
|
||||
sh -c 'zypper -n refresh >/dev/null 2>&1; zypper -n info <패키지>'
|
||||
```
|
||||
|
||||
새 버전으로 올릴 때는 trivy 의 해당 SLE 버전 커버리지도 다시 확인한다 —
|
||||
`CoverageProbe` 가 `none` 이면 findings 0 이 진짜 0 이 아니다(게이트가 막는다).
|
||||
|
||||
### `bci-micro` 위에 패키지를 얹을 때 — rootfs 는 반드시 "씨앗" 방식으로
|
||||
|
||||
`bci-micro` 는 패키지 매니저가 없지만 **rpmdb 는 갖고 있다**
|
||||
(`/usr/lib/sysimage/rpm`, 2026-08-07 실측). 빈 installroot 에 설치한 rootfs 를 micro
|
||||
위에 그냥 덮으면 **micro 의 rpmdb 가 가려져 micro 자체 패키지가 SBOM 에서 통째로
|
||||
사라진다** — CVE 가 줄어드는 게 아니라 스캔 사각지대가 생기는 것이다.
|
||||
|
||||
micro 의 파일시스템을 씨앗으로 깔고 그 위에 설치한다:
|
||||
|
||||
```dockerfile
|
||||
FROM registry.suse.com/bci/bci-micro:15.7 AS micro
|
||||
FROM registry.suse.com/bci/bci-base:15.7 AS builder
|
||||
COPY --from=micro / /rootfs
|
||||
RUN rpm --root /rootfs --import /usr/lib/rpm/gnupg/keys/*.asc && \
|
||||
zypper --non-interactive --installroot /rootfs --gpg-auto-import-keys refresh && \
|
||||
zypper --non-interactive --installroot /rootfs install -y --no-recommends <패키지들>
|
||||
FROM scratch AS final
|
||||
COPY --from=builder /rootfs/ /
|
||||
```
|
||||
|
||||
`rpm --import` 를 빠뜨리면 설치되는 패키지마다 `NOKEY` 경고로 개별 서명 검증이
|
||||
생략된다. 빌드 후 SBOM 의 OS 패키지 수도 반드시 확인한다 — 한 자릿수로 떨어졌으면
|
||||
마스킹이 일어난 것이다. 실례는 `images/keycloak/suse.Dockerfile`.
|
||||
|
||||
### `bci-micro` 에 없는 흔한 도구 (2026-08-07 실측)
|
||||
|
||||
`sed`·`grep`·`find` 가 **셋 다 없다**. `bash`·`coreutils`·`readlink`·`dirname`·
|
||||
`uname`·`locale` 은 있다.
|
||||
|
||||
- 최종 이미지의 앱이 이 도구들을 쓰면(예: Keycloak `bin/kc.sh` 는 `sed`·`grep` 을
|
||||
쓴다) 런타임 패키지 목록에 **명시적으로 넣어야 한다.**
|
||||
- `verify.sh` 의 **게스트** 스크립트는 이 도구들에 의존하지 말고 순수 셸 루프
|
||||
(`while IFS= read -r line; do ...; done`)로 작성한다 — 이미지마다 설치 여부가 다르다.
|
||||
|
||||
### SLE 패키지명이 RHEL/Debian 과 다른 것들 (실측)
|
||||
|
||||
| 다른 배포판 | SLE_BCI 15.7 |
|
||||
| --- | --- |
|
||||
| `tzdata` | `timezone` |
|
||||
| `tzdata-java` | **없음** (JDK 내장 tzdb 사용) |
|
||||
| `glibc-langpack-en` | `glibc-locale-base` |
|
||||
| `coreutils-single` | `coreutils` |
|
||||
|
||||
업스트림 Dockerfile 의 패키지 목록을 그대로 옮기면 `No provider of '...' found` 로
|
||||
빌드가 실패한다. `zypper -n search -t package '<패턴>'` 로 먼저 확인한다.
|
||||
|
||||
## 두 가지 유형 (둘 다 같은 스크립트를 쓴다)
|
||||
|
||||
| 유형 | Dockerfile 이 하는 일 | 셸 유무 |
|
||||
|
||||
Reference in New Issue
Block a user