자체 빌드 이미지 프레임워크를 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:
wbsong111
2026-08-24 15:21:29 +09:00
parent e948436f53
commit 7746570ec0
87 changed files with 693 additions and 5554 deletions
+4 -1
View File
@@ -9,7 +9,10 @@
" - 예외는 '위험을 수용한다'는 기록이다. 숫자를 지우는 수단으로 쓰면 목표 자체가 무의미해진다.",
"",
"예외를 늘리기 전에 먼저 검토할 것: 상위 태그로 교체 / 베이스 OS 교체 /",
"scripts/build/build-hardened-image.sh 로 자체 빌드. 예외는 마지막 수단이다."
"그래도 안 되면 별도 레포 security-images 에서 자체 빌드. 예외는 마지막 수단이다.",
"자체 빌드 이미지(docker.io/paasup/*)는 그 레포도 같은 이미지를 스캔하므로,",
"그 이미지에 대한 예외는 이 파일과 security-images 레포의 cve-exceptions.json",
"양쪽에 각각 등록해야 두 게이트 모두 통과한다."
],
"exceptions": [
{
@@ -1,63 +0,0 @@
# 0001. cnpg-cluster 의 PostgreSQL 이미지를 SUSE BCI 자체 빌드로 한다
- 날짜: 2026-07-28 (결정) · 2026-08-03 (`docker.io/paasup` 로 재빌드)
- 상태: 확정
- 원본: security-catalog ADR 0001 — dip-catalog 맥락으로 다시 썼다([README](README.md) 참고)
## 결정
`manifests/helm/cnpg-cluster/1.0.0` 의 PostgreSQL 이미지를 SUSE BCI 15.7 기반 자체 빌드로
한다. 현재 태그는 [custom-values.yaml](../../manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml)
`postgresql.imageName` 이 단일 출처이고, 빌드 정의는
[images/cnpg-postgresql/](../../images/cnpg-postgresql/)(`suse.Dockerfile` + `suse.build.env`)다.
## 배경
이전 값 `ghcr.io/cloudnative-pg/postgresql:18.4-system-trixie` 에 두 문제가 있었다.
1. `system` 은 **업스트림에서 deprecated 된 타입**이다 (in-core barman phase out 예정)
2. 실효 CRITICAL/HIGH **6/26**, 게이트 차단 **32건** (2026-07-28 실측)
후보 셋을 비교했다.
| | A `standard-trixie` | B ubuntu:24.04 자체 빌드 | **C SUSE BCI 15.7 자체 빌드** |
| --- | --- | --- | --- |
| 실효 고유 C/H | 23 | 0 | 0 |
| 측정 가능성 | trivy 완전 | trivy 완전 | trivy 완전 |
| 서명·attestation | ✅ | ❌ | ❌ |
| 미조사 사각지대 | 0 | 18건 | 18건 |
| 배포 검증 | — | failover 2초 | failover 3초 |
| gid | `postgres` | ⚠️ `tape` 가 gid 26 선점 | `postgres` |
## 근거
- **A 의 차단 23건은 전부 수정 버전이 없다.** Debian 판정이 `unimportant` 9 / `no-dsa` 7 /
`postponed` 3 / 미분류 4 로, 어떤 조치로도 해소되지 않는다. KEV 교집합은 0건이다.
- **C 는 0건이고, 그 0건이 측정된 0건이다.** 결정 당시에는 "trivy 가 SLES 15.7 을 커버하지
않으므로 직접 평가해야 한다" 고 봤는데 **그 전제가 틀렸다**(2026-07-29 재측정). 깨끗한
이미지도 findings 0 이라 "0건" 과 "데이터 없음" 이 구분되지 않았던 것이 원인이다. 올바른
검사는 양성 대조이고, dip-catalog 는 그것을 `CoverageProbe`**매 스캔마다** 한다
([doc/sbom-pipeline.md](../sbom-pipeline.md)).
- **C 는 B 의 gid 위생 문제가 없다.** B 는 gid 26 이 `tape` 그룹에 선점되어 있다.
- **C 는 배포 검증을 통과했다** — CNPG 오퍼레이터 통합, 롤링 전환, 데이터 보존, failover 3초.
- **SUSE 공식 PostgreSQL 차트는 대안이 아니었다** — 확장이 `plpgsql` 하나뿐이다. C 는
`pgaudit`·`pgvector`·contrib 를 포함한다.
## 받아들인 비용
- **업스트림 서명·provenance·SBOM attestation 을 잃는다.**
- **재빌드 책임을 진다.** PostgreSQL 마이너 릴리스마다, 베이스 보안 업데이트마다. 후보 B 에서
이미 실증됐다 — 빌드 후 하루도 안 되어 glibc 업데이트로 findings 38 → 46.
- **업스트림이 테스트하지 않는 구성이다.** CNPG bake 매트릭스는 Debian 3종뿐이다.
- **`pg-failover-slots` 확장이 없다.** PGDG zypp 저장소에 패키지가 없다.
- **미조사 사각지대 18건.** SUSE 는 "미평가" 를 문서 부재로 표현하므로 **무엇이 미평가인지
열거할 수 없다.** 이것은 어떤 도구를 쓰든 같다(이 이미지 기준 벤더 데이터가 언급하지 않는
패키지 61개 / 176개). `0/0` 의 근거가 그만큼 불완전하다.
## 재검토 조건
- **A 의 차단 23건에 수정 버전이 도착하면** — 업스트림 이미지로 돌아가는 것이 유리해진다.
게이트 리포트의 `status: fixed` 여부로 감지된다.
- **사각지대 18건 조사 결과가 C 에 불리하면.**
- **재빌드 부담이 실제로 문제가 되면** — 분기 1회 이상 재빌드가 필요해지는 경우.
- **SUSE 가 BCI 기반 CNPG 호환 이미지를 직접 발행하면** — 자체 빌드가 불필요해진다.
@@ -1,72 +0,0 @@
# 0002. cloudnative-pg 오퍼레이터를 소스 컴파일 자체 빌드로 대체하고, 자체 빌드 오케스트레이션을 하나로 통일한다
- 날짜: 2026-07-30 (결정) · 2026-08-04 (`docker.io/paasup` 로 재빌드)
- 상태: 확정
- 원본: security-catalog ADR 0005 — dip-catalog 맥락으로 다시 썼다([README](README.md) 참고)
## 결정
`manifests/helm/cloudnative-pg/0.29.0` 의 오퍼레이터 이미지를 자체 빌드로 한다. 현재 태그는
[custom-values.yaml](../../manifests/helm/cloudnative-pg/0.29.0/custom-values.yaml) 의
`image.repository`/`image.tag` 가 단일 출처이고, 빌드 정의는
[images/cloudnative-pg/](../../images/cloudnative-pg/)다 — 업스트림 `release-1.30` 브랜치의
pinned commit 을 직접 `go build` 로 컴파일하고, 최종 런타임 베이스는 SUSE BCI(`bci-micro`)다.
**추가로, 이 작업에서 `scripts/build/build-hardened-image.sh` 를 일반화해 OS 패키지
재설치형(`cnpg-postgresql`)과 소스 컴파일형(`cloudnative-pg`)이 오케스트레이션 스크립트
하나를 공유하게 했다.** 자체 빌드 이미지가 늘어날 때마다 방법론이 갈라지지 않게 하기
위함이다. 계약은 [.claude/image-authoring.md](../../.claude/image-authoring.md) 가 갖는다.
## 배경
업스트림 `cloudnative-pg:1.30.0` 이 게이트에서 실효 HIGH 3건으로 차단됐다 — `CVE-2026-39822`
(stdlib), `CVE-2026-56852`(`golang.org/x/text`), `GHSA-hrxh-6v49-42gf`(grpc).
셋 다 Go 바이너리에 **정적 링크된 모듈 버전**이 원인이라 앞선 두 레버가 통하지 않는다.
- 상위 태그 교체 — 상위 태그가 없다(2026-07-30 확인, 최신 릴리스가 여전히 `v1.30.0`)
- 베이스 OS 교체 — 배포 이미지 베이스가 distroless 라 OS 패키지가 사실상 0개다
자체 빌드만 유효한 대응이었다. 세 CVE 모두 업스트림 `release-1.30` 브랜치에 이미 백포트돼
있어, Go 의존성을 직접 올릴 필요 없이 그 커밋을 그대로 컴파일하면 됐다.
## 근거
- **`release-1.30` HEAD 는 실측으로 확인된 안전한 픽업 지점이다.** `go.mod` 비교로 세 CVE 의
수정 버전이 전부 포함됨을 확인했고(stdlib 1.26.5, x/text 0.39.0, grpc 1.82.1), 재스캔으로
실효 C/H 0/0 을 확인했다.
- **게이트 통과가 "동작함" 을 증명하지 않는다는 원칙이 실제로 작동했다.** 1차 빌드는 게이트를
0/0 으로 통과했지만 배포 검증에서 `Cluster` 리컨실이 `invalid architecture: amd64`
실패했다. 오퍼레이터가 자신의 가용 아키텍처를 `operator/manager_<arch>` **파일 존재**로
판단하는데, 업스트림의 멀티아치 심볼릭 링크를 "부수 장치" 로 오판해 제거했기 때문이다.
같은 바이너리를 `/manager``/operator/manager_amd64` 양쪽에 COPY 해 해결했다.
- **베이스 OS 는 BCI 로 통일한다는 결정([0001](0001-cnpg-postgresql-image.md))이 이 이미지에도
적용된다.** 최초 설계는 "업스트림과 최대한 동일하게" 를 따라 distroless 를 그대로 썼으나
카탈로그 정책과 어긋나 `bci-micro` 로 교체했고, 교체 후에도 게이트·배포 검증 PASS 를
유지하는 것을 확인했다.
- **오케스트레이션 통일이 회귀를 만들지 않는 것을 확인했다** — 스크립트를 일반화한 직후 기존
`cnpg-postgresql` 을 다시 빌드해 태그·게이트 결과가 동일함을 확인했다.
## 받아들인 비용
- **업스트림 서명·provenance·SBOM attestation 을 잃는다.** ADR [0001](0001-cnpg-postgresql-image.md)
과 동일한 비용이다.
- **`SOURCE_COMMIT` 갱신을 사람이 한다.** 자동 추적하지 않는다 — 다음 CVE 발생 시 그 시점의
유지보수 브랜치 최신 커밋을 사람이 골라 `source.build.env` 를 고쳐 PR 을 여는 것 자체가
트리거다.
- **`release-1.30` 은 순수 패치 백포트가 아니다.** 기능 커밋 2개(`feat(pooler)` auth_user,
`feat` check_empty_wal_archive)가 섞여 있다. 59 commits 전체를 라인 단위로 감사하지 않았고,
배포 검증 스모크 수준으로만 회귀 여부를 봤다.
- **업스트림이 테스트하지 않는 구성이다.** BCI 기반 오퍼레이터 빌드는 CNPG 의 CI 매트릭스에 없다.
- **`bci-micro``bci-base` 와 달리 nonroot 계정이 없어 직접 만들어야 한다** — BCI
micro/minimal 계열을 쓰는 다른 자체 빌드 이미지도 같은 작업이 필요하다.
## 재검토 조건
- **업스트림이 `v1.30.1` 이상을 정식 릴리스하면** — 상위 태그 교체가 다시 가능해지므로 자체
빌드보다 우선한다.
- **`release-1.30` 의 기능 커밋 2개가 실제 회귀를 일으키면** — 기능 커밋 이전 시점 등 다른
pinned commit 으로 되돌린다.
- **SUSE 가 CNPG 호환 공식 오퍼레이터 이미지를 발행하면** — 자체 빌드가 불필요해진다.
- **`bci-micro` 에서 예기치 못한 런타임 문제(CA 신뢰 체인, TLS 등)가 나오면** — `bci-base`
승격하거나 다른 BCI 변종을 재검토한다.
+2 -1
View File
@@ -35,7 +35,8 @@ dev 클러스터 `apisix` 네임스페이스에 이미 `data-apisix-etcd-0` PVC
1. 패치가 멈춘 이미지는 시간이 지날수록 미수정 CVE 가 구조적으로 누적된다 —
"CRITICAL/HIGH 0건" 을 유지 가능한 상태로 지속할 수 없다.
2. 버전 고정 없는 `latest`-only 배포는 **롤링 태그 금지** 규칙과 애초에 양립하지 않는다
([.claude/image-authoring.md](../../.claude/image-authoring.md)).
(자체 빌드 이미지에 적용되는 이 규칙은 `security-images` 레포의 `docs/image-authoring.md`
가 갖는다).
`groundhog2k/etcd` 는 업스트림 etcd 프로젝트의 원본 이미지(`quay.io/coreos/etcd`)를 그대로
쓰므로 이 문제가 없다 — 다른 카탈로그 항목과 동일한 "업스트림 원본 이미지 + 버전 고정 태그"
@@ -1,66 +0,0 @@
# 0004. etcd 이미지를 소스 컴파일 자체 빌드로 대체한다
- 날짜: 2026-07-31 (결정) · 2026-08-04 (`docker.io/paasup` 로 재빌드)
- 상태: 확정
- 원본: security-catalog ADR 0007 — dip-catalog 맥락으로 다시 썼다([README](README.md) 참고)
## 결정
[manifests/helm/etcd/1.1.12/](../../manifests/helm/etcd/1.1.12/) 의 이미지를 자체 빌드로
한다. 현재 태그는 `custom-values.yaml``image.registry`/`image.repository`/`image.tag`
단일 출처이고, 빌드 정의는 [images/etcd/](../../images/etcd/)다 — 업스트림 `v3.7.1` 태그가
가리키는 commit 을 그대로 컴파일하되, `go.work` 워크스페이스 전역 `replace`
`golang.org/x/text` 만 강제 업그레이드한다(버전은 `source.build.env``XTEXT_FIX_VERSION`).
최종 런타임 베이스는 SUSE BCI(`bci-micro`) — [0002](0002-cloudnative-pg-operator-self-build.md)
와 같은 패턴이다.
**추가로, 이 작업에서 `build-image.yml` CI 를 이미지 종류 무관하게 일반화했다**
(`images/<image>/catalog.env` + `scripts/build/patch-catalog-tag.py`) — [0002](0002-cloudnative-pg-operator-self-build.md)
의 백로그였다. 자체 빌드 이미지가 둘일 때는 하드코딩으로 버틸 수 있었지만 세 번째가 생기며
실제로 한계에 부딪혔다.
## 배경
업스트림 `quay.io/coreos/etcd:v3.7.1` 이 게이트에서 실효 HIGH 1건으로 차단됐다 —
`CVE-2026-56852`, `golang.org/x/text` 의 깨진 UTF-8 입력에 대한 `norm.Iter` 무한루프 DoS.
Go 바이너리에 정적 링크된 모듈 버전이 원인이라 베이스 OS 교체가 통하지 않고, 상위 태그도
없다(2026-07-31 확인, 최신 릴리스가 여전히 `v3.7.1`). 게다가 [0002](0002-cloudnative-pg-operator-self-build.md)
와 달리 **`release-3.7` 브랜치에 이 수정이 백포트조차 안 돼 있다**(브랜치 HEAD 가 태그와
완전히 동일). 자체 빌드 외에는 즉시 대응할 방법이 없었다.
## 근거
- **워크스페이스 전역 `replace` 한 줄로 게이트가 PASS 로 바뀌는 것을 실측했다** — 실효 C/H
0/1 → **0/0**. 이미 백포트된 커밋을 그대로 가져다 쓸 수 없었지만, 취약 모듈이 이 CVE
하나뿐이라 직접 강제 업그레이드로도 충분했다.
- **기능 검증(단일 노드 기동 + `etcdctl` put/get 왕복)이 통과했다.** 게이트 PASS 가 "동작함"
을 증명하지 않으므로 `images/etcd/verify.sh` 로 확인한다. 최초 시도는 최종 베이스에 `sed`
가 없어 실패했다(`sh: sed: command not found`) — `bci-base` 에는 있지만 `bci-micro` 에는
없다는 것을 이때 처음 확인했다. 순수 셸 루프로 교체해 해결하고 그 제약을
[.claude/image-authoring.md](../../.claude/image-authoring.md) 에 기록했다.
- **CI 일반화가 기존 이미지에 회귀를 만들지 않는 것을 확인했다**`catalog.env` 를 세
이미지에 추가한 뒤 매트릭스로 동시에 검증 빌드했고 기존 두 이미지도 동일하게 PASS 했다.
- **태그 치환 로직을 실제 카탈로그 파일 사본에 돌려 검증했다**`imageName` 단일 필드
(cnpg-cluster)와 `image:` 분리 필드(cloudnative-pg, etcd) 두 스타일 모두 기존 주석·포매팅을
보존한 채 정확히 치환됨을 diff 로 확인했다.
## 받아들인 비용
- **업스트림 서명·provenance·SBOM attestation 을 잃는다.** [0001](0001-cnpg-postgresql-image.md)
·[0002](0002-cloudnative-pg-operator-self-build.md) 와 동일한 비용이다.
- **`SOURCE_COMMIT`·`XTEXT_FIX_VERSION` 갱신을 사람이 한다.** 자동 추적하지 않는다.
- **매 patch 릴리스마다 이 자체 빌드를 다시 판단해야 한다** — 업스트림이 이 CVE 를
`release-3.7` 에 백포트하기 전까지는 "이미 백포트된 안전한 픽업 지점" 이 없어 재검토 부담이
[0002](0002-cloudnative-pg-operator-self-build.md) 보다 크다.
- **업스트림이 테스트하지 않는 구성이다.** BCI 기반 etcd 빌드는 etcd 의 CI 매트릭스에 없다.
- **강제 업그레이드가 `server`/`etcdctl`/`etcdutl` 각각에 반영됐는지는 SBOM 스캔으로만 간접
확인했다** — `go version -m` 직접 대조는 하지 않았다.
## 재검토 조건
- **etcd 가 `release-3.7` 에 이 CVE 를 백포트하고 `v3.7.2` 이상을 릴리스하면** — 상위 태그
교체가 다시 가능해지므로 자체 빌드보다 우선한다.
- **`bci-micro` 에서 예기치 못한 런타임 문제가 나오면** — `bci-base` 로 승격하거나 다른 BCI
변종을 재검토한다([0002](0002-cloudnative-pg-operator-self-build.md) 와 동일한 조건).
- **다중 노드(쿼럼) 배포 검증에서 이 자체 빌드 이미지에 문제가 발견되면.**
+13 -22
View File
@@ -1,6 +1,6 @@
# 결정 기록 (ADR)
카탈로그의 이미지·차트 선택 중 **재측정으로 복원되지 않는 것**만 여기 둔다.
카탈로그의 차트 선택 중 **재측정으로 복원되지 않는 것**만 여기 둔다.
CVE 건수·커버리지·패키지 버전 같은 수치는 게이트가 매 실행마다 다시 재므로 문서로 남기지
않는다([doc/sbom-pipeline.md](../sbom-pipeline.md)). 반면 "왜 이 후보를 골랐고 무엇을 비용으로
@@ -8,31 +8,22 @@ CVE 건수·커버리지·패키지 버전 같은 수치는 게이트가 매 실
| # | 결정 | 날짜 | 상태 |
|---|---|---|---|
| [0001](0001-cnpg-postgresql-image.md) | cnpg-cluster 의 PostgreSQL 이미지를 SUSE BCI 자체 빌드로 한다 | 2026-07-28 | 확정 |
| [0002](0002-cloudnative-pg-operator-self-build.md) | cloudnative-pg 오퍼레이터를 소스 컴파일 자체 빌드로 대체한다 | 2026-07-30 | 확정 |
| [0003](0003-etcd-chart-selection.md) | etcd 는 `groundhog2k/etcd` 단일 차트 · replicas=1 로 배포한다 | 2026-07-31 | 확정 |
| [0004](0004-etcd-image-self-build.md) | etcd 이미지를 소스 컴파일 자체 빌드로 대체한다 | 2026-07-31 | 확정 |
**이미지 자체 빌드 ADR(0001·0002·0004)은 별도 레포 `security-images` 로 이관됐다** —
그 이미지들의 빌드 정의와 함께 그 레포의 `docs/decisions/` 에 있다. 카탈로그가 무엇을
배포하는가(차트)와 그 이미지를 어떻게 만드는가(자체 빌드)가 다른 레포에 놓이며 근거도
함께 옮긴 것이다. 이관 배경은
[doc/migrations/self-build-images-to-security-images.md](../migrations/self-build-images-to-security-images.md).
번호가 0003 하나만 남아 이어지지 않아 보이지만, 다른 세 번호가 예약돼 있던 것일 뿐이고
새 ADR 은 0005 부터 잇는다.
## 이 문서들의 출처
결정은 **security-catalog 프로젝트에서 내려졌**, 그 레포의 ADR 0001·0005·0006·0007 이
원본이다. dip-catalog 의 이미지·차트가 그 결정의 산물이므로 이 레포에도 근거가 있어야 한다.
**원문을 복사하지 않고 다시 썼다.** 원본은 같은 레포의 다른 문서(`doc/analysis/*`,
`doc/plans/*`)를 근거로 인용하는데, 그것들까지 연쇄로 가져오면 dip-catalog 에 이중 기록이
생기고 그러고도 링크가 또 밖으로 나간다. 그래서 **결론과 근거만 추려 자립적으로** 옮겼다 —
이 디렉토리의 문서는 레포 밖을 가리키는 링크가 없다.
이관하지 않은 것과 그 이유. 아래 경로는 **security-catalog 안의 경로이고 이 레포에는 없다**
링크로 걸지 않은 이유가 그것이다.
| security-catalog 의 원본 | 왜 안 가져왔나 |
|---|---|
| `analysis/sles-oval-measurement.md` | 스캐너 커버리지 실측. 원문 스스로 "재측정하면 갱신된다" 고 밝히는 스냅샷이고, dip-catalog 는 이것을 `CoverageProbe` 로 **매 스캔마다 자동 재측정**한다 |
| `analysis/*-cve.md` (3종) | 착수 시점의 CVE 스냅샷. 결론은 각 ADR 과 `images/<image>/README.md` 에 들어 있고, 현재 수치는 게이트가 낸다 |
| `cve-zero-pipeline.md` · `architecture/build-pipeline.md` | [doc/sbom-pipeline.md](../sbom-pipeline.md) 와 [.claude/image-authoring.md](../../.claude/image-authoring.md) 가 dip-catalog 의 단일 출처다 |
| `image-selection.md` | 이미지 선정 규칙은 [.claude/image-authoring.md](../../.claude/image-authoring.md) 가 갖는다 |
| `charts/*/deploy-test.md` | 배포 검증은 `scripts/deploy-test/*.sh` 가 실행하고 절차는 [.claude/deploy-test-procedure.md](../../.claude/deploy-test-procedure.md) 가 갖는다. 기록은 실행 산출물이라 문서로 고정하지 않는다 |
이 카탈로그의 결정은 원래 **security-catalog 프로젝트에서 내려졌다.** 결정 당시의 원본
분석·계획 문서(`doc/analysis/*`, `doc/plans/*`)까지 연쇄로 가져오면 이중 기록이 생기므로
**결론과 근거만 추려 자립적으로** 옮겼다 — 이 디렉토리의 문서는 레포 밖을 가리키는 링크가
없다.
## 새 ADR 을 언제 쓰나
@@ -0,0 +1,95 @@
# 자체 빌드 하드닝 이미지 → `security-images` 레포 이관
이 문서는 자체 빌드 이미지 프레임워크·이미지 정의를 별도 public 레포
`security-images` 로 분리한 작업의 기록이다. 무엇이 어디로 갔고, 두 레포가 지금
무엇으로 계약하는지, 아직 안 된 것이 무엇인지를 담는다.
## 배경
이 카탈로그는 두 서로 다른 성격의 축을 한 레포에 담고 있었다.
- **차트 카탈로그 축** — "무엇을 배포 중인가" (`manifests/helm/`, warn-only 게이트)
- **자체 빌드 축** — "그 이미지를 어떻게 만드는가" (`images/`, 강제 게이트)
`security-images` 가 public 이 될 예정이라 세 조건이 붙었다: **외부 의존성 없이
단독 동작**(클론 + `docker` + `trivy` 만으로 빌드·게이트 완결), **슬림 게이트**(이미지
판정에 불필요한 차트 축 로직 제거), **산업화**(내부 호스트명·개인 계정·이슈/PR 번호
제거). 이 조건들이 원래 `.claude/image-authoring.md` 가 적어 두었던 "레포 분리 후
무엇이 끊기는가" 계획을 여러 지점에서 다시 설계하게 만들었다 — 아래 "원래 계획과
달라진 점"이 그 차이다.
## 무엇이 옮겨갔는가
| 카탈로그(이 레포)에 있던 것 | `security-images` 로 |
|---|---|
| `images/<image>/` 8개 | 그대로(경로 보존) |
| `scripts/build/build-hardened-image.sh` | 그대로(경로 보존) |
| `scripts/build/suggest-go-upgrades.py` | `--apply`/`--dry-run` 신규 추가 |
| `scripts/pipeline/scan-sbom.sh` · `cve-gate.py` (이미지 판정 부분만) | `scripts/gate/scan-image.sh` · `image-gate.py` (슬림화) |
| `.github/workflows/build-image.yml` | 카탈로그 무의존으로 재구성(`catalog.env` 의존 제거, drift 모드 제거) |
| `.claude/image-authoring.md` | `docs/image-authoring.md` (정책 중심으로 재작성) |
| `doc/decisions/0001·0002·0004`(이미지 ADR) | `docs/decisions/`(같은 번호 유지) |
| `.claude/skills/self-build-image/` | 그대로(문서 경로만 수정) |
## 무엇이 남았는가 (이 레포)
카탈로그가 "무엇을 배포 중인가"를 알아야 하는 부분만 남았다 — 전부 새로 만든 것이다.
| 경로 | 역할 |
|---|---|
| `catalog/image-map/<image>.env` | 자체 빌드 이미지 → 어느 차트의 어느 필드(`CHART_DIRS`·`TAG_STYLE`·`TAG_BLOCK`). 옛 `images/<image>/catalog.env` 의 카탈로그 레이아웃 정보만 뗀 것 |
| `scripts/build/check-rebuild-needed.py` | 드리프트 탐지(A 파트만) — 배포 중인 이미지가 새 CVE 로 규정을 벗어났는가. 핀 판단(B 파트)은 제거했다 — `security-images``suggest-go-upgrades.py` 가 갖는다 |
| `scripts/build/apply-published-tags.py` | `security-images``published.json` 을 카탈로그 values 에 반영 |
| `scripts/build/patch-catalog-tag.py` | 변경 없음(원래도 카탈로그 파일만 다루는 순수 도구였다) |
| `.github/workflows/self-build-drift-check.yml` | 주간, 배포 중인 이미지 스캔 → 재빌드 필요하면 `security-images``build-image.yml``workflow_dispatch` 로 트리거만 |
| `.github/workflows/catalog-tag-update.yml` | 매일, `published.json` 조회 → 카탈로그 values 패치 → 브랜치 push |
## 계약 — `published.json` 하나뿐
`security-images`**이 카탈로그를 모른다.** 게이트 PASS + 레지스트리 push 가 실제로
일어났을 때만 자기 레포의 `published.json` 을 갱신한다:
```json
{"schemaVersion": 1, "images": {"<image>": {"ref": "...", "tag": "...", "digest": "...", "gate": "pass"}}}
```
`catalog-tag-update.yml` 이 이 파일을 **public raw URL** 로 읽어간다 — 인증도, 그
레포의 시크릿도 필요 없다. 의존 방향은 **카탈로그 → 이미지 단방향**이다.
`repository_dispatch` 같은 역방향 알림은 쓰지 않는다 — 그러려면 `security-images`
시크릿에 이 카탈로그 쓰기 권한 PAT 을 둬야 하는데, 그건 그 레포의 "외부 의존성 없이
단독 동작" 원칙과 충돌한다.
## 원래 계획과 달라진 점
`.claude/image-authoring.md`(삭제됨, 원문은 `security-images` 의 git 히스토리에)의
"레포 분리 후 무엇이 끊기는가" 는 게이트를 **composite action** 으로 공개해 두 축이
`uses:` 로 공유하는 안을 제시했다. 실제로는 그렇게 하지 않았다 — 두 가지 이유다.
1. **`suggest-go-upgrades.py``cve-gate.py``importlib` 로 모듈 로드했다.**
composite action 은 워크플로 스텝만 공유하고 파이썬 모듈 임포트를 공유하지 못한다.
`effective_severity` 함수를 게이트의 "정식 소유"로 옮기고(이 레포의 `cve-gate.py` ·
`security-images``image-gate.py` 양쪽에 독립적으로), 두 게이트가 완전히 갈라지는
쪽을 택했다.
2. **게이트 규칙 분기를 허용하기로 했다.** 차트 축은 warn-only, 자체 빌드 축은 강제라
강제력부터 다르다. 공유 action 대신 각자 자기 게이트를 소유하고, `max(벤더, NVD)`
계산과 예외 파일 스키마만 같게 유지하기로 했다(승인 예외는 카탈로그 자체 빌드
이미지에도 별도로 등록해야 한다 — `doc/cve-exceptions.json``_readme` 참고).
카탈로그 반영도 원래 계획(`patch-catalog-tag.py` 를 카탈로그 쪽에서 그대로 재사용)은
맞았지만, **트리거 방식**이 달라졌다 — `repository_dispatch` 가 아니라 `published.json`
을 카탈로그가 주기적으로 읽어가는 pull 방식이다. 위 "계약" 절 참고.
## 아직 안 된 것
- **`SECURITY_IMAGES_DISPATCH_TOKEN` 시크릿 미등록.** `self-build-drift-check.yml`
`security-images``build-image.yml``workflow_dispatch` 로 부르려면
그 레포에 `workflow_dispatch` 권한이 있는 PAT 이 필요하다. 등록 전까지 트리거 스텝은
명시적으로 실패한다.
- **`security-images` 레포 자체가 아직 GitHub 에 없다.** 로컬 레포만 있는 상태에서
이 이관을 진행했다 — GitHub 레포 생성·push, `DOCKERHUB_USER`/`DOCKERHUB_TOKEN` 시크릿
등록이 선행돼야 위 두 워크플로가 실제로 동작한다.
- **`published.json``digest` 필드 대부분 공란.** `security-images` 가 아직 실제로
이미지를 재빌드·push 하지 않아서다. 다음 빌드가 채운다 — MEMORY.md 의 "카탈로그 태그가
클러스터보다 앞서 있다" 항목이 이 필드를 쓸 계획이다.
- **`manifests/applicationset/**``catalog/image-map/` 대상이 아니다.** 옛
`catalog.env` 도 그랬다 — MEMORY.md #42(고착된 낡은 차단 태그)와 같은 공백이다.
+15 -12
View File
@@ -11,8 +11,9 @@ GitHub Actions([.github/workflows/sbom.yml](../.github/workflows/sbom.yml))로
| | 다룬다 — **차트 카탈로그 축** | 다루지 않는다 — **자체 빌드 축** |
|---|---|---|
| 질문 | 우리가 배포하는 이미지에 무엇이 있는가 | 그 이미지를 어떻게 만드는가 |
| 워크플로 | `sbom.yml` · `cve-edge-post.yml` | `build-image.yml` |
| 단일 출처 | **이 문서** | [.claude/image-authoring.md](../.claude/image-authoring.md) |
| 실행 위치 | 이 레포 | 별도 레포 `security-images` |
| 워크플로 | `sbom.yml` · `cve-edge-post.yml` | `security-images``build-image.yml`(이 레포에서는 `self-build-drift-check.yml` 이 필요할 때 그것을 부른다) |
| 단일 출처 | **이 문서** | `security-images` 레포의 `docs/image-authoring.md` |
**카탈로그 values 가 자체 빌드 이미지(`docker.io/paasup/*`)를 가리키므로 그 이미지도 이 문서의
스캔 대상이다** — 축이 갈린 것은 "누가 만들고 결정하는가" 이고 "누가 스캔되는가" 가 아니다.
@@ -136,8 +137,8 @@ docker buildx build --platform linux/amd64 \
> python 으로 갖고 있어 **승인 예외(`cve-exceptions.json`)도 실효 등급(`max(벤더,NVD)`)도
> 적용하지 않는다.** 같은 스캔 데이터에서 다른 숫자가 나올 수 있다.
**PR 을 실제로 막는 게이트는 `images/**` 를 건드린 PR 에만 있다** — 이 축의 `sbom.yml`
warn-only 다.
**PR 을 실제로 막는 게이트는 이 축에 없다** — `sbom.yml` warn-only 다. 강제 게이트는
자체 빌드 축(`security-images` 레포의 `images/**` PR)에만 있다.
> **`manifests/applicationset/**` 를 스캔하지 않는 것은 의도다.** 그 아래 `dip-values.yaml`
> dip-console 이 배포 values 를 만들 때 쓰는 **참조 파일**이고, 같은 이미지를 `manifests/helm/`
@@ -146,7 +147,8 @@ warn-only 다.
>
> 단, 그 가정은 **"두 곳이 같은 이미지를 가리킨다"** 에 의존한다. 참조 파일이 차트와 다른
> 태그를 들고 있으면 스캔한 것과 배포되는 것이 갈린다 — 태그 동기화는 스캔 커버리지와 별개
> 문제이고 `scripts/build/patch-catalog-tag.py``CHART_DIRS` 범위가 그것을 결정한다.
> 문제이고, 자체 빌드 이미지에 대해서는 `catalog/image-map/<image>.env``CHART_DIRS`
> 그 범위를 결정한다(`manifests/applicationset/**` 는 포함하지 않는다 — MEMORY.md #42 참고).
### `sbom.yml` 스캔 범위
@@ -193,7 +195,8 @@ Repo Secret `CVE_API_KEY`).
**`sbom.yml` 에서는 아직 `--warn-only` 다** — 게이트가 실패해도 워크플로/PR 을 막지 않는다. 카탈로그
차트 전체가 아직 이 게이트로 트리아지된 적이 없어, 강제 전환 전에 먼저 전체 스캔 1회로 현황을
파악해야 한다(`MEMORY.md`). `build-image.yml` 의 게이트는 이미 강제다.
파악해야 한다(`MEMORY.md`). 자체 빌드 축(`security-images` 레포의 `build-image.yml`)의
게이트는 이미 강제다 — 단, 그 게이트는 별도 레포가 소유한다.
> **커버리지 자가진단(`CoverageProbe`).** `scan-sbom.sh` 는 os-pkgs findings 가 0건인
> 이미지의 SBOM 사본에 배포판별(deb/rpm/apk) 센티널 패키지를 주입해 재스캔하고, 발화
@@ -205,15 +208,15 @@ Repo Secret `CVE_API_KEY`).
## 자체 빌드 축은 이 문서가 다루지 않는다
게이트가 상위 태그 교체·베이스 OS 교체로 해소되지 않는 차단 CVE 를 찾으면 자체 빌드로 간다.
그 축의 단일 출처는 **[.claude/image-authoring.md](../.claude/image-authoring.md)** 다 —
`build-image.yml` 의 트리거·게이트 강도·카탈로그 반영·레포 분리 계획이 전부 거기 있다.
그 축**별도 레포 `security-images`** 가 갖는다 — 빌드·검증·게이트·push 전부 그 레포
안에서 이루어지고, 단일 출처는 그 레포의 `docs/image-authoring.md` 다. 이 카탈로그에는
"어느 차트가 그 이미지를 가리키는가"(`catalog/image-map/`)와 드리프트 탐지
(`scripts/build/check-rebuild-needed.py`, 주간 `self-build-drift-check.yml`)만 남아 있다 —
이관 배경은 [doc/migrations/](migrations/self-build-images-to-security-images.md).
이 문서가 알아야 할 것은 하나뿐이다: **자체 빌드 이미지도 카탈로그가 가리키는 한 위 스캔·게이트
대상이다.** 실제로 `docker.io/paasup/*` 가 게이트 리포트에 등장한다.
> `build-image.yml``sbom.yml` 과 별도 파일인 이유: `sbom.yml``vars.SBOM_PIPELINE_IMAGE`
> 컨테이너 안에서 도는데 거기엔 docker/buildx 가 없다. 빌드는 호스트 러너여야 한다.
## GitHub 설정 (워크플로 활성화에 필요)
`Settings → Secrets and variables → Actions`
@@ -221,7 +224,7 @@ Repo Secret `CVE_API_KEY`).
| 종류 | 이름 | 용도 |
|------|------|------|
| **Variable** | `SBOM_PIPELINE_IMAGE` | 실행 이미지 태그 (예: `docker.io/paasup/sbom-pipeline:20260820`) |
| Secret | `DOCKERHUB_USER` / `DOCKERHUB_TOKEN` | **docker.io + docker.getcollate.io rate limit 회피**. getcollate(openmetadata)는 Docker Hub 프록시라 익명 pull 시 rate limit(TOOMANYREQUESTS)에 걸림 → Docker Hub 자격증명으로 인증. `build-image.yml` 의 push 에도 같은 시크릿을 쓴다 |
| Secret | `DOCKERHUB_USER` / `DOCKERHUB_TOKEN` | **docker.io + docker.getcollate.io rate limit 회피**. getcollate(openmetadata)는 Docker Hub 프록시라 익명 pull 시 rate limit(TOOMANYREQUESTS)에 걸림 → Docker Hub 자격증명으로 인증. `security-images` 레포도 자체 push 용으로 별도 등록된 같은 이름의 시크릿을 쓴다(이 레포와는 무관하게 그 레포에 따로 등록) |
| Secret | `NGC_API_KEY` | **nvcr.io 인증**(NVIDIA nemo/nim — 없으면 pull 불가) |
| Secret | `CVE_API_KEY` | `cve-edge-post.yml` 의 외부 엔드포인트 인증 |