hardened-containers 에 infisical 이미지가 추가·발행됐다(gate pass). image-map
을 등록하고 custom-values.yaml 을 자체 빌드 이미지(docker.io/paasup/infisical)로
전환해, keycloak 과 동일하게 catalog-tag-update.yml 이 앞으로의 태그 갱신을
자동 반영하도록 한다.
* chart_version_detector: repo가 이미 등록돼 있으면 update를 건너뛰던 버그를 고친다
repo alias가 이미 helm repo list에 있으면 add/update를 통째로 건너뛰어, 로컬에
예전에 캐시된 index.yaml을 그대로 썼다 — latest_version이 조용히 낡은 값으로
나온다(실측: secrets-operator에서 실제 최신 0.11.8 대신 0.11.4가 나왔다). repo
등록 여부와 무관하게 update는 항상 호출하도록 고친다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* secrets-operator v0.10.33 → 0.11.8
호환성: breaking_change_check 결과 breaking=false(이 차트는 custom-values.yaml이
없어 오버라이드 충돌 자체가 불가능). CVE: infisical/kubernetes-operator 이미지를
trivy+CoverageProbe로 실측한 결과 실효 HIGH 차단이 34→10건으로 줄었다(CRITICAL은
둘 다 0, 둘 다 CoverageProbe: ok로 측정 신뢰 가능). 신규 CRD 3개
(InfisicalAuth·InfisicalConnection·InfisicalStaticSecret) 추가 — 상세는
CUSTOM-README.md.
기존 v0.10.33 디렉토리는 카탈로그 정책대로 동결 보관한다(삭제하지 않음).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* secrets-operator 자체 빌드 이미지 배포 테스트 오버라이드 추가
dev 클러스터에서 hardened-containers 자체 빌드 이미지(v0.11.8-security-hardened)로
실제 업그레이드·CRD 적용·Infisical 시크릿 동기화까지 검증할 때 쓴 오버라이드.
카탈로그 값(이 차트는 애초에 custom-values.yaml 없음)은 건드리지 않는다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* kyverno 3.9.0(appVersion v1.19.0) 차트 추가 — 자체 빌드 이미지 사용
업스트림 이미지가 CVE 스캔 불가능한 베이스를 써서 자체 빌드로 대체한다(빌드 정의·근거는
hardened-containers). custom-values.yaml이 docker.io/paasup/* 7개 이미지를 가리키도록
고정했다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* kyverno 3.4.1 삭제
3.9.0 추가 후 정리
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
이번 세션에서 워크플로 수동 실행을 두 번 잘못 썼다: (1) 이미 연 PR에 대고
workflow_dispatch를 또 돌려 pull_request 자동 트리거와 완전히 중복시켰다,
(2) catalog-tag-update.yml이 GITHUB_TOKEN으로 force-push한 브랜치에서
--chart 스코프를 빠뜨려 카탈로그 전체(58차트)를 스캔했다.
sbom-cve-gate SKILL.md에 "gh workflow run은 언제 실제로 필요한가" 절을 추가해
정리했다 — PR이 이미 그 경로를 건드리면 pull_request 트리거가 자동으로 돈다
(gh pr checks로 먼저 확인). 수동 실행이 실제로 필요한 건 PR이 아직 없거나,
GITHUB_TOKEN이 force-push해 재귀 방지로 pull_request 이벤트가 안 뜨는 경우뿐이다.
수동 실행 시엔 -f chart=<차트명>으로 반드시 스코프를 좁힌다.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
"베이스 OS 교체"를 독립 레버로 두던 기존 표현을 없앤다 — 이 저장소의 베이스 교체
이력을 전부 찾아보면 예외 없이 hardened-containers 자체 빌드 커밋이다(argocd·
keycloak·cnpg-postgresql/etcd·apisix). 다른 배포판의 backport를 실제로 쓰려면 그
배포판 위에 앱을 다시 얹어야 하므로 그 자체가 이미 자체 빌드다. 빌드 없이 끝나는
진짜 무료 치환은 태그 교체와, 관리가 끊긴 벤더 이미지(bitnamilegacy 등)를 활성
벤더의 기존 이미지로 바꾸는 것뿐이다.
dip-catalog는 이제 이미지를 전혀 빌드하지 않는다(자체 빌드는 hardened-containers로
완전히 이관됨) — 이 사실을 CLAUDE.md·관련 skill에 명시한다. 유일한 예외인 apisix
keycloak-authz는 빌드가 아니라 이미 private으로 빌드된 이미지의 태그 반영이라
원칙에 위배되지 않는다.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
apisix에 얹는 keycloak-authz 커스텀 플러그인 오버레이는 hardened-containers
표준 파이프라인 밖에서 별도로 빌드돼 같은 리포지토리에 -keycloak-authz 접미사를
붙여 추가로 push된다. published.json에는 이 접미사 태그가 절대 안 잡혀서,
apply-published-tags.py가 무필터로 ref를 그대로 적용하는 지금 방식으로는
카탈로그가 순정(keycloak-authz 없는) 태그를 가리키게 되는 실질적 회귀가 있었다
(manifests/helm/apisix/2.17.0/custom-values.yaml이 실제로 이 상태였다).
scripts/build/resolve-apisix-keycloak-authz-tag.py를 새로 만들어
apply-published-tags.py가 읽기 전에 published.json 사본의 apisix 항목을
제자리에서 고쳐 쓴다 — base 태그에 -keycloak-authz 버전이 실제로 있으면 그걸로
교체하고, 없으면 apisix 항목 자체를 지워 표준 "없다" 분기가 자연히 건너뛰게
한다(순정 태그로 되돌리는 일은 없다). apply-published-tags.py는 한 줄도
안 건드렸다.
catalog-tag-update.yml에 이 전처리를 한 스텝만 끼워 넣고, 즉시 1회 실행해
2.17.0의 회귀를 이 커밋에서 바로 고쳤다(3.18.0-security-hardened-20260826 →
...-keycloak-authz).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hardened-containers 레포의 published.json(게이트 PASS + push 확인된 발행 기록)을
반영한다. 상세는 이 실행의 Job Summary 참고.
Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
hardened-containers가 apisix 3.17 라인 EOL로 3.18.0을 게이트 PASS로 새로 발행했다
(hardened-containers 커밋 3518f98: "3.17 line went EOL"). 이게 이 카탈로그의
차트 버전 갱신 트리거다 — appVersion을 3.18로 맞추려면 차트도 2.17.0(appVersion
3.18.0)으로 올려야 한다.
breaking_change_check: breaking=false. 다만 자동 diff가 못 잡는 실제 변경을
수동으로 하나 찾았다 — ingress-controller.enabled=true로 켜서 쓰는
apisix-ingress-controller 서브차트(1.2.0→1.3.0)에 새 CRD
l4routepolicies.apisix.apache.org가 추가됐다. helm_diff는 이 서브차트를 기본값
(off)으로만 렌더링해 애초에 스캔 대상에서 빠뜨린다 — CUSTOM-README.md에 수동
적용 안내를 남기고, 이 사각지대 자체를 catalog-update-pipeline SKILL.md에
기록해 다음 리뷰 때 놓치지 않게 했다.
catalog/image-map/{apisix,apisix-ingress-controller,adc}.env의 CHART_DIRS를
2.17.0으로 교체(2.16.0은 동결)하고, apply-published-tags.py로 발행된 실제 태그
(apisix 3.18.0-20260826, ingress-controller/adc는 최근 재스캔 리빌드분)를 반영했다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
catalog/image-map/README.md는 "이미지 하나가 여러 버전 디렉토리에 걸리는 것이
정상"이라 서술했지만, 실측하니 근거가 반대였다. 다버전 동시 매핑은 카탈로그
전체에서 cnpg-postgresql 하나뿐이었고, 그 cnpg-cluster 1.0.0/1.1.0도 "병행 유지"
근거가 없이 같은 태그를 쓰고 있었다 — 매핑 갱신 시 옛 버전을 안 지운 결과에
가까웠다. 반대로 argocd.env는 이미 argo-cd/10.4.0 하나만 가리키고 7.8.11은
빠져 있는데, 이건 사고가 아니라 의도적 정책이었다(MEMORY.md: "직전 버전을
없애는 결정이라 PR #28에서 보류했다" — 동결 자체는 이미 관행이었다).
정정한 정책: CHART_DIRS는 기본적으로 최신 버전 하나만 가리키고, 차트를
올리면 이 값을 교체한다(추가 아님) — 옛 버전은 그 시점 태그로 동결된다.
그리고 차트를 올리는 트리거 자체도 "업스트림에 새 차트가 있다"가 아니라
"지금 쓰는 앱 버전을 유지 못 하는 구체적 이유"(CVE·EOL·필수 기능·호환성)여야
한다 — catalog-update-pipeline SKILL.md에 이 기준이 없었어서 추가했다.
keycloak.env(7.2.2→7.3.0)·cnpg-postgresql.env(1.0.0 제거)를 새 정책에 맞춰
바로잡는다 — keycloakx 7.3.0 업그레이드(#51) 때 빠뜨렸던 매핑 갱신이기도 하다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5)단계가 다른 단계들과 다르게 update_docs_file 모듈을 sys.path.insert("$UPDATE_CATALOG_ROOT/src")
로 직접 import했는데, agent/update_catalog/src/ 는 애초에 존재한 적이 없다(각 Skill은
skills/<name>/scripts/update_catalog/ 에 자기 완결적으로 있다) — 그래서 이 단계는 항상
ModuleNotFoundError로 죽었다. keycloakx 7.2.2→7.3.0 업그레이드 중 실측 확인.
다른 단계와 동일하게 update_docs_file/scripts/run.py를 서브프로세스로 호출하도록 고쳤다.
격리된 샌드박스 레포에서 재현 시나리오로 수정 후 정상 동작 확인.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
breaking_change_check 결과 breaking=false — custom-values.yaml이 실제로 쓰는 키
(image, http.relativePath, command, ingress, proxy, resources, database, extraEnv)
중 어느 것도 diff에 걸리지 않았다. 유일한 템플릿 변경(probe 경로가
managementRelativePath를 coalesce로 우선하도록 바뀜)도 relativePath: "/" 를 그대로
쓰므로 동작 영향이 없다. CRD·의존성 변경 없음.
appVersion은 26.6.4→26.7.2로 오르지만 custom-values.yaml이 자체 빌드 이미지
태그(26.7.1-bci15.7-hardened)를 명시적으로 고정하므로 이 업그레이드로 실제 배포
버전이 바뀌지는 않는다 — 26.7.2로 올리려면 hardened-containers에서 별도로
자체 빌드해야 한다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hardened-containers가 이미 rescan.yml로 매일 자율 재스캔·재빌드하고, sbom.yml이
custom-values.yaml 기준으로 자체 빌드 이미지를 다른 카탈로그 이미지와 동일하게
스캔하고 있어 self-build-drift-check.yml의 트리거·전용 스캔이 순수 중복이었다
(SECURITY_IMAGES_DISPATCH_TOKEN도 등록된 적 없어 트리거 스텝은 항상 실패하던
죽은 코드). check-rebuild-needed.py가 더하던 fixable/no-fix 구분도 cve-gate.py
리포트에 이미 있어 흡수할 필요 없이 삭제했다. 근거는 ADR 0005.
곁들여 CI 위생 문제(trivy DB 캐시 없음, concurrency 없음, catalog-tag-update.yml의
브랜치 누적)를 함께 고치고, hardened-containers의 docs/image-authoring.md가
docs/image-authoring/ 로 분할된 것을 반영해 관련 링크를 정정했다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
실제 레포명은 hardened-containers인데 이관 커밋 이후 문서·워크플로·차트 주석에
잘못된 이름 security-images가 남아 있었다. 텍스트 참조 전체를 정정하고
doc/migrations/의 이관 핸드오프 문서도 파일명까지 리네임했다. env var/secret
이름(SECURITY_IMAGES_REPO, SECURITY_IMAGES_DISPATCH_TOKEN)은 GitHub Secret
재등록이 필요한 별도 운영 작업이라 이번 텍스트 정정 범위에서 제외했다.
덧붙여 scripts/build/patch-catalog-tag.py의 docstring이 삭제된 build-image.yml을
호출자로 여전히 가리키고 있던 것도 실제 호출 경로(check-rebuild-needed.py /
apply-published-tags.py → catalog-tag-update.yml)로 고쳤다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
images/·scripts/build/build-hardened-image.sh·suggest-go-upgrades.py·
build-image.yml·.claude/image-authoring.md·이미지 ADR(0001·0002·0004)을 삭제했다 —
전부 별도 public 레포 security-images 로 이미 이관됐다.
카탈로그 쪽에는 "무엇을 배포 중인가"를 아는 부분만 남긴다:
- catalog/image-map/<image>.env — 옛 catalog.env 의 카탈로그 레이아웃 정보만 뗀 것
- scripts/build/check-rebuild-needed.py — 드리프트 탐지(A 파트)만 남기고 핀 판단
(B 파트: pin_changes/apply_changes/parse_module_specs)은 제거
- scripts/build/apply-published-tags.py(신규) — security-images 의 published.json
을 읽어 카탈로그 values 를 패치
- .github/workflows/{self-build-drift-check,catalog-tag-update}.yml(신규) — 각각
드리프트 스캔+트리거, 발행 태그 반영
effective_severity 를 cve-gate.py 로 옮겼다 — check-rebuild-needed.py 가 핀 도구를
거치지 않고 게이트를 직접 로드하게 하기 위한 선행 작업이다.
두 레포의 계약은 published.json 스키마 하나뿐이다 — security-images 는 이 카탈로그를
모른다(단방향 의존). 이관 배경·결합점 전체는
doc/migrations/self-build-images-to-security-images.md.
부수 수정: 자체 빌드 이미지를 참조하는 차트 values/README 의 죽은 링크(images/**,
doc/decisions/000{1,2,4}, .claude/image-authoring.md)를 security-images 레포를
가리키는 서술로 교체. deploy-test 스크립트·CUSTOM-README 의 개인 Docker Hub 계정
(docker.io/wbsong111) 을 docker.io/paasup 로 교체.
pitfalls.md 의 "스캐너 결과를 그대로 믿지 말 것" 절은 sbom-cve-gate skill 이 차트
축 설명에 실제로 참조하고 있어 남겼다 — "이미지 태그의 베이스 OS" 절만 제거했다
(다른 참조 없음, security-images 문서로 이관 완료).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nemo 의 NGC API 키와, airflow git-sync · gitea · keycloakx 가 공유하던 암호가
평문으로 커밋돼 있었다. airflow 쪽은 같은 값을 base64 로 감싼 것이라 5개 파일을
함께 교체했다.
4개 차트 helm template 렌더 확인. 히스토리와 GitHub PR ref 에는 남으므로 키
로테이션이 별도로 필요하다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#29 는 webserverSecretKey 에 placeholder 를 두고 콘솔이 배포 시점에 난수로 치환하게 했다.
신규 배포는 고정됐지만 재배포 경로에서 되살아났다 — 콘솔의 재배포 분기가 이미 치환된
values 가 아니라 placeholder 가 남아 있는 default_values_yaml 을 다시 parse 해서 매번 새
난수를 만든다. 실측에서 재배포 3회에 webserver ReplicaSet 이 3개 생겼고, airflow-config ·
metadata-secret · webserver-config 등 다른 체크섬은 전부 같은데
checksum/webserver-secret-key 하나만 달랐다.
fernet key(#38, PR #45)가 이미 쓰는 "이름을 넘긴다" 방식으로 바꾼다. 이쪽이 값을 고정하는
것보다 강하다 — webserverSecretKeySecretName 이 설정되면 Secret 템플릿이 `if not` 가드로
비고 checksum/ 애노테이션이 해싱하는 대상이 상수가 된다. Deployment spec 이 애초에 키 값에
의존하지 않게 되므로 앞으로 어떤 이유로 키가 바뀌어도 롤링이 발생하지 않는다. 키가 tenant
레포(git)에 평문으로 남지도 않는다.
값 이름은 webserverSecretKeyName 이 아니라 webserverSecretKeySecretName 이다
(values.yaml:500). 차트가 읽는 Secret 의 키 이름은 webserver-secret-key 로 하드코딩돼
있다(_helpers.yaml:99).
CUSTOM-README 5절은 두 비밀값을 같은 방식으로 설명하도록 고치고, helm 직접 배포 절차에
webserver Secret 생성을 추가했다. 마이그레이션 시 ArgoCD prune 주의사항도 적었다 —
차트가 만들던 Secret 은 tracking-id 가 붙은 추적 대상이라 콘솔이 인수할 때 그 애노테이션을
벗겨내야 한다.
세 파일 모두 service-catalog(ac5ae54, 배포 반영 완료)와 바이트 동일하다.
Closes#46
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
차트가 fernet key Secret 을 직접 만들면 helm.sh/hook: pre-install +
hook-delete-policy: before-hook-creation 이 붙는다. ArgoCD 는 훅을 "삭제 후 재생성" 하는데,
변경이 없는 sync 에서는 삭제 직후 operation 이 종료되어 재생성되지 않는다. 컨트롤러 로그로
확인했다.
변경 있는 sync waiting for deletion ... -> serverside-applied -> Succeeded 'all tasks run'
no-op sync waiting for deletion ... -> Succeeded (같은 초) 'no more tasks'
파드는 이미 주입된 env 로 계속 돌아 증상이 즉시 드러나지 않지만, 재시작하는 순간
CreateContainerConfigError 로 기동하지 못한다. refresh·selfHeal 로 흔히 발생하는 상황이다.
fernetKeySecretName 을 주면 차트가 Secret 을 아예 만들지 않으므로
(templates/secrets/fernetkey-secret.yaml:23) 훅 자체가 사라지고, 모든 워크로드가 그 이름을
secretKeyRef 로 참조한다(templates/_helpers.yaml:395-397).
$FERNET_KEY_SECRET 은 placeholder 다. dip-console-api 가 <릴리스명>-fernet-key Secret 을
만들고(없을 때만) 이 자리를 그 이름으로 치환한다. 콘솔이 만든 Secret 은 ArgoCD 추적 대상이
아니라 sync 의 영향을 받지 않는다.
값이 아니라 이름을 넘기는 이유가 하나 더 있다 — fernet key 는 메타DB 의
Connection·Variable 암호화에 쓰이므로 tenant 레포(git)에 평문으로 남기지 않는다.
업스트림도 can only be set during install, not upgrade 라고 못박고 있어, 이미 배포된
테넌트는 재배포 시 현재 Secret 을 그대로 인수한다.
CUSTOM-README 5절은 이 문서의 성격(배포 방법 · custom-values.yaml 키 설명)에 맞게 키
테이블과 배포 절차 중심으로 다시 썼다(69줄 -> 48줄). 상세 근거는 이슈로 넘긴다.
redis 훅 Secret 2종은 CeleryExecutor 전환 예정이 없어 조치하지 않는다. 참조 워크로드가
0개인 것을 클러스터에서 확인했다. 전환 시 처리 방법만 문서에 남긴다.
검증
service-catalog 에 선반영해 테스트 클러스터에서 확인했다. 재배포 후 sync 를 걸었을 때 훅인
redis Secret 2종은 또 삭제됐고 콘솔이 관리하는 fernet Secret 만 살아남았다(resourceVersion
미동, 값 동일). 이 레포 기준으로도 helm template 2회 렌더에서 차트가 fernet Secret 을 만들지
않고 webserver 체크섬이 동일함을 확인했다.
관련: paasup/dip-catalog#38, paasup/dip-console-api#100
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adc 배포 검증(#32)은 완료로 닫았다 — 클러스터에 8일 이상 떠 있고, adc 가 컨트롤러가
APISIX 에 쓰는 유일한 경로인데 admin API 에 라우트 15건·SSL 9건이 전부 컨트롤러가 쓴
것으로 들어가 있다. 이슈 본문의 CLI 체크리스트(dump/diff/sync)는 배포 형태(사이드카 +
유닉스 소켓)와 맞지 않아 실측 항목으로 다시 매핑했다.
닫으면서 드러난 것을 미결로 남긴다 — 게이트 PASS 만으로 patch-catalog-tag.py 가 태그를
올리므로 카탈로그가 클러스터보다 앞서 있고(adc 0812 vs 0813, ingress-controller 0811 vs
0820), 그 격차를 재검증하는 경로가 없다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#31 코멘트에 "자체 빌드 이미지를 쓰는 차트 7곳을 Always 로 바꿔야 한다" 고 적었는데
잘못됐다. Always 를 카탈로그 기본값으로 넣으면 파드 시작마다 kubelet 이 레지스트리에서
digest 를 해석하고, 레지스트리 장애·Docker Hub rate limit 이 파드 기동을 막는 경로가
생긴다. 태그 겹침은 같은 날 재빌드한 검증 시점에만 생기는 문제인데 그 대가를 운영 내내
지불하는 셈이다.
절차는 deploy-test-procedure.md 로 옮긴다 — 검증 대상 워크로드에만 patch 로 걸고
digest(imageID)로 확인한다. 태그가 같으므로 태그를 보는 것은 아무것도 증명하지 않는다.
self-build-image SKILL 의 함정 항목은 이 문서를 가리키게 하고 "프레임워크 차원의 미결" 을
지운다 — 미결이 아니라 결정됐다. MEMORY.md 의 "values 반영 안 됨" 항목은 반영할 것이
없으므로 지운다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
닫힌 이슈 3건(#29 · #30 · #31)이 "열린 이슈" 표에 남아 있었고, 머지된 PR #28 이
"진행 중" 에 있었다. 대신 아직 열려 있는 #42 · #38 · #37 이 표에 없었다.
닫으면서 사라질 뻔한 것을 "아직 이슈로 만들지 않은 것" 으로 옮긴다 —
`imagePullPolicy: Always` 는 #31 에 결정만 기록하고 닫았고 values 반영은 안 됐다.
레포 분리 시점 작업 2건(카탈로그 반영 워크플로 · 게이트 composite action)과 워크플로
결함 3건도 어디에도 없어 같이 적는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`demo01-air` 에서 webserver 가 반복 롤링되고 migrations job 이 5분 주기로 재실행되던
것을 추적한 결과, 원인이 서로 다른 두 개였고 둘 다 카탈로그 기본값에서 비롯됐다 —
모든 airflow 테넌트가 해당된다.
원인 1 — webserverSecretKey 가 렌더할 때마다 바뀐다
값을 주지 않으면 차트가 템플릿에서 직접 난수를 만들고(`randAlphaNum 32`), 그 Secret 이
webserver·worker Deployment 의 `checksum/webserver-secret-key` 애노테이션에 물려 있어
렌더할 때마다 Deployment spec 이 바뀐다. 캐시된 매니페스트를 쓰는 reconcile 로는
일어나지 않지만 일반 refresh 만으로도 일어난다(실측).
차트 안에서는 못 고친다. `lookup` 재사용은 ArgoCD repo-server 가 클러스터 접근 없이
클라이언트 렌더를 하므로 항상 빈 값이다(클러스터에 Secret 이 실재하는데도 빈 값인 것을
확인). 릴리스명·네임스페이스 기반 결정적 파생은 렌더는 안정되지만 파생 입력이 전부 공개
정보라 세션 서명 키를 예측할 수 있어 부적합하다. 값이 차트 바깥에서 와야 한다.
그래서 `$WEBSERVER_SECRET_KEY` placeholder 를 두고, 배포 시점에 dip-console-api 가
난수로 치환한다(`catalogs/catalogtype/catalog_airflow.go` 의 `DeployPreInstall`).
치환된 values 가 tenant 레포에 저장되므로 이후 렌더는 그 고정값을 쓴다. 공용
`catalog_type_base.go` 가 아니라 airflow 전용 훅에 둔다.
원인 2 — migrations job 이 TTL 삭제 ↔ selfHeal 재생성 루프에 빠진다
`useHelmHooks: false`(업스트림이 ArgoCD 환경에 권고) 로 Job 이 ArgoCD 추적 리소스가
됐는데 차트 기본값 `ttlSecondsAfterFinished: 300` 이 그대로 남아 있었다. 완료 300초 뒤
TTL 컨트롤러가 Job 을 지우면 ArgoCD 가 OutOfSync 로 보고 selfHeal 이 다시 만든다.
템플릿에서 TTL 은 `useHelmHooks` 와 무관하게 항상 렌더되고 `kindIs "invalid"` 일 때만
빠지므로, `~` 로 꺼서 필드 자체를 없앤다. 대신 완료된 Job 객체가 남는다.
반영 위치가 2곳이다 — helm 카탈로그와 applicationset(`---` 뒤 두 번째 문서) 양쪽.
한쪽만 고치면 배포 경로에 따라 증상이 남는다.
주의 — dip-console-api 와 짝이다. 한쪽만 머지되면 placeholder 문자열이 그대로 키가
되거나(카탈로그만) 아무 일도 일어나지 않는다(console 만). 이미 배포된 테넌트는
재배포해야 적용된다.
fernet key 는 같은 구조지만 체크섬에 물려 있지 않아 증상·마이그레이션 경로가 다르다 —
별건(#38)으로 둔다.
Refs #29
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
파이프라인 실행 이미지가 개인 네임스페이스(docker.io/wbsong111)를 가리키고 있었다. 카탈로그의
다른 자체 빌드 이미지는 2026-08-03 에 전부 docker.io/paasup 으로 이전됐는데 이것만 남아 있었다 —
"카탈로그가 배포하는 이미지가 아니라 CI 도구" 라 이전 대상에서 빠졌던 것이다.
before docker.io/wbsong111/sbom-pipeline:latest
after docker.io/paasup/sbom-pipeline:20260820
롤링 태그를 쓰지 않는다
---------------------
:latest 를 유지하지 않고 날짜 태그로 갔다. 이 Dockerfile 은 helm/trivy 를 **빌드 시점 최신으로
설치**하므로 같은 파일이 매번 다른 이미지를 낸다 — 태그가 "무엇으로 스캔했는지" 의 기록이어야
한다. Repo Variable 이 전체 ref 를 담으므로 고정 태그를 써도 워크플로 수정이 필요 없다.
이번 이미지가 담은 것: helm v3.21.4 · trivy 0.74.0 · python 3.13.5 · git 2.47.3
검증
----
push docker.io/paasup/sbom-pipeline:20260820 (linux/amd64)
변수 갱신 gh variable set SBOM_PIPELINE_IMAGE
CI 실제 동작 helm-catalog-sbom limit=3 실행 → 전체 success
(Preflight 도구 확인 · 인벤토리 추출 · SBOM · 스캔 · 게이트 판정 전부 통과)
문서의 낡은 참조 4곳(doc/sbom-pipeline.md)과 Dockerfile 헤더 절차, sbom.yml 주석 예시를 고쳤다.
Dockerfile 헤더에는 왜 :latest 를 쓰지 않는지도 적었다.
Closes#30
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"manifests/applicationset/** 는 어떤 워크플로도 보지 않는다" 를 사각지대로 적었는데 잘못된
평가였다. 그 아래 dip-values.yaml 은 dip-console 이 배포 values 를 만들 때 쓰는 참조 파일이고,
같은 이미지를 manifests/helm/ 차트에서 이미 스캔·게이트한다. 같은 이미지를 두 번 스캔할 이유가
없어 인벤토리 추출 대상을 manifests/helm/ 으로 한정한 것이다.
이유를 적어두지 않으면 다음 사람이 또 "미검사 결함" 으로 보고한다 — 실제로 내가 그렇게 했다.
다만 그 가정이 무엇에 의존하는지도 함께 적었다: **두 곳이 같은 이미지를 가리킨다는 것**.
참조 파일이 차트와 다른 태그를 들고 있으면 스캔한 것과 배포되는 것이 갈린다. 태그 동기화는
스캔 커버리지와 별개 문제이고 patch-catalog-tag.py 의 CHART_DIRS 범위가 그것을 결정한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"파이프라인이 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>
* 차단 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>
위 4개 커밋은 build-image.yml 이 자동 생성한 것이고, cnpg-postgresql 은 1.0.0 만 갱신됐다.
1.1.0 도 같은 이미지를 가리키는데 낡은 태그(20260803)로 남아 있었다.
원인은 매핑이다. images/cnpg-postgresql/catalog.env 의 CHART_DIRS 가 1.0.0 하나만 선언해
워크플로가 1.1.0 을 볼 방법이 없었다. 카탈로그는 여러 버전을 동시에 보관하는 "버전 보관소"
이므로 이미지 하나가 여러 버전 디렉토리에 걸리는 것이 정상이다 — 매핑이 그것을 표현해야 한다.
CHART_DIRS="manifests/helm/cnpg-cluster/1.0.0"
→ CHART_DIRS="manifests/helm/cnpg-cluster/1.0.0 manifests/helm/cnpg-cluster/1.1.0"
다른 세 이미지는 전수 확인 결과 CHART_DIRS 가 참조를 전부 덮는다(etcd 1.1.12 ·
cloudnative-pg 0.29.0 · apisix 2.16.0). cnpg-postgresql 만 누락이었다.
이 누락은 조용히 지나간다는 점이 문제다 — patch-catalog-tag.py 는 "예상 패턴을 못 찾으면
실패" 하지만, 애초에 대상 목록에 없는 파일은 검사하지 않는다. 매핑이 불완전하면 게이트만
계속 그 이미지를 차단으로 잡고 이유를 알기 어렵다.
Refs #35
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
etcd·cloudnative-pg·apisix-ingress-controller 가 GO_BUILDER_TAG=1.26.5-trixie 에 핀돼
있어 새로 공개된 stdlib 차단 CVE 8건에 걸린다. 전부 Go 1.26.6 에서 고쳐졌다.
소스는 하나도 안 바꿨다 — CVE 데이터가 갱신됐을 뿐이다. 자체 빌드 이미지는 이렇게
가만히 있어도 게이트를 벗어난다는 것이 이번 사례의 요지다.
발견 경위
--------
PR #34(문서 전용)가 images/** 를 건드려 검증 빌드가 돌면서 드러났다. 오늘 날짜로
재빌드하니 etcd·cloudnative-pg 가 각각 실효 0/8 로 FAIL 했고, 기능 검증(verify.sh)은
둘 다 VERIFY-OK 였다 — 게이트만 실패했다. 대조군으로 어제 1.26.6 으로 올린 argocd 는
같은 실행에서 PASS 했다.
값은 손으로 고르지 않았다
------------------------
python3 scripts/build/suggest-go-upgrades.py --reports <trivy-reports>
→ GO_BUILDER_TAG=1.26.6-trixie / 모듈 업그레이드 제안 없음(stdlib 전용)
FixedVersion 의 `1.25.13, 1.26.6, 1.27.0-rc.3` 은 브랜치별 대안이라 최대값을 고르면
프리릴리스를 정식 버전으로 오독한다. 스크립트가 그 규칙을 처리한다.
apisix-ingress-controller 는 이번 검증 빌드 대상이 아니었다
--------------------------------------------------------
PR #34 가 이 이미지 파일을 건드리지 않아 매트릭스에서 빠졌을 뿐, 같은 툴체인 핀이다.
trivy 는 바이너리에 박힌 Go 버전으로 stdlib 을 판정하므로 1.26.5 로 빌드한 Go 바이너리는
예외 없이 해당된다. 이 PR 의 검증 빌드가 셋 다 돌려 확인한다.
각 build.env 주석에 "이 값은 go.mod 최소 요구가 아니라 stdlib 차단 CVE 를 해소하는
지점으로 정한다" 를 적어뒀다 — 다음 사람이 go.mod 만 보고 되돌리지 않게 하기 위함이다.
Refs #35
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>