From 63d25221df683c45750c832ff9d377c66a196bba Mon Sep 17 00:00:00 2001 From: wbsong111 Date: Wed, 19 Aug 2026 16:56:05 +0900 Subject: [PATCH] =?UTF-8?q?MEMORY.md=20=EB=A5=BC=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=90=98=EB=8F=8C=EB=A6=B0?= =?UTF-8?q?=EB=8B=A4=20(264=EC=A4=84=20=E2=86=92=2064=EC=A4=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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) --- .claude/image-authoring.md | 11 + .claude/skills/cve-remediation/SKILL.md | 17 +- .claude/skills/self-build-image/SKILL.md | 4 + CLAUDE.md | 18 +- MEMORY.md | 284 ++++------------------- 5 files changed, 85 insertions(+), 249 deletions(-) diff --git a/.claude/image-authoring.md b/.claude/image-authoring.md index 75fb075..c77676c 100644 --- a/.claude/image-authoring.md +++ b/.claude/image-authoring.md @@ -83,6 +83,17 @@ docker run --rm registry.suse.com/bci/bci-base:<태그> \ 새 버전으로 올릴 때는 trivy 의 해당 SLE 버전 커버리지도 다시 확인한다 — `CoverageProbe` 가 `none` 이면 findings 0 이 진짜 0 이 아니다(게이트가 막는다). +**CVE 수치만 재고 고르면 배포에서 죽는다.** 베이스 OS 는 런타임이 요구하는 도구의 **버전**도 +같이 바꾼다. 실측(2026-08-19, `argocd`): argo-cd 차트의 repo-server init 컨테이너가 +`cp --update=none` 을 쓰는데 이 형식은 GNU coreutils 9.3+ 에서만 된다. BCI 15.7 로 빌드한 +이미지가 **게이트도 `verify.sh` 도 통과하고 배포 시점에** `Init:CrashLoopBackOff` 로 죽었다. +스캐너는 "설치된 패키지에 알려진 CVE 가 있는가" 만 보지 "이 이미지를 쓰는 차트가 무엇을 +요구하는가" 는 전혀 보지 못한다. + +→ **차트가 이 이미지에 대고 실제로 실행하는 명령을 `verify.sh` 에서 그대로 재현한다.** +베이스를 바꿀 때 빌드 단계에서 걸린다. `images/argocd/verify.sh` 의 "차트가 실제로 실행하는 +명령" 절이 예다. + ### `bci-micro` 위에 패키지를 얹을 때 — rootfs 는 반드시 "씨앗" 방식으로 `bci-micro` 는 패키지 매니저가 없지만 **rpmdb 는 갖고 있다** diff --git a/.claude/skills/cve-remediation/SKILL.md b/.claude/skills/cve-remediation/SKILL.md index 5227938..e27faf6 100644 --- a/.claude/skills/cve-remediation/SKILL.md +++ b/.claude/skills/cve-remediation/SKILL.md @@ -16,7 +16,12 @@ description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH) 2. 차단 CVE를 컴포넌트 단위로 나눈다. "N건"이 성격 다른 여러 그룹(OS 패키지 vs 앱이 번들/pin한 라이브러리)일 수 있고 그룹마다 레버가 다를 수 있다 — keycloak은 17건이 OS rpm 5건 + Quarkus BOM pin jar 12건으로 갈렸다. -3. 그룹별로 위부터 먼저 맞는 것을 쓴다: +3. **레버를 고르기 전에 "이 컴포넌트가 필요하긴 한가"를 먼저 묻는다.** 차트 기본값으로 켜져 + 있을 뿐 우리가 쓰지 않는 컴포넌트가 차단의 큰 몫을 만들고 있을 수 있다 — argo-cd 는 + `configs.cm.oidc.config` 로 Keycloak 에 직접 붙는데 차트 기본값 `dex.enabled: true` 때문에 + 쓰이지도 않는 파드가 떠서 혼자 차단 57건(전체의 58%)을 만들고 있었다. 한 줄로 끝났다. + 가장 값싼 레버이고, 아래 표에는 없다. +4. 그룹별로 위부터 먼저 맞는 것을 쓴다: | 조건 | 레버 | |---|---| @@ -24,17 +29,17 @@ description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH) | 베이스가 distroless/scratch, CVE가 정적 링크 바이너리(Go 모듈 등)에 있다 | 베이스 OS 교체 불가 → 자체 빌드 | | 앱이 번들·직접 pin한 라이브러리(jar 등)에 있다(OS 패키지 아님) | 태그·베이스 OS 둘 다 무효 → 자체 빌드(overlay/재컴파일) | | 다른 배포판이 backport로 이미 고쳤다 | 베이스 OS 교체 | - | 컴포넌트가 중복/오탐 의심 | 4번 검증 후 예외, 아니면 자체 빌드 | + | 컴포넌트가 중복/오탐 의심 | 5번 검증 후 예외, 아니면 자체 빌드 | -4. 예외는 검증 후에만 — "위험이 낮아 보인다"는 근거가 아니다. InstalledVersion이 +5. 예외는 검증 후에만 — "위험이 낮아 보인다"는 근거가 아니다. InstalledVersion이 FixedVersion 이상인지, 같은 파일이 SBOM에서 컴포넌트 두 개로 잡히지 않는지(PkgPath 비교) 확인한다. 실례: keycloak `CVE-2025-59250`(mssql-jdbc jar 하나가 두 컴포넌트로 잡혀 잘린 쪽만 매칭된 오탐). 근거·만료일은 `doc/cve-exceptions.json`. -5. 자체 빌드는 `self-build-image` + [image-authoring.md](../../image-authoring.md)로, +6. 자체 빌드는 `self-build-image` + [image-authoring.md](../../image-authoring.md)로, 태그/베이스 OS 교체는 카탈로그 values만 바꾸고 재게이트한다 — 별도 스크립트 없음. -6. 수정 후 반드시 재게이트하고 PASS라도 배포 검증까지 끝나야 종료다 — 1회로 끝난다고 +7. 수정 후 반드시 재게이트하고 PASS라도 배포 검증까지 끝나야 종료다 — 1회로 끝난다고 가정하지 않는다(keycloak은 1차 수정 후 재스캔에서 micrometer 2건이 새로 잡혔다). -7. 착지는 브랜치 push까지다 — 조직 정책상 `GITHUB_TOKEN`으로 PR을 못 연다. 사람이 PR을 +8. 착지는 브랜치 push까지다 — 조직 정책상 `GITHUB_TOKEN`으로 PR을 못 연다. 사람이 PR을 열고, 판단 근거는 PR 설명 + `MEMORY.md`에 남긴다. ## 참고 diff --git a/.claude/skills/self-build-image/SKILL.md b/.claude/skills/self-build-image/SKILL.md index 06bf552..6ffbf17 100644 --- a/.claude/skills/self-build-image/SKILL.md +++ b/.claude/skills/self-build-image/SKILL.md @@ -42,6 +42,10 @@ CI는 `build-image.yml`이 `image` 입력으로 이미 파라미터화돼 있다 스캐너에 그 배포판 데이터가 없다는 뜻이다(`sbom-cve-gate` skill 참고). - **롤링 태그를 쓰지 않는다.** 같은 앱 버전이라도 베이스 업데이트 결과가 시점마다 달라 태그에 빌드일을 포함한다(예: `1.30.0-security-hardened-20260804`). +- **단, 같은 날 다시 빌드하면 그 태그가 겹친다.** 노드가 캐시한 옛 digest 가 그대로 쓰여 + (`imagePullPolicy: IfNotPresent`) 고친 것이 반영되지 않은 채 "안 고쳐졌다" 로 보인다. + 배포 검증 중이라면 `imagePullPolicy: Always` 로 우회하고, digest 로 확인한다 — + `kubectl get pod -o jsonpath='{..imageID}'`. 프레임워크 차원의 미결 사항이다. ## 마무리 diff --git a/CLAUDE.md b/CLAUDE.md index 3f28000..aef5d68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ dip-catalog/ │ ├── build/ # 자체 빌드 이미지 프레임워크 (build-image.yml 이 쓴다) │ └── deploy-test/ # 배포 검증 스크립트 + fixtures (helm/kubectl 실행 전담) ├── images// # 자체 빌드 하드닝 이미지 정의 (목록은 이 디렉토리가 단일 출처) -├── MEMORY.md # 현재 상태·미결 결정 (작업 이어받을 때 여기서 시작) +├── MEMORY.md # 현재 상태·미결 (작업 이어받을 때 여기서 시작 — 유지 규칙은 파일 안에) └── doc/ # 차트 리소스 프로파일 + 스택 분류 + CVE/SBOM 파이프라인 + 운영 가이드 ├── sbom-pipeline.md # SBOM 생성·스캔·게이트 메커니즘 ├── cve-exceptions.json # 게이트 승인 예외 목록 @@ -189,6 +189,22 @@ extract-helm-images.sh → generate-sbom.sh → scan-sbom.sh → cve-gate.py --- +## 작업 기록을 어디에 남기는가 + +같은 사실을 두 곳에 적지 않는다. 성격에 따라 목적지가 정해져 있다. + +| 성격 | 목적지 | +|------|--------| +| **무엇을 왜 바꿨나** (완료된 작업의 경위) | 커밋 메시지 · PR 설명 · `images//README.md` | +| **다시 밟지 말아야 할 함정** | [.claude/pitfalls.md](.claude/pitfalls.md) · [.claude/image-authoring.md](.claude/image-authoring.md) · 해당 Skill | +| **추적·논의·배정이 필요한 미결** | GitHub 이슈 | +| **지금 상태와 다음에 할 일** | [MEMORY.md](MEMORY.md) — 위 셋에 속하면 여기 남기지 않고 링크만 | + +`MEMORY.md` 는 완료 기록이 쌓이는 곳이 아니다. 항목이 "다음에 할 일" 이 아니게 되면 위 셋 중 +하나로 내보내고 지운다 — 상세 규칙과 월 1회 점검 절차는 그 파일 안에 있다. + +--- + ## 설계 원칙 아래 원칙을 위반하는 코드를 제안하거나 작성하지 않는다. diff --git a/MEMORY.md b/MEMORY.md index 1aaa8c9..f4ccb4c 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,264 +1,64 @@ -# 현재 상태 · 미결 결정 +# 현재 상태 · 미결 -작업을 이어받을 때 여기서 시작한다. **지금 시점의 상태와 다음에 할 일**만 담는다. +작업을 이어받을 때 여기서 시작한다. - 프로젝트 개요·설계 원칙 → [CLAUDE.md](CLAUDE.md) - SBOM·CVE 게이트 메커니즘 → [doc/sbom-pipeline.md](doc/sbom-pipeline.md) -최종 갱신: 2026-08-19 +최종 갱신 · 점검: 2026-08-19 --- -## 다음 작업 +## 이 파일의 유지 규칙 -**argo-cd@10.4.0 이 게이트 차단 0건으로 완결됐다(2026-08-19).** ArgoCD 가 k8s 1.35 를 -지원하지 못해 시작한 업그레이드가 CVE 조치까지 이어진 건이다. +**지금 시점의 상태와 다음에 할 일만 담는다.** 완료된 작업의 경위는 담지 않는다 — 커밋 +메시지·PR 설명·`images//README.md` 가 이미 권위 있는 기록이고, 복사본을 두면 나중에 +어느 쪽이 맞는지가 새 문제가 된다. -**발단 — k8s 1.33 신규 필드로 ArgoCD 가 전멸했다.** `.status.terminatingReplicas`(k8s 1.33 -도입)를 ArgoCD v2.14.5(k8s 라이브러리 0.31/0.32)가 몰라서, `ServerSideApply=true` 인 -Application 이 전부 `ComparisonError: field not declared in schema` 로 죽었다. 차트 -7.8.11 → 10.4.0(ArgoCD v3.5.1, k8s 라이브러리 0.36.1)으로 해소했고 dev 클러스터 -(1.35.7+rke2r1)에서 발생·해소를 모두 실측했다. +항목이 "다음에 할 일" 이 아니게 되면 **셋 중 하나로 내보내고 여기서 지운다.** -**CVE 조치 — 레버를 순서대로 적용해 574 → 239건.** +| 성격 | 목적지 | +|---|---| +| 추적·논의·배정이 필요한 미결 | GitHub 이슈 — 여기엔 **한 줄 링크만** | +| 재발 방지 교훈 | `.claude/pitfalls.md` · `.claude/image-authoring.md` · 해당 skill | +| 단순 완료 기록 | 삭제 | -| 조치 | 차단 | -|---|---:| -| 시작(7.7.0·7.8.11·10.4.0 전부) | 574 | -| `argo-cd/7.7.0` 삭제(구버전, EOSL 이미지 2종 포함) | 337 | -| `dex.enabled=false` — 쓰지도 않는데 떠 있었다 | 280 | -| `images/argocd/` 자체 빌드 | **239** | - -남은 239건은 전부 **동결된 7.8.11**(직전 버전) 몫이고, 운영에 쓰는 **10.4.0 은 0건**이다. -7.8.11 을 언제 지울지가 미결이다 — 지우면 게이트가 PASS 로 떨어진다. - -**dex 를 끈 것이 가장 값싼 레버였다.** ArgoCD 는 `oidc.config` 가 있으면 dex 를 우회하는데 -차트 기본값이 `enabled: true` 라 유휴 파드가 떠서 차단의 절반 이상을 만들고 있었다. -dipup 도 항상 `configs.cm.oidc.config` 로 Keycloak 에 직접 붙으므로 dex 를 쓰지 않는다. -**대응 레버를 보기 전에 "이 컴포넌트가 필요하긴 한가" 를 먼저 묻는다** 는 교훈이다. - -**`images/argocd/` 는 기존 자체 빌드 중 가장 크다** — Go 프로젝트 4개(argocd·helm· -kustomize·git-lfs) + C 2개(tini·connect-proxy) + Node UI. 업스트림이 번들 도구를 **미리 -빌드된 릴리스 바이너리로 내려받기** 때문에 그 바이너리의 Go 버전을 우리가 통제할 수 없고, -같은 CVE 가 여러 바이너리에 공유돼 **부분 조치가 무효**였다(본체만 고치면 거의 줄지 않았다). - -실측 함정 셋: - -- **`cp --update=none` 이 베이스 OS 를 갈랐다.** 차트의 repo-server init 컨테이너가 쓰는 - 이 형식은 GNU coreutils 9.3+ 에서만 된다. BCI 15.7 로 빌드한 이미지가 게이트도 - `verify.sh` 도 통과하고 **배포 시점에** `Init:CrashLoopBackOff` 로 죽었다. BCI 16.0 - (coreutils 9.6)으로 올려 해소했고, `verify.sh` 가 이 명령을 직접 실행해 다음엔 빌드 - 단계에서 걸리게 했다. **게이트 PASS 는 동작을 증명하지 않는다** 의 실제 사례다. -- **같은 날 재빌드는 태그가 겹친다.** 태그에 빌드일을 넣어도 같은 날 다시 빌드하면 태그가 - 같아, 노드가 캐시한 옛 digest 가 그대로 쓰인다(`imagePullPolicy: IfNotPresent`). - 이번엔 검증용으로 `Always` 를 넣어 우회했다 — 프레임워크 차원의 미결 사항이다. -- **모듈 업그레이드 제안값은 최소치다.** 모듈 간 제약으로 더 올려야 할 수 있다 - (`requires @vX, not @vY` 로 빌드가 실패한다). - -**Go 모듈 CVE 조치를 자동화했다** — `scripts/build/suggest-go-upgrades.py` 가 스캔 -리포트의 `FixedVersion` 에서 `GO_MODULE_UPGRADES` / `GO_BUILDER_TAG` 를 산출한다. 사람이 -CVE 를 훑어 최대값을 고르지 않는다. 다음 조치에서 바뀌는 것은 `build.env` 뿐이고 -Dockerfile 은 그대로다(모듈 목록·패키지 목록을 전부 값으로 뺐다). - -### 남은 것 (argo-cd) - -- `argo-cd/7.8.11` 삭제 여부 — 지우면 게이트 PASS -- 자체 빌드 이미지의 정식 반영 경로: CI `build-image.yml`(`workflow_dispatch`) 로 다시 태워 - 볼지, 로컬 push 로 끝낼지. 이번엔 로컬에서 `docker.io/paasup` 에 push 하고 운영 argocd 를 - 교체해 동작 확인까지 마쳤다. - -### 별건으로 발견한 airflow 문제 2건 - -- **매니페스트가 비결정적이다.** `webserver-secret-key-secret.yaml` 이 `randAlphaNum` 으로 - 매 렌더마다 새 값을 만들어, repo-server 가 매니페스트를 다시 렌더할 때마다 Secret 과 - Deployment 의 체크섬 애노테이션이 함께 바뀌어 webserver 가 롤링되고 웹 세션이 끊긴다. - 캐시된 매니페스트를 그대로 쓰는 reconcile 만으로는 일어나지 않지만(4시간 19분 동안 롤아웃 - 0회), **일반 refresh 로도 일어난다**(hard refresh 나 ArgoCD 재시작에 한정되지 않는다 — - refresh 직후 새 ReplicaSet 생성을 실측). 카탈로그 기본값에 없어 모든 테넌트가 해당된다. - - 조치 방향은 정해졌고 **아직 커밋 전이다**: `dip-values.yaml` 에 - `webserverSecretKey: "$WEBSERVER_SECRET_KEY"` 를 두고, dip-console-api 의 - `catalog_airflow.go` `DeployPreInstall` 이 배포 시점에 난수로 치환한다. 차트 안에서는 못 - 고친다 — ArgoCD repo-server 는 클러스터 접근 없이 렌더하므로 `lookup` 이 항상 빈 값이고 - (실측), 릴리스명 기반 결정적 파생은 공개 정보라 세션 서명 키로 부적합하다. 이미 배포된 - 테넌트는 저장된 values 에 placeholder 가 없어 **재배포해야 적용된다**. -- **migrations job 이 5분마다 재실행된다.** `migrateDatabaseJob.useHelmHooks: false` 로 - hook 이 아닌 추적 리소스가 됐는데 `ttlSecondsAfterFinished: 300` 이 그대로라, TTL 삭제 → - ArgoCD selfHeal 재생성이 반복된다. `ttlSecondsAfterFinished` 를 끄거나 hook 으로 되돌린다. +트리거는 시간이 아니라 **상태**다. 다만 놓친 것을 걷어내기 위해 **월 1회 점검**한다 — +30일 넘게 남아 있는 항목이 있으면 위 셋 중 하나로 보내고 위의 "점검" 날짜를 갱신한다. --- -**apisix@2.16.0 전체가 게이트 PASS 로 완결됐다(2026-08-12).** `images/adc/` 신규 추가로 -마지막 남은 이미지까지 해소 — 차단 55건(2026-08-12 초 기준) → **0건**. adc· -apisix-ingress-controller·apisix(paasup/apisix) 세 자체 빌드 이미지 전부 게이트 -실효 CRITICAL/HIGH 0/0. apisix-ingress-controller·apisix 두 이미지는 사용자가 별도 -테스트 클러스터에서 **배포 검증 완료**(2026-08-12) — adc 는 아직 미완료 -(`.claude/deploy-test-procedure.md` 로 apisix-ingress-controller 와 함께 배포해 -dump/diff/sync 백엔드 연동까지 확인 필요). +## 열린 이슈 -**adc — "태그 교체로 끝난 줄 알았는데 게이트 재검증하니 아니었다".** 0.27.1→0.29.0 -태그 교체 후 `custom-values.yaml`에 "게이트 PASS 확인"이라고 적어뒀는데, 이건 벤더 -등급만 본 결과였다 — 실제 `cve-gate.py`(max(벤더,NVD))로 재검증하니 glibc regex/ -collating 스택오버플로 4건(CVE-2019-1010022/23, CVE-2018-20796, CVE-2019-9192, -전부 libc6, Debian "affected, 수정 없음" 영구 고정)이 실효 CRITICAL/HIGH 로 여전히 -차단(FAIL). `images/adc/` 자체 빌드로 해소 — 업스트림 Dockerfile -(`libs/tools/src/docker/Dockerfile`)이 `node:lts-bookworm-slim` 빌더 + -`gcr.io/distroless/nodejs24-debian13` 최종인데, **빌더 스테이지는 그대로 두고 최종 -베이스 한 줄만 SUSE BCI + `nodejs24`(zypper)로 교체** — apisix/ -apisix-ingress-controller 자체 빌드보다 훨씬 단순한 유형(cnpg-postgresql 과 같은 -"OS 패키지 재설치형", 빌드 시간 2분 이내). SUSE 글리브C 에 이 4건이 없음을 사전에 -직접 SBOM·스캔으로 검증한 뒤 진행했다. 게이트 PASS(실효 C/H 0/0). -`docker.io/paasup/adc:0.29.0-security-hardened-20260812` 로 push·카탈로그 반영 완료. -**이 사례는 일반화할 교훈이 있다 — "벤더 릴리스 노트가 CVE 를 줄였다고 명시해도, 게이트 -로 직접 재검증하기 전엔 PASS 로 단정하지 않는다"**(`sbom-cve-gate`/`cve-remediation` -skill 이 이미 강조하는 원칙이지만 이번에 실제로 걸렸다). +| # | 내용 | 상태 | +|---|---|---| +| [#29](https://github.com/paasup/dip-catalog/issues/29) | airflow 반복 재배포 — 비결정적 webserver secret key + migrations job TTL 루프 | 조치안 확정, **로컬 미커밋** | +| [#31](https://github.com/paasup/dip-catalog/issues/31) | 자체 빌드: 같은 날 재빌드 시 태그 충돌로 노드 캐시가 반영을 막는다 | 선택지 3안, 미결정 | +| [#30](https://github.com/paasup/dip-catalog/issues/30) | `SBOM_PIPELINE_IMAGE` 를 `docker.io/paasup` 으로 이전할지 | 수동 작업 3종, 미결정 | +| [#32](https://github.com/paasup/dip-catalog/issues/32) | adc 이미지 배포 검증 미완료 (게이트만 PASS) | apisix-ingress-controller 와 함께 배포 필요 | +| [#33](https://github.com/paasup/dip-catalog/issues/33) | 이미지 문서가 이 레포에 없는 `doc/decisions`·`doc/analysis` 를 인용 | 우선순위 낮음 | +| [#19](https://github.com/paasup/dip-catalog/issues/19) | 카탈로그 CVE 트리아지 기준선 | 게이트 강제 전환의 선행 조건 | +| [#7](https://github.com/paasup/dip-catalog/issues/7) | CVE 조치 파이프라인 (상위 추적 이슈) | 게이트 강제력 전환 결정이 여기 있다 | +| [#3](https://github.com/paasup/dip-catalog/issues/3) · [#9](https://github.com/paasup/dip-catalog/issues/9) | SBOM 파이프라인 도입 · postgresql-ha 대체 | 본문상 완결로 보인다 — **닫을지 확인 필요** | -**apisix 자체 이미지도 SUSE BCI 자체 빌드로 교체 완료(2026-08-12).** `images/apisix/` -신규 — 기존엔 별도 프로젝트(`paasup/dataup` 레포 `experiment/apisix/apisix-plugin`)에서 -`apache/apisix:3.17.0-debian`(이미 최신 태그) 위에 keycloak-authz 플러그인만 얹어 -빌드했는데, 그 Debian 베이스 자체가 차단 26건이었다. 문제는 이게 vanilla OpenResty가 -아니라 WASM(wasmtime)·mod_dubbo 등 15개 커스텀 모듈을 정적으로 얹은 "APISIX-Runtime" -이라, apiseven 의 빌드 파이프라인(`api7/apisix-build-tools`, 태그 -`apisix-runtime/1.3.6`)이 Debian/RHEL 용으로만 배포하고 SUSE 용은 없다 — vanilla -OpenResty(openresty.org 가 SLES 공식 지원)로 축소하는 것도 검토했으나, WASM·dubbo -관련 기능이 필요하다는 판단으로 커스텀 모듈 전체를 소스에서 그대로 재현하는 쪽으로 -갔다. 3단계(runtime: OpenSSL/zlib/PCRE+OpenResty+6개 모듈 컴파일 → apisix-app: -luarocks 로 APISIX 앱 설치 → final: SUSE BCI 로 산출물 이관+keycloak-authz 오버레이) -빌드, 실측 함정 다수 해결(zlib→OpenSSL 순서, lua-resty-saml 의 libxml2 2.12 시그니처 -불일치를 gcc 래퍼로 우회, `ui/`(admin 대시보드) 부재 허용, SUSE 공유 라이브러리 -soname 등 — 상세는 `images/apisix/README.md`). 실제 nginx 기동 + HTTP 요청 응답까지 -확인하는 `verify.sh` 작성(초판에 curl 연결 실패를 "000" 문자열로 오판하는 버그가 -있었음 — 수정 완료). 게이트 PASS(실효 C/H 0/0, 업스트림 26건에서 완전 해소). -`docker.io/paasup/apisix:3.17.0-security-hardened-20260811` 로 push·카탈로그 반영 -완료. **배포 검증 완료(2026-08-12, 사용자가 별도 테스트 클러스터에서 확인).** +## 진행 중 -**apisix 카탈로그 CVE 조치 완료(2026-08-11).** `cve-remediation` skill(신설, -`.claude/skills/cve-remediation/SKILL.md`)의 결정 절차를 처음 실제 적용한 사례. 조치 전 -차단 실효 CRITICAL/HIGH 165건(2.16.0 기준, 구버전 2.14.0 포함 시 331건) → 조치 후 55건. +- **PR [#28](https://github.com/paasup/dip-catalog/pull/28)** — argo-cd CVE 조치(7.7.0 제거 · + dex 비활성 · 자체 빌드). 리뷰 대기. -- **`docker.io/bitnamilegacy/etcd:latest`(차단 65건) 완전 제거** — bitnami 가 무료 - 레포를 동결한 레거시 미러라 `latest` 태그만 제공(bitnami/containers#83267), 태그 - 교체로 해소 불가. `etcd.enabled: false` 로 기본값을 바꾸고 카탈로그 자체 `etcd` 차트 - (`manifests/helm/etcd/1.1.12`)를 `externalEtcd` 기본 대상으로 연결했다 - (`manifests/helm/apisix/2.16.0/custom-values.yaml`). -- **`ghcr.io/api7/adc` 를 최신 태그(0.27.1)로 교체** — 완전 해소는 아니고 34→29건으로 - 부분 완화(같은 debian 12.14 베이스가 계속 쓰여 OS 패키지 CVE 잔존). 상위 태그가 - 이미 최신이라 추가 레버는 자체 빌드뿐 — 아직 안 함. -- **`images/apisix-ingress-controller/` 신규 자체 빌드** — 이미 최신 태그(2.1.0)인데도 - 차단 25건(21건이 정적 링크 Go 모듈: stdlib/x-net/x-text/grpc/otel — 태그 교체 불가, - 나머지 4건은 distroless 베이스 OS 패키지). `v2.1.0` 태그 코드는 그대로 두고 취약 - 모듈만 `go get`+`go mod tidy` 로 강제 업그레이드, 베이스는 SUSE BCI(`bci-micro`)로 - 교체 → 게이트 PASS(실효 C/H 0/0). `docker.io/paasup/apisix-ingress-controller: - 2.1.0-security-hardened-20260811` 로 push·카탈로그 반영 완료. **배포 검증은 아직 - 안 함**(이 세션에 클러스터 없음) — `.claude/deploy-test-procedure.md` 로 필수 수행. -- **`scripts/build/patch-catalog-tag.py` 확장** — `TAG_BLOCK` 이 점 구분 중첩 경로(예: - `ingress-controller.deployment.image`)를 지원하도록 바꿨다(기존엔 top-level 키만 - 가능). apisix 서브차트 alias 때문에 필요해짐. 기존 이미지(단일 세그먼트 `image`) - 회귀 테스트로 동작 동일함 확인. -- **`manifests/helm/apisix/2.14.0` 삭제** — 사용자 지시. 자체 빌드가 앱 버전(2.0.1)까지 - 같이 올리며 두 카탈로그 버전이 서로 다른 컨트롤러 버전을 요구하는 복잡도가 생겨서, - 구버전을 유지하는 대신 최신(2.16.0) 하나로 정리했다. `doc/catalog-stack-classification.md` - 갱신 완료. -- **남은 것**: `paasup/apisix:3.17.0-keycloak-authz`(Keycloak authz 플러그인 포함 커스텀 - 빌드, 차단 26건, debian 12.14 OS 패키지) — 빌드 소스가 dip-catalog 밖의 별도 프로젝트 - (`dataup experiment/apisix/apisix-plugin`)에 있어 **이 레포에서 재빌드 불가**. 그 - 프로젝트에서 베이스를 갱신해야 한다. +## 아직 이슈로 만들지 않은 것 -**CVE 게이트(`scripts/pipeline/cve-gate.py`)를 도입했다 — 아직 warn-only다.** `sbom.yml` -에 게이트 판정 스텝을 추가했지만 `--warn-only` 로 실행돼 실패해도 워크플로/PR 을 막지 -않는다. **45+ 개 카탈로그 차트가 이 게이트로 한 번도 트리아지된 적이 없다** — 강제 -게이트(`--warn-only` 제거)로 전환하려면 먼저 전체 카탈로그 스캔 1회로 몇 개 이미지가 -차단되는지 파악해야 한다. `gh workflow run helm-catalog-sbom --ref main`(또는 대상 -브랜치)으로 전체 스캔 후 `cve-gate.md` 아티팩트를 확인한다. +- **`argo-cd/7.8.11` 삭제 여부** — 카탈로그 차단 CVE 239건이 전부 이 동결 버전 몫이고, + 지우면 게이트가 PASS 로 떨어진다. 직전 버전을 없애는 결정이라 PR #28 에서 보류했다. +- **자체 빌드 이미지의 정식 반영 경로** — argocd 는 로컬에서 빌드·push 했다. CI + `build-image.yml`(`workflow_dispatch`)로 다시 태울지, 로컬 push 로 끝낼지. +- **`doc/define-chart-resources.md` 에 cloudnative-pg·cnpg-cluster·etcd 전용 절이 없다** — + 리소스 프로파일 자체는 각 차트의 `dip-resources-quotas.yaml` 에 있으므로 누락은 아니다. + 이 문서만 보는 사람이 세 차트를 못 찾는 것뿐이라 급하지 않다. -**`doc/cve-exceptions.json` 에 첫 예외가 들어갔다(2026-08-07, keycloak 자체 빌드).** -`CVE-2025-59250` — 트리비가 같은 mssql-jdbc jar 하나로 컴포넌트를 두 개 만들어 -(`13.2.1.jre11` from pom.properties / `13.2.1` from 파일명) 접미사가 잘린 쪽이 취약 -범위에 매칭된 **파싱 오탐**이다. 만료 2026-11-07. 이후 차단 항목이 나오면 같은 대응 -우선순위(상위 태그 교체 → 베이스 OS 교체 → 자체 빌드 → 예외 승인)를 지킨다. +## 알아둘 현재 상태 -**keycloak 자체 빌드로 베이스 OS 정책이 확정됐다(2026-08-07).** SUSE BCI 로 통일하되 -**버전은 이미지마다 실측해서 고른다** — BCI 16.0 이 나와 있지만 SLE_BCI 의 -`java-21-openjdk-headless` 가 15.7 은 `21.0.12`, 16.0 은 `21.0.11` 이라 최신 베이스가 -오히려 CVE 를 남긴다. 상세: `.claude/image-authoring.md` 원칙 2, -`images/keycloak/README.md`. - -**커버리지 자가진단(`CoverageProbe`)을 security-catalog 에서 이식했다(2026-08-03).** -`scan-sbom.sh` 가 os-pkgs findings 0건인 이미지의 SBOM 사본에 배포판별 센티널 패키지 -(deb/rpm/apk)를 주입해 재스캔하고, 발화 여부로 `CoverageProbe`(ok|none|n/a)를 리포트에 -기록한다 — `cve-gate.py` 는 이미 이 키를 읽도록 구현돼 있었으므로 소비 쪽 변경은 없다. -로컬에서 rpm(SUSE)·deb(Debian)·apk(Alpine) 세 경로 전부 실측 검증했고, 병렬 스캔(여러 -SBOM 동시 처리)에서도 회귀 없음을 확인했다. **이 이식으로 아래 "images/ 3종" 항목의 -결과가 실제로 바뀌었다** — cloudnative-pg·cnpg-postgresql 이 전에는 "데이터 커버리지 -이상"으로 FAIL 했으나(findings 가 전 심각도 0건이라 구버전 로직이 "측정 안 됨"으로 오판) -CoverageProbe 이식 후 재게이트하니 셋 다 `커버리지 ok, 실효 C/H 0/0, PASS` 로 나온다 -(양성 대조로 재확인: 같은 SBOM 에 오래된 취약 curl 버전을 주입해 재스캔하면 SUSE-SU -어드바이저리가 정상 검출됨 — trivy 의 SLES 15.7 커버리지 자체는 문제 없었다). - -**`images/`의 이미지 3종(`cloudnative-pg`, `cnpg-postgresql`, `etcd`)을 `docker.io/paasup` -로 재빌드·게이트·push 완료했다(2026-08-03).** `REGISTRY=docker.io/paasup` 로 빌드 → -`verify.sh` 전부 VERIFY-OK → 게이트 셋 다 `커버리지 ok, 실효 C/H 0/0, PASS` → push ​→ -`docker manifest inspect` 로 레지스트리 존재 재확인: -- `docker.io/paasup/cloudnative-pg:1.30.0-security-hardened-20260803` -- `docker.io/paasup/cnpg-postgresql:18.4-bci15.7-hardened-20260803` -- `docker.io/paasup/etcd:3.7.1-security-hardened-20260803` - -카탈로그 values 6개 파일(`manifests/helm/{cloudnative-pg/0.29.0,cnpg-cluster/1.0.0, -etcd/1.1.12}/{custom-values,dip-values}.yaml` — etcd 는 `dip-values.yaml`에 image -오버라이드 없어 5개만 실제 갱신)도 `patch-catalog-tag.py`로 새 태그로 교체하고 -`helm template`·`extract-helm-images.sh` 로 렌더링 결과까지 재확인했다. -`DOCKERHUB_USER`/`DOCKERHUB_TOKEN` 은 `paasup` 조직에 push 권한이 있음을 이번에 -확인했다(기존 "미확인" 상태 해소). 최종 런타임 베이스 OS 정책은 security-catalog 의 -SUSE BCI 고정 결정을 그대로 따랐고(ADR 자체는 미이관), 2026-08-07 keycloak 작업에서 -이 레포의 정책으로 확정됐다(위 참고). - -**남은 것: `SBOM_PIPELINE_IMAGE`(sbom.yml 이 쓰는 실행 컨테이너)는 이번 마이그레이션 -대상이 아니다** — 여전히 `docker.io/wbsong111/sbom-pipeline:latest` 를 가리킨다. -이건 자체 빌드 이미지와 무관한 별개 결정(아래 미결 결정 참고). - -**`doc/decisions/`·`doc/analysis/` 디렉토리 자체가 dip-catalog 에 없다.** 포팅된 3개 -이미지의 README·values 코멘트가 `doc/decisions/000X-*.md`, `doc/analysis/*.md`, -`doc/image-selection.md`, `doc/cve-zero-pipeline.md`, `doc/architecture/build-pipeline.md` -를 근거로 계속 인용하지만 이 경로들은 dip-catalog 에 하나도 없다(security-catalog -프로젝트에만 있음). 당장 급한 건 아니지만, 이 상태로는 이 레포만 보는 사람이 자체 빌드 -결정의 CVE 실측·비교 근거를 확인할 방법이 없다 — 각 README 에 이미 요약된 근거(CVE -번호·후보 비교표)를 압축한 로컬 stub ADR 작성을 검토한다. - -**`doc/define-chart-resources.md` 에 신규 차트 3종(`cloudnative-pg`, `cnpg-cluster`, -`etcd`) 의 전용 절이 없다 — 다만 리소스 프로파일 자체는 누락이 아니다.** 세 차트 모두 -`dip-resources-quotas.yaml` 에 small/medium/large 티어를 갖고 있고(차트 도입 커밋 6878b61 에 -함께 들어갔다), 다른 차트 절이 cnpg-cluster 를 외부 DB 로 쓸 때 그 파일을 가리킨다. 그래서 -CLAUDE.md 의 "신규 차트 추가" 규칙은 지켜진 상태다. 남은 건 이 문서만 보는 사람이 세 차트를 -찾지 못한다는 것뿐이라, 급하지 않다. - -**`SBOM_PIPELINE_IMAGE` 재빌드가 보류돼 있다.** 이 마이그레이션으로 빌드 컨텍스트 경로가 -`doc/scripts/Dockerfile` → `scripts/pipeline/Dockerfile` 로 바뀌었다. Dockerfile 내용 -자체는 안 바뀌었으므로 기존 `docker.io/wbsong111/sbom-pipeline:latest` 는 당장 깨지지 -않지만, `paasup` 네임스페이스로 이전할지는 별도 결정이 필요하다. **재빌드 + push + -Repo Variable `SBOM_PIPELINE_IMAGE` 갱신은 git 커밋으로 되지 않는 수동 작업**이다. - -**`build-image.yml` 의 `REGISTRY_HOST` 를 `docker.io/paasup` 로 설정했고, CI 에서 -동작을 확인했다(2026-08-04).** `workflow_dispatch` 로 `etcd`·`cloudnative-pg` 를 돌려 -레지스트리 로그인 → 빌드 → `verify.sh` → SBOM → 스캔 → 게이트 PASS → push → 카탈로그 -브랜치 push 까지 전부 성공했다(`build/etcd-20260804060806`, -`build/cloudnative-pg-20260804063006` 브랜치 생성 + `helm-catalog-sbom` PR 스캔 통과). -**GitHub Actions 시크릿(`DOCKERHUB_USER`/`DOCKERHUB_TOKEN`)이 `docker.io/paasup` push -권한을 갖는다는 것도 이때 확인됐다** — 기존 "로컬 자격증명만 확인" 상태 해소. - -**단, PR 자동 생성은 조직 정책으로 불가하다(실측 확정).** `GITHUB_TOKEN` 으로는 PR 을 -만들 수 없다(run 30882785612, GraphQL: "GitHub Actions is not permitted to create or -approve pull requests"). 리포 설정으로 못 바꾸는 제약이라 `gh pr create` 호출을 -워크플로에서 제거했고(`d449504`), 브랜치 push + Job Summary 의 compare 링크까지만 -자동화한다. **PR 오픈·병합 전 게이트 재확인·배포 검증은 사람의 몫이다.** - ---- - -## 미결 결정 - -### 게이트 강제력 전환 시점 -"45+ 차트 미검증" 문제 해소 후 결정한다. branch protection(필수 상태 체크)도 게이트 -강제와 짝을 이뤄야 의미가 있다 — 지금은 미설정. - -### SBOM_PIPELINE_IMAGE 재빌드/네임스페이스 전환 -결정 나면 `docker.io/paasup/sbom-pipeline:...` 로 빌드·push 하고 Repo Variable -`SBOM_PIPELINE_IMAGE` 를 갱신한다(수동, out-of-band). +- **게이트는 warn-only 다.** `sbom.yml` 이 `cve-gate.py --warn-only` 로 실행해 실패해도 + 워크플로·PR 을 막지 않는다. 강제 전환은 #19 → #7 순으로 간다. +- **CI 는 PR 을 자동 생성할 수 없다.** 조직 정책상 `GITHUB_TOKEN` 으로 PR 을 못 연다 + (실측 확정). 워크플로는 브랜치 push + Job Summary 의 compare 링크까지만 자동화한다.