자체 빌드 이미지 프레임워크를 security-images 레포로 이관하고 카탈로그 쪽을 정리한다
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>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
---
|
||||
name: cve-remediation
|
||||
description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH)를 잡았을 때 태그 교체·베이스 OS 교체·자체 빌드·예외 승인 중 어느 레버를 쓸지 판단할 때 사용한다. "이 CVE 어떻게 없애", "차단 CVE 뭐부터 해야 해", "자체 빌드 가야 하나 예외 가야 하나", "게이트 실패 다음 스텝" 같은 요청이 해당한다. sbom-cve-gate·self-build-image 사이 결정 단계만 담당하며 둘의 절차는 복제하지 않는다.
|
||||
description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH)를 잡았을 때 태그 교체·베이스 OS 교체·자체 빌드·예외 승인 중 어느 레버를 쓸지 판단할 때 사용한다. "이 CVE 어떻게 없애", "차단 CVE 뭐부터 해야 해", "자체 빌드 가야 하나 예외 가야 하나", "게이트 실패 다음 스텝" 같은 요청이 해당한다. sbom-cve-gate 로 게이트를 해석하는 것과 security-images 레포에서 자체 빌드를 실행하는 것 사이의 결정 단계만 담당하며 둘의 절차는 복제하지 않는다.
|
||||
---
|
||||
|
||||
# 차단 CVE 대응 절차
|
||||
|
||||
이 skill 은 실행하지 않는다 — 레버를 판단해 실행 skill로 넘긴다. 게이트 실행/해석은
|
||||
`sbom-cve-gate`, 자체 빌드는 `self-build-image`, 배포 검증은
|
||||
`sbom-cve-gate`, 자체 빌드는 **별도 레포 `security-images`**(그 레포의
|
||||
`docs/image-authoring.md`가 단일 출처), 배포 검증은
|
||||
[deploy-test-procedure.md](../../deploy-test-procedure.md)가 단일 출처다 — 여기 반복 안 한다.
|
||||
|
||||
## 흐름
|
||||
@@ -35,8 +36,11 @@ description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH)
|
||||
FixedVersion 이상인지, 같은 파일이 SBOM에서 컴포넌트 두 개로 잡히지 않는지(PkgPath
|
||||
비교) 확인한다. 실례: keycloak `CVE-2025-59250`(mssql-jdbc jar 하나가 두 컴포넌트로
|
||||
잡혀 잘린 쪽만 매칭된 오탐). 근거·만료일은 `doc/cve-exceptions.json`.
|
||||
6. 자체 빌드는 `self-build-image` + [image-authoring.md](../../image-authoring.md)로,
|
||||
태그/베이스 OS 교체는 카탈로그 values만 바꾸고 재게이트한다 — 별도 스크립트 없음.
|
||||
6. 자체 빌드는 별도 레포 `security-images`에서 한다 — 그 레포의
|
||||
`docs/image-authoring.md`가 절차 단일 출처다. 재빌드가 필요하면 그 레포의
|
||||
`build-image.yml`을 `workflow_dispatch`로 부른다(`scripts/build/check-rebuild-needed.py`
|
||||
가 배포 중인 이미지를 스캔해 대상을 판단한다). 태그/베이스 OS 교체는 카탈로그
|
||||
values만 바꾸고 재게이트한다 — 별도 스크립트 없음.
|
||||
7. 수정 후 반드시 재게이트하고 PASS라도 배포 검증까지 끝나야 종료다 — 1회로 끝난다고
|
||||
가정하지 않는다(keycloak은 1차 수정 후 재스캔에서 micrometer 2건이 새로 잡혔다).
|
||||
8. 착지는 브랜치 push까지다 — 조직 정책상 `GITHUB_TOKEN`으로 PR을 못 연다. 사람이 PR을
|
||||
@@ -44,5 +48,5 @@ description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH)
|
||||
|
||||
## 참고
|
||||
|
||||
- 레버 실행: [sbom-cve-gate](../sbom-cve-gate/SKILL.md) · [self-build-image](../self-build-image/SKILL.md)
|
||||
- 레버 실행: [sbom-cve-gate](../sbom-cve-gate/SKILL.md) · 자체 빌드는 별도 레포 `security-images`
|
||||
- 함정: [pitfalls.md](../../pitfalls.md) · 미결 사항: [MEMORY.md](../../../MEMORY.md)
|
||||
|
||||
@@ -55,8 +55,8 @@ gh run watch --repo <org>/dip-catalog
|
||||
상위 태그 교체 → 베이스 OS 교체 → 자체 빌드 → 예외 승인
|
||||
```
|
||||
|
||||
자체 빌드로 가야 한다면 `self-build-image` skill과
|
||||
[.claude/image-authoring.md](../../image-authoring.md)를 따른다. 판정 로직 상세는
|
||||
자체 빌드로 가야 한다면 별도 레포 `security-images`에서 한다 — 그 레포의
|
||||
`docs/image-authoring.md`가 절차 단일 출처다. 판정 로직 상세는
|
||||
`scripts/pipeline/cve-gate.py`의 모듈 docstring을 1차 출처로 본다.
|
||||
|
||||
## 참고
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
---
|
||||
name: self-build-image
|
||||
description: 자체 빌드 하드닝 이미지를 추가하거나 기존 빌드 정의를 변경할 때 사용한다. "이미지 자체 빌드해줘", "하드닝 이미지 추가", "차단 CVE를 자체 빌드로 해소", "build-hardened-image.sh 실행", "images/ 아래 새 이미지" 같은 요청이 해당한다. 현재 adc, apisix, apisix-ingress-controller, argocd, cloudnative-pg, cnpg-postgresql, etcd, keycloak 8종이 있다(정확한 목록은 images/ 디렉토리가 단일 출처).
|
||||
---
|
||||
|
||||
# 자체 빌드 하드닝 이미지
|
||||
|
||||
CVE 게이트 대응 우선순위(상위 태그 교체 → 베이스 OS 교체 → **자체 빌드** → 예외 승인)에서
|
||||
앞의 두 단계로 해소가 안 될 때만 온다.
|
||||
|
||||
## 원칙 — 오케스트레이션은 항상 하나다
|
||||
|
||||
**`scripts/build/build-hardened-image.sh` 하나가 모든 자체 빌드 이미지를 빌드한다.**
|
||||
OS 패키지 재설치든 소스 컴파일이든 스크립트는 같고, 차이는 전부 `images/<image>/` 안에 있다.
|
||||
|
||||
**"이 이미지는 성격이 다르다"는 이유로 새 오케스트레이션 스크립트를 만들지 않는다** —
|
||||
절차(빌드 → 기능검증 → SBOM → 스캔 → 게이트 → push)는 이미지 종류와 무관하게 동일하다.
|
||||
SBOM·스캔·게이트도 다시 만들지 않는다 — `build-hardened-image.sh`가 이미
|
||||
`scan-sbom.sh`/`cve-gate.py`를 호출한다.
|
||||
|
||||
## 실행
|
||||
|
||||
```sh
|
||||
IMAGE=<image> BASE_OS=<variant> bash scripts/build/build-hardened-image.sh /tmp/out
|
||||
```
|
||||
|
||||
`images/<image>/<variant>.build.env`가 계약(`DOCKERFILE`, `TARGET`, `BUILD_ARGS` 등)을
|
||||
선언하면 스크립트는 이미지 종류를 몰라도 된다. 전체 계약표와 신규 이미지 추가 7단계 절차는
|
||||
[.claude/image-authoring.md](../../image-authoring.md)에 있다 — 여기 복제하지 않는다.
|
||||
|
||||
CI는 `build-image.yml`이 `image` 입력으로 이미 파라미터화돼 있다. `images/<image>/catalog.env`만
|
||||
추가하면 워크플로 수정 없이 태울 수 있다.
|
||||
|
||||
## 새 Dockerfile 은 두 축을 먼저 고른다
|
||||
|
||||
**작성 규칙 본문은 [.claude/image-authoring.md](../../image-authoring.md)가 단일 출처다** —
|
||||
여기 복제하지 않는다. 이 표는 "어느 절을 읽어야 하는가" 만 정한다.
|
||||
|
||||
두 축은 **독립**이다. 같은 `bci-micro` 최종 위에 Go 빌더가 오기도 하고 Node 빌더가 오기도 한다.
|
||||
|
||||
**축 1 — 최종 런타임 베이스** (스캔·배포 대상. 원칙 2)
|
||||
|
||||
| 런타임이 필요로 하는 것 | 고른다 | 선례 |
|
||||
|---|---|---|
|
||||
| OS 패키지·셸 (zypper 설치, 셸 entrypoint) | `bci-base` | `adc` `apisix` `argocd` `cnpg-postgresql` |
|
||||
| 정적 링크 바이너리 하나뿐 | `bci-micro` | `apisix-ingress-controller` `cloudnative-pg` `etcd` |
|
||||
| 런타임 트리를 builder 에서 조립 (JVM 등) | `scratch` + micro rootfs 씨앗 | `keycloak` |
|
||||
|
||||
→ 세 조합 밖으로 나가려면 **왜 셋으로 안 되는지 먼저 적는다.** `micro`/`scratch` 는
|
||||
nonroot 계정·`sed`/`grep` 부재를 직접 감당해야 한다(image-authoring.md "어느 BCI 변종").
|
||||
|
||||
**축 2 — 빌더 스테이지** (최종에 남지 않음. 원칙 2 대상 아님 → 공식 언어 이미지 그대로)
|
||||
|
||||
| 언어 | 빌더 | 핀 키 |
|
||||
|---|---|---|
|
||||
| Go | `golang:${GO_BUILDER_TAG}` + `$BUILDPLATFORM`/`TARGETARCH` | `GO_BUILDER_TAG` · `GO_MODULE_UPGRADES` |
|
||||
| Node | `node:*` (런타임은 **OS 패키지** `nodejs24`) | `NODE_BUILDER_TAG` · `NODE_PKG` |
|
||||
| JVM | BCI base — 재컴파일 없이 **취약 jar 만 교체** | `<LIB>_OLD` / `<LIB>_VERSION` 쌍 |
|
||||
| C · Lua | BCI base — **정적 링크 금지**(static glibc 없음) | 컴포넌트별 버전 ARG |
|
||||
|
||||
→ 상세는 image-authoring.md "언어별 빌더 규칙". 세 가지가 언어와 무관하게 공통이다:
|
||||
**버전은 Dockerfile 에 박지 말고 `build.env` 값으로**, `BUILD_ARGS` 에 **반드시 등록**(빠뜨리면
|
||||
조용히 기본값으로 빌드된다), 그리고 **업스트림 런타임 계약(`USER`·`ENTRYPOINT`·파일 레이아웃)은
|
||||
보존**한다.
|
||||
|
||||
## 자동 재빌드 — 실행 계약만
|
||||
|
||||
`build-image.yml` 이 주간(월요일 02:00 UTC)으로 배포 중인 이미지를 스캔해 재빌드가 필요한
|
||||
것을 찾아 빌드·검증·게이트까지 돈다. **트리거 조건과 왜 그 조건인지는
|
||||
[.claude/image-authoring.md](../../image-authoring.md) "핀은 가만히 있어도 뒤처진다" 가
|
||||
단일 출처다** — 여기 복제하지 않는다.
|
||||
|
||||
```sh
|
||||
# 수동으로 같은 것을 돌린다
|
||||
gh workflow run self-build-image --repo <org>/dip-catalog -f mode=drift
|
||||
# 판정만 로컬에서 본다
|
||||
python3 scripts/build/check-rebuild-needed.py --list-refs
|
||||
python3 scripts/build/check-rebuild-needed.py --reports <trivy-reports> [--apply]
|
||||
```
|
||||
|
||||
이 경로는 **레지스트리 push 와 카탈로그 반영을 하지 않는다** — push 는
|
||||
`workflow_dispatch` + `mode=image` 에서만 켜진다.
|
||||
|
||||
## 실측된 함정
|
||||
|
||||
- **`FROM`에 쓰는 `ARG`는 첫 `FROM` 이전(전역 스코프)에 선언한다.** 스테이지 내부에 선언하면
|
||||
그 스테이지 지역 변수가 되어 이후 `FROM`의 이미지명 해석에 쓰이지 않고 빈 이미지명 에러가 난다.
|
||||
- **게이트 PASS는 "동작한다"를 증명하지 않는다.** CVE 스캐너는 런타임 요구사항(오퍼레이터가
|
||||
자신의 파일 레이아웃에 의존하는 것 등)을 전혀 보지 못한다. 배포 검증을 반드시 한다 —
|
||||
절차는 [.claude/deploy-test-procedure.md](../../deploy-test-procedure.md).
|
||||
- **`CoverageProbe`가 `ok`인지 확인한다.** `none`이면 findings 0건이 진짜 0건이 아니라
|
||||
스캐너에 그 배포판 데이터가 없다는 뜻이다(`sbom-cve-gate` skill 참고).
|
||||
- **롤링 태그를 쓰지 않는다.** 같은 앱 버전이라도 베이스 업데이트 결과가 시점마다 달라
|
||||
태그에 빌드일을 포함한다(예: `1.30.0-security-hardened-20260804`).
|
||||
- **핀을 고정한 대가로 핀이 뒤처진다.** 소스를 안 바꿔도 새 CVE 가 공개되면 어제 PASS 였던
|
||||
핀이 오늘 FAIL 이 된다. 위 "자동 재빌드" 가 이것을 잡는다. 조치 전에 그 결과를 먼저 본다:
|
||||
`python3 scripts/build/check-rebuild-needed.py --reports <trivy-reports> [--image <name>] [--apply]`
|
||||
- **단, 같은 날 다시 빌드하면 그 태그가 겹친다.** 노드가 캐시한 옛 digest 가 그대로 쓰여
|
||||
(`imagePullPolicy: IfNotPresent`) 고친 것이 반영되지 않은 채 "안 고쳐졌다" 로 보인다.
|
||||
**검증 대상 워크로드에 `imagePullPolicy: Always` 를 수동으로 걸고 digest 로 확인한다** —
|
||||
절차는 [deploy-test-procedure.md](../../deploy-test-procedure.md) "같은 날 재빌드했다면"
|
||||
이 단일 출처다. **카탈로그 values 에는 넣지 않는다**(같은 문서에 이유).
|
||||
|
||||
## 마무리
|
||||
|
||||
카탈로그 values(`custom-values.yaml`/`dip-values.yaml`) 태그 갱신은
|
||||
`scripts/build/patch-catalog-tag.py`가 한다. 조사·결정 근거는 PR 설명과
|
||||
[MEMORY.md](../../../MEMORY.md)에 남긴다. `images/**`+`manifests/helm/**` 변경은 PR로만 반영한다.
|
||||
Reference in New Issue
Block a user