Files
service-catalog/images/keycloak/README.md
T
wbsong111 f9a2d8400f 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>
2026-08-07 13:49:42 +09:00

192 lines
11 KiB
Markdown
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.
# keycloak — 자체 빌드
업스트림 Keycloak 배포본(tar.gz)을 SUSE BCI rootfs 위에 재패키징하고, 배포본에 정적으로
들어 있는 취약 jar 를 수정 버전으로 교체한다.
`manifests/helm/keycloakx/7.2.2/custom-values.yaml``image.repository`/`image.tag`
이 산출물을 가리킨다.
신규 자체 빌드 이미지 추가 절차 전반은
[.claude/image-authoring.md](../../.claude/image-authoring.md) 참고.
> **자체 빌드는 대응 우선순위 3번이다.** 상위 태그 교체·베이스 OS 교체로 목표를
> 만족할 수 있으면 그 쪽을 쓴다. 자체 빌드는 업스트림 서명·provenance·SBOM attestation 을
> 잃고 재빌드 책임을 지는 선택이다.
## 왜 자체 빌드하나
업스트림 `quay.io/keycloak/keycloak:26.6.4` 는 게이트에서 **차단 17건(실효 HIGH 17,
CRITICAL 0)** 이다(PR #18`helm-catalog-sbom` run 31085455385 실측). 계층별로 성격이
전혀 다르다.
| 계층 | 건수 | 대표 CVE | 상위 태그·베이스 OS 교체로 풀리나 |
| --- | --- | --- | --- |
| UBI9 rpm | 5 | `java-21-openjdk-headless` ×3, `libacl`, `pcre2` | 일부. 3건은 rpm 최신화로 해소 |
| 번들 jar | 12 | netty ×6, jackson ×3, postgresql-jdbc, mssql-jdbc, keycloak-services | **아니다** |
### 상위 태그 교체를 먼저 검토한 결과 (image-authoring.md 체크리스트 1)
**jar 12건은 keycloak 버전을 올려도 풀리지 않는다.** keycloak `26.6.4` 와 최신
`26.7.1``quarkus.version` 이 둘 다 `3.33.2.1` 이고, Quarkus 3.33.2.1 BOM 이
`netty 4.1.135.Final` / `jackson-bom 2.21.2` 를 고정한다(실측: keycloak `pom.xml`
태그 비교 + `quarkusio/quarkus` `bom/application/pom.xml@3.33.2.1`). 차단 CVE 의 수정
버전은 netty `4.1.136.Final`, jackson `2.21.4` 다 — 다음 Quarkus BOM 이 올라오기 전까지
업스트림 이미지로는 방법이 없다.
**베이스 OS 교체도 jar 에는 통하지 않는다.** CVE 가 OS 패키지가 아니라 배포본에 함께
실려 있는 jar 자체이기 때문이다 — `cloudnative-pg`/`etcd` 의 정적 링크 Go 모듈과 같은
구조다.
그래서 **jar 를 직접 교체하는 자체 빌드가 유일한 수단**이다. `etcd` 이미지가
`go.work``replace golang.org/x/text` 한 줄을 넣어 해결한 것과 같은 성격의 의존성
override 이며, 오케스트레이션은 동일한
[scripts/build/build-hardened-image.sh](../../scripts/build/build-hardened-image.sh)
하나를 공유한다.
### 그래도 버전은 26.7.1 로 올린다
`26.6.4``26.7.1`(및 `26.6.5`)에서만 패치된 `keycloak-services` HIGH 5건에
취약하다 — CVE-2026-16102 / 16442 / 16443 / 15572 / 15573 (+ MEDIUM 2건, GitHub
Security Advisory 실측). 스캔 시점 trivy DB 에 아직 없어 게이트에 잡히지 않았을 뿐이다.
차트(`keycloakx`)는 **7.2.2 가 최신이고 그대로 둔다.** 차트의 `appVersion: 26.6.4`
`image.tag` 미지정 시의 기본값일 뿐이고(`templates/statefulset.yaml`
`.Values.image.tag | default .Chart.AppVersion`), 우리는 `custom-values.yaml` 에서
태그를 명시한다. appVersion 이 26.7.1 보다 낮은 것은 codecentric 차트의 릴리스 캐던스
지연이지 차트 결함이 아니다.
## 유형과 베이스 OS
**유형: "업스트림 산출물을 다른 배포판에 재설치"** (image-authoring.md 두 유형 중 첫
번째). Keycloak 을 소스에서 Maven 빌드하지 않는다 — 업스트림이 릴리스한 tar.gz 를
그대로 쓰고 런타임 rootfs 만 SUSE 로 바꾼다.
| 항목 | 값 |
| --- | --- |
| 배포본 | `github.com/keycloak/keycloak/releases/download/$KEYCLOAK_VERSION/keycloak-$KEYCLOAK_VERSION.tar.gz` |
| 빌더 | `registry.suse.com/bci/bci-base:15.7` (zypper 필요) |
| 최종 rootfs 씨앗 | `registry.suse.com/bci/bci-micro:15.7` |
| 최종 스테이지 | `FROM scratch` + 위 rootfs |
### 베이스 OS 결정 배경 (image-authoring.md 원칙 2)
업스트림은 `ubi9` 빌더 + `ubi9-micro` 최종이다. 카탈로그의 기존 자체 빌드 3종
(`cloudnative-pg`·`cnpg-postgresql`·`etcd`)이 전부 SUSE BCI 이고 trivy 의 SLES 15.7
커버리지가 양성 대조로 실측 확인돼 있어(`doc/analysis/sles-oval-measurement.md`),
**"업스트림과 최대한 동일하게" 보다 카탈로그 내 일관성을 우선**했다.
**BCI 16.0 이 나와 있지만 15.7 을 쓴다 — 최신이 이 이미지에는 더 낡았다**(2026-08-07
실측). 필요한 나머지 패키지는 16.0 에도 전부 있지만 JDK 가 뒤처져 있다.
| 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건이 그대로 남았다. **16.0 의 JDK 가 15.7 을 따라잡으면
그때 올린다** — 재측정 방법은 `suse.build.env` 주석에 있다.
### rootfs 를 왜 "씨앗" 방식으로 만드나
업스트림 `ubi-null.sh` 는 빈 installroot 에 패키지를 깔고 그 rootfs 를 `ubi9-micro`
**위에 덮는다**. 이 구조를 SUSE 에 그대로 옮기면 `bci-micro` 의 rpmdb 가 새 rpmdb 로
가려져 **micro 자체 패키지가 SBOM 에서 통째로 사라진다** — CVE 가 줄어드는 게 아니라
스캔 사각지대가 생기는 것이고, 게이트가 경고하는 "베이스 OS 교체로 수치만 낮아진 것"의
전형이다.
그래서 `bci-micro` 파일시스템을 씨앗으로 깐 뒤 그 위에 `zypper --installroot`
설치한다. rpmdb 가 micro 것 위에 이어 써지므로 최종 이미지의 모든 OS 패키지가 SBOM 에
잡힌다. 빌드 후 이 점을 반드시 확인한다(아래 "빌드" 절).
## 업스트림과 다른 부분
업스트림 [`quarkus/container/Dockerfile`](https://github.com/keycloak/keycloak/blob/main/quarkus/container/Dockerfile)
대비 차이는 셋뿐이다.
1. **런타임 rootfs 가 SUSE BCI** — 위 참조. 패키지 목록(`RUNTIME_PACKAGES`)은 업스트림
이미지 SBOM 의 rpm 44종을 근거로 정했다. `sed`·`grep`**없으면 안 된다**
`bin/kc.sh``/bin/sh` 스크립트로 `esceval()` 에서 `sed`, 인자 파싱에서 `grep`
쓰는데 `bci-micro` 에는 `sed` 가 없다.
2. **취약 jar 오버레이** (`overlay-jars.sh`) — `lib/lib/main/` 의 netty·jackson·
postgresql-jdbc jar 를 **파일명은 유지하고 내용만** 수정 버전으로 바꾼다. Quarkus
fast-jar 의 클래스패스가 파일명을 그대로 참조하기 때문이다. trivy 는 jar 내부
`META-INF/maven/**/pom.properties` 를 읽으므로 SBOM 에는 새 버전이 정확히 잡힌다
— 파일명으로 버전을 위장하는 것이 아니라 실제 내용이 새 버전이다.
3. **`bin/client/` 제거** — `keycloak-admin-cli-<ver>.jar` 는 jackson 을 shade 로 품은
uber-jar 라 jar 교체로 고칠 수 없다(SBOM 실측: `jackson-databind@2.21.2`
FilePath 가 이 파일). 서버 JVM 이 로드하지 않는 독립 CLI(`kcadm.sh`/`kcreg.sh`)이므로
하드닝 이미지에서는 제거했다. **업스트림 대비 유일한 기능적 차이다** — 운영에서
`kcadm` 이 필요하면 업스트림 이미지를 별도 컨테이너로 쓴다.
`kc.sh build` 를 최종 스테이지에서 한 번 돌리는 것은 차이가 아니다 — 옵션 없는 build 는
릴리스 tar 의 사전 augmentation 상태를 그대로 재현하며, 오버레이한 jar 로 augmentation 이
실제로 통과하는지 빌드 시점에 확인하기 위한 것이다(최적화 이미지로 만드는 것이 아니다).
## 버전 관리
`APP_VERSION`/`KEYCLOAK_VERSION``*_OLD`/`*_VERSION` jar 버전은 **자동 추적하지
않는다.** 사람이 업스트림 릴리스를 보고 `suse.build.env` 를 고쳐 PR 을 여는 것 자체가
갱신 트리거다.
`overlay-jars.sh``*_OLD` 버전 jar 를 하나도 못 찾으면 **빌드를 실패시킨다.**
업스트림이 의존성을 올렸는데 스크립트가 조용히 아무것도 안 해서 "CVE 는 그대로인데
빌드는 성공" 하는 상태를 막기 위함이다.
**권장 점검 주기**: 게이트가 이 이미지의 차단 CVE 를 다시 보고할 때, 또는 Keycloak 이
Quarkus BOM 을 올린 릴리스를 낼 때 — **BOM 이 올라가 오버레이가 불필요해지면 해당
spec 을 제거하는 것이 이 자체 빌드를 유지하는 것보다 항상 우선이다.**
## 빌드
```sh
# 로컬 빌드 (push 없음)
IMAGE=keycloak BASE_OS=suse bash scripts/build/build-hardened-image.sh /tmp/kc-out
# 레지스트리 push 까지
IMAGE=keycloak BASE_OS=suse REGISTRY=docker.io/paasup \
bash scripts/build/build-hardened-image.sh /tmp/kc-out
```
> **arm64 호스트(Apple Silicon)에서 돌릴 때**: `linux/amd64` 를 QEMU 로 에뮬레이션하므로
> `verify.sh` 의 실기동이 매우 느리다 — Quarkus augmentation 만 ~100초, 기동 전체가
> 300초를 넘는다(2026-08-07 실측). `verify.sh` 의 기본 대기 시간은 600초이고
> `VERIFY_BOOT_TIMEOUT` 으로 조정한다. 네이티브 amd64 러너에서는 1~2분이면 끝난다.
수행 순서: **빌드 → 기능 검증(`verify.sh`) → SBOM → 전 심각도 스캔 → 게이트 판정.**
빌드 후 확인할 것:
1. `verify.log` 마지막 줄이 `VERIFY-OK`
2. `cve-gate.md` 의 차단 항목 — `doc/cve-exceptions.json` 에 등록된 것 외에 없는지
3. **커버리지 자가진단이 `ok`** 인지(`none` 이면 findings 0 이 진짜 0 이 아니다 —
`doc/sbom-pipeline.md`)
4. **rpmdb 마스킹이 없는지** — SBOM 의 OS 패키지 수가 `bci-micro` 단독 + 설치분에
해당하는지. 업스트림 UBI9 이미지의 OS 패키지는 44종이었다. 한 자릿수로 떨어졌다면
씨앗 방식이 동작하지 않은 것이므로 설계를 재검토한다.
`verify.sh` 가 확인하는 것: kc.sh 가 쓰는 셸 도구·java 21·`en_US.UTF-8` 로케일·
`Asia/Seoul` 타임존, `bin/client` 제거, **오버레이한 jar 의 내부
`pom.properties` 버전**(파일명이 아니라 내용 — trivy 와 같은 근거), 그리고 실제
`start-dev` 기동 후 OIDC discovery 응답·admin 토큰 발급·`GET /admin/realms` 까지.
외부 postgres·ingress·클러스터링은 이 스모크 테스트 범위 밖이며 dev 클러스터 배포
테스트(`.claude/deploy-test-procedure.md`)가 담당한다.
### 파일 구성
| 파일 | 역할 |
| --- | --- |
| `suse.Dockerfile` | 빌드 정의 — bci-base 빌더(rootfs 구성 + 배포본 전개 + jar 오버레이) + `FROM scratch` 최종 |
| `suse.build.env` | keycloak 버전·베이스 이미지·런타임 패키지·jar 오버레이 버전. `BUILD_ARGS` 에 나열한 이름만 `--build-arg` 로 전달된다 |
| `overlay-jars.sh` | 취약 jar 교체 + `bin/client` 제거. builder 스테이지 전용(호스트에서 직접 쓰지 않는다) |
| `verify.sh` | 기능 검증. 호스트에서 bash 로 실행되며 게스트 셸 주입 + 호스트 python3 jar 검사 + 실기동을 조합한다 |
| `catalog.env` | 이 이미지가 갱신하는 차트 디렉토리와 태그 표기 스타일 (`build-image.yml` 이 읽는다) |
베이스 변종이 하나뿐이라 파일명이 `suse.*` 로 고정돼 있다.
### 태그
```
docker.io/paasup/keycloak:26.7.1-bci15.7-hardened-20260807
└ app ┘└ 슬러그 ┘└ 하드닝 ┘└ 빌드일 ┘
```