Commit Graph

11 Commits

Author SHA1 Message Date
wbsong111 79555215a0 파이프라인을 두 축으로 갈라 소유 문서를 확정한다
"파이프라인이 chart CVE 조치와 커스텀 이미지 빌드 2개로 나뉘어 있는가" 를 확인하다가 실제
구성이 그 모델과 다른 것이 드러났다.

  1. 워크플로는 3개다. cve-edge-post.yml 이 CLAUDE.md 에서 디렉토리 트리와 괄호 안에만
     등장해 파이프라인으로 읽히지 않았다 — "2개" 인식의 근원이다.
  2. doc/sbom-pipeline.md 가 두 축을 한 파일에 담고 있었다(자체 빌드 절 43줄).
     커스텀 이미지가 별도 레포로 분리될 예정인데 이 상태로는 분리 때 파일을 찢어야 한다.
  3. 그 문서가 이미 뒤집힌 결정을 담고 있었다 — "schedule 트리거는 없다. 블라인드 정기
     재빌드는 제거했다" 인데 PR #40 이 schedule 을 추가했다.

축을 이렇게 갈랐다
-----------------
  차트 카탈로그 축   sbom.yml · cve-edge-post.yml   →  doc/sbom-pipeline.md
  자체 빌드 축       build-image.yml                →  .claude/image-authoring.md

doc/sbom-pipeline.md — 차트 축만 남긴다
  - 상단에 "이 문서가 다루는 축" 을 두고 자체 빌드는 링크로 넘긴다
  - 자체 빌드 절(43줄)을 image-authoring.md 로 이관
  - sbom.yml ↔ cve-edge-post.yml 비교 표 신설. **판정기가 두 벌**이라는 사실을 명시했다 —
    cve-edge-post.yml 은 집계를 워크플로 YAML 안의 인라인 python 으로 갖고 있어 승인 예외도
    실효 등급도 적용하지 않는다. 같은 스캔 데이터에서 다른 숫자가 나올 수 있다
  - PR 을 실제로 막는 게이트는 images/** PR 뿐이고 manifests/applicationset/** 는 아무
    워크플로도 보지 않는다는 사각지대를 적었다

.claude/image-authoring.md — 자체 빌드 축의 단일 출처가 된다
  - 이관받은 워크플로 서술 + "이 워크플로의 게이트는 강제다"(차트 축 warn-only 와 다르다는
    사실이 지금까지 한 곳에만 있었다)
  - schedule 결정 정정 — 지금 것은 블라인드가 아니라 CVE 트리거다. 수정 버전이 있는 차단
    CVE 가 있을 때만 빌드하고 없으면 아무것도 하지 않는다. 거부된 것과 조건이 다르다
  - "레포 분리 후 무엇이 끊기는가" 결합점 7개 표. 3번(탐지가 카탈로그를 읽는다)이 가장 크고,
    게이트 공유는 workflow_call 이 아니라 composite action 이어야 한다는 것도 적었다
    (workflow_call 은 별도 job 이라 $OUT_DIR 를 공유하지 못한다)
  - 파일 상단에 "레포 분리 시 images/·scripts/build/·build-image.yml 과 함께 이동한다"
  - #35 에서 실측한 매핑 함정 추가 — CHART_DIRS 에 없는 파일은 patch-catalog-tag.py 가
    검사조차 하지 않아 cnpg-cluster/1.1.0 이 조용히 빠졌다

CLAUDE.md — 지도만 남긴다
  두 축 비교 표(질문·워크플로·게이트 강도·소유 문서)로 바꾸고 메커니즘 서술을 걷어냈다.
  cve-edge-post.yml 을 파이프라인으로 처음 등재했다.

검증
----
  표의 사실 대조   각 워크플로의 cve-gate 호출·warn-only·활성 schedule 을 파일에서 확인
  축 분리          sbom-pipeline.md 에 남은 build-image.yml 언급은 전부 링크·대조·시크릿
                   공유 문장(의도된 것)
  뒤집힌 결정      "schedule 트리거는 없다" 잔존 0건
  링크             3개 문서의 로컬 링크 전부 실재

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:26:14 +09:00
wbsong111 981d36daa4 차단 CVE 가 나오면 자체 빌드 이미지를 스스로 재빌드한다 + Dockerfile 작성 규칙 (#40)
* 차단 CVE 가 나오면 자체 빌드 이미지를 스스로 재빌드한다 + Dockerfile 작성 규칙

두 가지다. (1) 이전 이미지에서 CVE 가 발견되면 재빌드가 자동으로 일어나게 한다.
(2) Dockerfile 을 OS·언어 축으로 일관되게 쓰도록 규칙을 정한다.

(1) 재빌드 자동화 — 트리거를 로컬 실측으로 고쳤다
------------------------------------------------
처음에는 트리거를 "핀이 뒤처졌는가" 로 잡았다. **배포 중인 이미지 8개를 실제로 스캔해 보니
그게 틀렸다** — 차단 CVE 가 있는 4개 중 핀 변경이 필요한 것은 하나도 없었고, 그 판정은
"0건" 을 냈다. 재빌드로 고쳐지는 경우가 셋이기 때문이다.

  핀이 뒤처졌다                          → 핀 올려서 빌드
  핀은 맞는데 그 핀으로 아직 안 빌드됐다  → 그냥 빌드   ← 핀만 고친 PR 직후 상태
  베이스 OS 패키지가 뒤처졌다             → 그냥 빌드   ← 재빌드하면 zypper 가 최신을 깐다

그래서 트리거는 **"수정 버전이 있는 차단 CVE 가 있는가"** 다. 핀 변경은 필요하면 함께 하는
부수 작업이고 트리거가 아니다. 수정 버전 없는 차단(affected·will_not_fix)은 재빌드해도
그대로이므로 no-fix 로 따로 보고해 사람이 다른 레버를 판단하게 한다.

scripts/build/check-rebuild-needed.py 신규
  카탈로그가 실제로 가리키는 이미지를 기준으로 잰다 — 재빌드해 보지 않고도 "지금 배포 중인
  것이 규정을 벗어났는가" 를 답한다.
    catalog.env → 카탈로그 values 의 ref → 그 ref 의 스캔 리포트 ↕ build.env 의 핀
  판정 규칙은 게이트와 같은 출처를 쓴다(max(벤더,NVD)·승인 예외·만료 예외 무효) —
  cve-gate.py 의 함수를 그대로 가져다 쓴다. 핀 산출은 suggest-go-upgrades.py 재사용.
  --list-refs 로 "무엇을 스캔해야 하나" 도 스크립트가 낸다 — ref 해석 규칙이 워크플로로
  새면 두 곳이 어긋난다.

suggest-go-upgrades.py 는 산출 로직이 main() 안에 있어 재사용할 수 없었다. collect_modules/
collect_stdlib/builder_candidates/effective_severity 로 빼고 main() 은 호출해 출력만 한다 —
CLI 출력은 리팩터 전후 동일하다(실측 대조).

build-image.yml 에 schedule(월요일 02:00 UTC)과 mode=drift 를 추가했다. 판정 → (핀이
뒤처졌으면) 브랜치 push → 빌드·verify.sh·게이트까지 자동이고 **레지스트리 push 와 카탈로그
반영은 하지 않는다.** 게시 여부 로직에서 catch-all 을 true → false 로 바꿨다 — 원래는
트리거를 추가하면 자동으로 push 가 켜지는 구조였고 schedule 을 넣는 순간 사고가 났다.
이제 push 는 workflow_dispatch + mode=image 에서만 켜진다.

(2) Dockerfile 작성 규칙 — 실제 8개에서 추출했다
----------------------------------------------
image-authoring.md 가 규칙 본문의 단일 출처이고 skill 은 "어느 절을 읽어야 하는가" 선택표만
갖는다(CLAUDE.md 의 "Skill 은 절차 본문을 복제하지 않는다").

두 축은 독립이다 — 같은 bci-micro 최종 위에 Go 빌더도 Node 빌더도 온다.

  축 1 최종 베이스   bci-base / bci-micro / scratch+micro rootfs — 셋뿐이다.
                     네 번째를 만들기 전에 왜 셋으로 안 되는지 먼저 적는다.
                     micro·scratch 는 nonroot 계정을 직접 만들어야 한다(실측 형태 첨부).
  축 2 빌더          Go(BUILDPLATFORM/TARGETARCH, ldflags 에 SOURCE_COMMIT) /
                     Node(런타임은 OS 패키지) / JVM(재컴파일 없이 jar 만 OLD/NEW 쌍으로
                     교체 — OLD 를 받는 이유는 못 찾으면 실패시키기 위함) /
                     C·Lua(정적 링크 금지 — SLE_BCI 에 static glibc 없음)

"업스트림 런타임 계약은 보존한다" 절을 새로 넣었다(USER·ENTRYPOINT·파일 레이아웃).
cloudnative-pg 가 멀티아치 심볼릭 링크를 부수 장치로 오판해 지웠다가 리컨실이 실패한 것이
근거다. 스캐너는 이걸 전혀 보지 못한다.

로컬 검증 — 판정만이 아니라 빌드까지 돌렸다
-----------------------------------------
판정이 지목한 4건을 실제로 빌드해 전제를 확인했다. 문서에 단정만 해두고 넘어가지 않았다.

  이미지                     배포 중   재빌드 후                      핀 변경
  apisix-ingress-controller  차단 8    0/0 PASS · VERIFY-OK · cov=ok  없음
  cloudnative-pg             차단 8    0/0 PASS · VERIFY-OK · cov=ok  없음
  etcd                       차단 8    0/0 PASS · VERIFY-OK · cov=ok  없음
  cnpg-postgresql            차단 2    0/0 PASS · VERIFY-OK · cov=ok  없음

  - cnpg-postgresql 이 "베이스 OS 패키지 갱신으로 해소" 사례다(perl·rpm-ndb). 핀을 하나도
    안 바꾸고 재빌드만으로 0건이 됐다 — 검증 없이 단정했던 부분이라 이게 핵심 확인이다.
  - 빌드된 etcd 바이너리가 go1.26.6 + pinned commit 을 보고한다 — 핀이 실제로 반영됐다.
  - 4건 다 CoverageProbe=ok 이므로 0건이 "측정되지 않음" 이 아니다.
  - 빌드 후에도 git status images/ 가 깨끗하다 = 트리거를 핀 기준으로 뒀다면 4건 전부
    놓쳤을 것이라는 확인.

그 외: 예외 적용(keycloak CVE-2025-59250 → 0건), --fail-on-drift rc=1, 워크플로 셸 재현
(GITHUB_OUTPUT 4개 JSON·fromJson OK·핀 변경 0 → 브랜치 미생성 → 기본 ref 로 빌드),
push 정책 전 조합 확인.

CI 에서 미검증인 것은 환경 특성뿐이다 — trivy 설치 스텝, GITHUB_STEP_SUMMARY 출력,
matrix 팬아웃, contents:write 로 브랜치 push. 빌드 자체는 로컬과 같은 스크립트를 쓴다.

Refs #35

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* self-build-image SKILL 의 자동 재빌드 절을 실행 계약만 남긴다

같은 커밋에서 image-authoring.md 와 SKILL 양쪽에 자동 재빌드 절차를 거의 동일하게 썼다.
SKILL 이 "작성 규칙 본문은 image-authoring.md 가 단일 출처다 — 여기 복제하지 않는다" 고
두 번 선언한 파일에서 내가 그 규칙을 어겼다.

SKILL 에는 실행 계약(수동 실행 명령 · 로컬 판정 명령 · push 안 한다는 사실)만 남기고
트리거 조건과 그 근거는 image-authoring.md 를 가리킨다.

frontmatter 도 함께 고친다 — "7종" 에 argocd 가 빠져 있어 실제 images/ 8개와 어긋났다.
description 은 Skill 로딩 판단에 쓰이는 텍스트이므로 "목록은 images/ 가 단일 출처" 라는
회피 조항으로 덮을 수 없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 레포 분리 시 갈라지는 이음선을 코드와 문서에 명시한다

커스텀 이미지가 별도 레포로 분리될 예정인데, check-rebuild-needed.py 는 카탈로그(무엇을
배포 중인가)와 build.env(어떻게 만드는가)를 **양쪽 다** 읽는다. 분리 시 이 파일이 갈라진다.

절단면은 이미 함수 경계에 있다. 6개월 뒤에 "이 함수가 왜 카탈로그를 읽지" 를 다시 파지
않도록 어느 함수가 어느 쪽인지 docstring 에 적었다.

  A (카탈로그 레포)  resolve_current_ref + blocking_cves  → "차단됐는가"
  B (이미지 레포)    pin_changes + apply_changes          → "핀을 올려야 하는가"
  check_image        둘을 엮는 오케스트레이션 — 여기가 갈라진다

image-authoring.md 의 드리프트 절에도 같은 사실을 한 문단으로 적어 분리 작업 때 이 주석을
먼저 읽게 했다. 결합점 전체 목록은 경계 문서화 작업에서 표로 정리한다.

코드 동작은 바뀌지 않았다 — 주석과 문서만 추가했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:19:26 +09:00
wbsong111 1a747f61a8 자체 빌드 이미지 문서가 이 레포에 없는 경로를 인용하던 것을 없앤다 (#33) (#34)
images/·manifests/helm/·.claude/ 의 20개 파일이 doc/decisions·doc/analysis 등 **이 레포에
존재한 적 없는 경로 15종을 48곳에서** 인용하고 있었다. security-catalog 에서 포팅할 때
따라온 것인데, 그 레포는 개인 레포(github.com/wbsong111/security-catalog)라 팀 구성원은
접근조차 못 한다 — "security-catalog 에 있으나 이관되지 않았다" 는 안내가 아무 역할을
하지 못했다.

원문을 통째로 복사하지 않았다
----------------------------
원본 문서들이 서로를 근거로 인용한다. decisions/0001 하나만 봐도 analysis/cnpg-image-baseline.md
· analysis/vendor-unassessed-data-sources.md 처럼 **인용 목록에 없던 또 다른 미이관 문서**를
가리킨다. 복사는 문제를 옮기는 것이지 없애는 게 아니다.

그리고 대부분은 애초에 dip-catalog 가 더 나은 것을 갖고 있다. 7곳에서 인용되던
analysis/sles-oval-measurement.md 는 원문 스스로 "이 문서는 결정하지 않는다. 재측정하면
갱신된다" 고 밝히는 스냅샷인데, dip-catalog 는 같은 측정을 CoverageProbe 로 매 스캔마다
자동으로 한다. 문서를 복사하는 것보다 게이트를 가리키는 것이 정확하다.

그래서 성격별로 나눴다
---------------------
  재측정으로 복원 안 되는 것  →  doc/decisions/ 에 자립적 ADR 로 다시 씀 (4건)
  이미 단일 출처가 있는 것    →  그쪽으로 인용 교체 (11종 경로)

ADR 4건은 security-catalog 0001·0005·0006·0007 이 원본이고, 결론과 근거만 추려
dip-catalog 맥락으로 새로 썼다 — **레포 밖을 가리키는 링크가 0이다.** 번호는 이 레포에서
0001~0004 로 다시 붙였고 원본 대응은 각 문서와 README 에 적었다. 왜 안 가져온 것은 안
가져왔는지도 README 표에 남겼다.

인용 교체는 카테고리별로:
  analysis/*-cve.md, cnpg-image-vuln-comparison.md  →  해당 ADR · images/<image>/README.md
  analysis/sles-oval-measurement.md                 →  게이트 CoverageProbe (doc/sbom-pipeline.md)
  cve-zero-pipeline.md, architecture/build-pipeline.md → doc/sbom-pipeline.md
  image-selection.md                                →  .claude/image-authoring.md
  charts/*/deploy-test.md                           →  scripts/deploy-test/*.sh + 절차 문서

찾은 오류 2건
-------------
- images/cloudnative-pg/source.build.env 가 인용한 decisions/0004-cloudnative-pg-operator-self-build.md
  는 **번호 오기**다. 원본 0004 는 postgresql-chart-selection 이고 이 결정은 0005 다.
- cnpg-cluster values.yaml·templates/database.yaml 이 인용한 doc/deploy-test-cnpg.md 는
  **원본 레포에도 없다.** CREATE EXTENSION 함정 설명은 주석 자체에 이미 있어 인용만 뺐다.

검증
----
  우리 파일의 깨진 doc/ 인용        0건 (전수 스캔)
  새 문서·수정 문서의 로컬 링크     전부 실재 확인
  helm template                     cnpg-cluster · etcd · cloudnative-pg 정상 렌더

남은 doc/health-checking.md(144곳)·doc/integration/*(2곳)은 업스트림 CRD·차트 안의 문자열로
우리가 쓴 인용이 아니다 — 건드리지 않았다.

Closes #33

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:20:40 +09:00
wbsong111 63d25221df MEMORY.md 를 상태 파일로 되돌린다 (264줄 → 64줄)
MEMORY.md 는 3번째 줄에서 "지금 시점의 상태와 다음에 할 일만 담는다" 고 스스로 선언하는데
실제로는 완료 기록이 쌓여 264줄이 됐고, 이슈 참조가 한 건도 없었다. 그 결과:

- SBOM_PIPELINE_IMAGE 재빌드가 파일 안에서 3번 반복된다 (215·234·262줄)
- 게이트 강제력 전환이 2번 반복되고, 그 내용은 이미 이슈 #7 체크리스트와 #19 에 있다
- argo-cd 절 55줄은 커밋 33e91c0 메시지의 요약본이다
- 베이스 OS 정책·"벤더 릴리스 노트 믿지 말 것" 은 본문이 스스로 "이미 image-authoring.md /
  skill 에 있다" 고 밝히면서 중복 서술한다

같은 사실이 여러 곳에 있으면 나중에 어느 쪽이 맞는지가 새 문제가 된다.

성격별로 목적지를 정하고 내보냈다
---------------------------------
  추적이 필요한 미결   →  GitHub 이슈 (#29~#33 발행, MEMORY.md 엔 한 줄 링크)
  재발 방지 교훈       →  .claude/ 문서·skill
  단순 완료 기록       →  삭제 (커밋·PR·images/<image>/README.md 가 권위 있는 기록)

교훈 이관 — 아직 어디에도 없던 것만 옮겼다:

- image-authoring.md: 베이스 OS 를 CVE 수치만으로 고르면 배포에서 죽는다.
  cp --update=none(coreutils 9.3+) 실측과, 차트가 실행하는 명령을 verify.sh 에서
  재현하라는 규칙. 원칙 2 의 "BCI 버전은 실측해서 정한다" 바로 뒤에 붙였다.
- self-build-image skill: 같은 날 재빌드하면 태그가 겹쳐 노드 캐시가 반영을 막는다.
  근본 해결은 #31, 여기엔 우회법만.
- cve-remediation skill: 레버 표를 보기 전에 "이 컴포넌트가 필요하긴 한가" 를 먼저 묻는다.
  dex 가 한 줄로 차단 57건(58%)이었다. 가장 값싼 레버인데 표에 없어서 새 단계로 넣었다.

유지 규칙을 명시했다
--------------------
트리거는 시간이 아니라 상태다 — "1달 지났으니 정리" 는 아무도 시작하지 않고, 날짜로 자르면
오래됐지만 유효한 미결까지 잘린다. 항목이 "다음에 할 일" 이 아니게 되는 순간 내보낸다.
다만 놓친 것을 걷어내기 위해 월 1회 점검하고 파일 상단의 점검 날짜를 갱신한다.

규칙은 MEMORY.md 자체와 CLAUDE.md("작업 기록을 어디에 남기는가") 양쪽에 뒀다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:56:05 +09:00
wbsong111 33e91c0536 argocd: 자체 빌드 하드닝 이미지 추가, 게이트 차단 0건
업스트림 quay.io/argoproj/argocd 의 차단 CVE 는 대부분 OS 패키지가 아니라 바이너리에
정적 링크된 Go 모듈이라, 상위 태그 교체(이미 최신)로도 베이스 OS 교체로도 잡히지 않는다.

부분 조치는 효과가 없었다 — 같은 stdlib·x/crypto 취약점이 다섯 바이너리(argocd·helm·
kustomize·git-lfs·pebble)에 각각 들어있어 본체만 고치면 거의 줄지 않는다. 업스트림
Dockerfile 이 helm/kustomize/git-lfs 를 hack/install.sh 로 **미리 빌드된 릴리스 바이너리**
로 내려받기 때문에 그 바이너리의 Go 버전을 우리가 통제할 수도 없다. 그래서 넷 다 소스에서
다시 컴파일한다.

- images/argocd/ 신규 (Go 4개 + C 2개 + Node UI — 기존 자체 빌드 중 가장 크다)
  - 최종 베이스 ubuntu → SUSE BCI(원칙 2). ubuntu 가 딸려오던 pebble 도 함께 사라진다
  - tini·connect-proxy 는 SLE_BCI 에 없어 소스 빌드 — 기능을 빼지 않기 위해
  - UI 는 업스트림과 동일하게 node 로 빌드(Go embed 라 생략 불가)

- manifests/helm/argo-cd/10.4.0/custom-values.yaml 이 이 이미지를 가리킨다
  global.image 하나로 argocd 바이너리를 쓰는 5개 컴포넌트가 공유한다

재사용 설계 — Dockerfile 에 버전을 박지 않는다
---------------------------------------------
이 이미지는 앞으로도 CVE 조치를 반복해서 받는다. 모듈 목록·패키지 목록을 전부 값으로 빼서
다음 조치에서 바뀌는 것이 source.build.env 한 곳뿐이게 했다. 이전 방식이라면 모듈 하나
추가에 build.env·ARG 선언·go get 목록·BUILD_ARGS 네 곳을 고쳐야 했다.

- scripts/build/suggest-go-upgrades.py 신규 — 스캔 리포트의 FixedVersion 에서
  GO_MODULE_UPGRADES / GO_BUILDER_TAG 를 산출한다. 사람이 CVE 를 훑어 최대값을 고르지
  않는다. 빌드 시점에 최신을 당기는 방식(go get -u)은 재현성을 버리므로 택하지 않았다 —
  값은 제안만 하고 채택은 사람이 커밋한다.
- images/argocd/go-mod-upgrade.sh 신규 — 목록 하나를 네 프로젝트에 재사용한다.
  go get 은 의존성에 없는 모듈도 go.mod 에 추가하므로 그래프에 있는 것만 골라 적용한다.

실측 함정 — verify.sh 에 반영했다
--------------------------------
차트의 repo-server init 컨테이너가 `cp --update=none` 을 쓴다. 이 형식은 GNU coreutils
9.3+ 에서만 되는데, BCI 15.7 로 빌드한 이미지가 게이트도 verify.sh 도 통과하고 **배포
시점에** Init:CrashLoopBackOff 로 죽었다. BCI 16.0(coreutils 9.6)으로 올려 해소했고,
verify.sh 가 이 명령과 copyutil 흐름을 직접 재현하도록 해서 다음엔 빌드 단계에서 걸린다.
16.0 의 패키지 가용성과 trivy 커버리지(CoverageProbe=ok, EOSL 아님)도 확인했다.

검증
----
  게이트     PASS — 실효 CRITICAL/HIGH 0건, CoverageProbe ok
  기능       VERIFY-OK (심볼릭 링크 9개 · 번들 도구 3종 · LFS 필터 · copyutil 흐름)
  배포       docker.io/paasup 에 push 후 운영 argocd 교체 — 파드 8종 Running,
             admin 로그인, Application 2건 Synced/Healthy, repo-server 렌더링 확인
             (서버가 go1.26.6 · helm v4.2.4 로 보고 — 우리 빌드가 맞다)

카탈로그 차단은 574 → 239 건이 됐고(7.7.0 삭제 · dex 비활성 · 이 커밋), 남은 239 건은
전부 동결된 7.8.11 몫이다. 운영에 쓰는 10.4.0 은 0 건이다. 경과·미결은 MEMORY.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 15:04:05 +09:00
wbsong111 4f67f63f69 SBOM 파이프라인 문서 정비 + 이미지 목록 열거 제거
## doc/sbom-pipeline.md — 중복·모순 정리 (328 → 286줄)

같은 사실이 여러 절에 흩어져 있었고, 일부는 문서가 아니라 변경 이력이었다.

- `SEVERITY` 를 전 심각도로 덮어써야 하는 이유가 환경변수 절·CI 절·요약 절 3곳에
  있었다. 스크립트 절의 blockquote 하나로 합쳤다 — "게이트를 돌릴 거라면 전 심각도로
  스캔해야 한다"가 핵심이고 나머지는 그 결과다.
- Job Summary 1MB 제한이 CI 절과 결과 확인 절에 중복됐다. CI 절 하나로 합쳤다.
- `--warn-only` 서술이 mermaid 라벨·CI 절·게이트 절·트리아지 절 4곳에 있었다.
  게이트 절 하나로 합치고, `build-image.yml` 쪽은 이미 강제라는 대비를 함께 적었다.
- 자체 빌드 트리거 표가 "PR 은 push 안 함"을 말하는데 바로 아래 불릿이 같은 말을
  반복했다. 표는 그대로 두고 불릿은 **왜** 그런지(REGISTRY 미전달 → localhost 태그라
  push 를 시도할 수조차 없다)만 남겼다.
- "오해를 주던 단일 '총 소요'는 제거" 같은 변경 이력 서술을 걷어냈다. 문서는 현재
  상태를 적는 곳이다.
- "첫 전체 실행 결과(2026-07-08)" 절은 수치를 싣고 바로 아래에서 "현재 수치가
  아니다"로 무효화하는 구조였다. 절 자체를 없애고, 거기서 유일하게 쓸모 있던 사실
  (SBOM 생성 실패는 대부분 사설/미인증 레지스트리이거나 대용량 timeout)만 스크립트
  절로 옮겼다.
- "실행 이력(2026-08-04)" 절은 MEMORY.md 와 중복이라 제거했다. 거기서만 알 수 있던
  사실(Actions 시크릿의 push 권한 확인)은 GitHub 설정 표에 반영했다.
- `CVE_API_KEY` 가 본문에만 있고 GitHub 설정 표에 빠져 있어 추가했다.
- 아키텍처 절 불릿이 mermaid 서브그래프 라벨과 같은 말을 하고 있어, "pull 을 ② 한
  곳에 몰아둔 것이 핵심"이라는 결론 한 문장으로 줄였다.

## 이미지 목록을 문서에 박아두지 않는다

이미지는 계속 추가되므로 열거하면 추가할 때마다 낡는다. `images/` 디렉토리를 단일
출처로 삼고 CLAUDE.md·image-authoring.md·build-image.yml·sbom-pipeline.md 의 열거를
걷어냈다. keycloak README 의 베이스 OS 결정 근거도 "기존 3종" 대신 "먼저 들어온
이미지들"로 바꿨다 — 근거의 내용은 그대로다.

## 현황 서술 정정

- build-image.yml 주석이 "아직 도입된 자체 빌드 이미지가 없다(images/ 가 비어 있음)"
  로 남아 있었다. 이 레포 CI 에서 빌드→검증→게이트→push→카탈로그 브랜치 push 까지
  실제로 검증된 상태다.
- MEMORY.md: cve-exceptions.json 첫 예외 등록, 베이스 OS 정책 확정, PR 자동 생성이
  조직 정책으로 불가하다는 실측(run 30882785612)을 반영했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:27:03 +09:00
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
wbsong111 01ec67ae1f 파이프라인 실행 절차를 Claude Code Skill 3종으로 등록
세 파이프라인의 실행법이 문서에만 있어 "그 문서를 읽어야만" 알 수 있었다.
관련 작업 시 자동 로드되도록 Skill 로 등록한다.

- catalog-update-pipeline: agent/update_catalog 파이프라인. CATALOG_ROOT 가
  개인 로컬 경로로 하드코딩돼 있어 오버라이드 필수라는 점, create_pr 단계가
  주석 처리돼 PR 이 생성되지 않는다는 점을 명시했다.
- sbom-cve-gate: SBOM·스캔·게이트. CoverageProbe(ok/none/n-a) 해석과 게이트가
  현재 warn-only 라는 점을 명시했다.
- self-build-image: 자체 빌드. 오케스트레이터는 하나뿐이라는 원칙과 전역 ARG
  선언, 게이트 PASS 가 동작을 보장하지 않는다는 점을 명시했다.

Skill 은 절차 본문을 복제하지 않고 권위 문서를 가리킨다 — 문서가 단일 출처이고
Skill 은 실행 계약과 함정·현재 상태만 담는다.

함께 고친 stale 문서(image-authoring.md):
- "images/ 디렉토리 자체가 없다" → 실제로는 이미지 3종이 있고 push 까지 됐다
- "자동화된 배포 테스트 절차는 아직 없다" → deploy-test-procedure.md 와
  전용 스크립트가 있다

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 15:37:57 +09:00
wbsong111 6597059a15 scan-sbom.sh: CoverageProbe 커버리지 자가진단 이식 + 관련 파이프라인 버그 수정
security-catalog 프로젝트에서 자체 빌드 이미지 3종을 실제로 로컬 빌드·게이트
검증하는 과정에서 발견한 문제들:

- extract-helm-images.sh 가 `image:` 필드만 잡고 `imageName:`(CNPG Cluster CRD 관례)
  은 놓쳐, cnpg-cluster 차트의 이미지가 SBOM·스캔·게이트 어디에도 안 나타났다.
- patch-catalog-tag.py 의 split 포맷 패처가 registry 필드가 따로 없고 repository 에
  registry+repo 를 합쳐 쓰는 차트(cloudnative-pg 오퍼레이터, 업스트림 템플릿이
  image.registry 를 아예 참조하지 않음)에서 tag 만 조용히 갱신하고 repository 는
  그대로 남겨 깨진 참조를 만들 수 있었다.
- CoverageProbe(센티널 패키지 주입 재스캔으로 "0건"과 "측정 안 됨"을 구분)가
  이식되지 않아, 전 심각도 0건인 자체 빌드 이미지가 실제로는 깨끗한데도 게이트가
  "데이터 커버리지 이상"으로 오탐 처리했다 — security-catalog 의 scan-sbom.sh 를
  이식해 해소. cve-gate.py 는 이미 이 키를 읽도록 구현돼 있어 소비 쪽 변경은 없다.

rpm(SUSE)·deb(Debian)·apk(Alpine) 세 센티널 경로와 병렬 스캔 회귀를 로컬에서
확인했고, 이식 후 cloudnative-pg·cnpg-postgresql 게이트가 실제로 FAIL→PASS 로
바뀌는 것도 확인했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 15:30:57 +09:00
wbsong111 a168a3c7e6 docs(image-authoring): CoverageProbe cov= 확인 지시 제거 + Dockerfile 경로 주석 정정
scan-sbom.sh 에 커버리지 자가진단이 없는데도 체크리스트가 cov= 확인을 지시해
같은 문서 104-106줄과 모순됐다. 실제 동작(findings 0건 시 보수적 실패)과 판단
방법으로 교체. Dockerfile 헤더 주석의 doc/scripts/ 경로도 scripts/pipeline/ 로
갱신(경로 이전 시 누락됨).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 11:25:20 +09:00
wbsong111 844c567d0d 자체 빌드 이미지 프레임워크 도입 (실사용 이미지 없음)
security-catalog 에서 포팅: build-hardened-image.sh, patch-catalog-tag.py,
build-image.yml(REGISTRY_HOST=docker.io/paasup). images/ 는 아직 비어있다 —
베이스 OS 정책 미결 등은 .claude/image-authoring.md, MEMORY.md 참고.
2026-08-03 09:55:52 +09:00