Files
service-catalog/images/apisix-ingress-controller
wbsong111 a9720af69b 자체 빌드 Go 이미지 3종의 툴체인을 1.26.6 으로 올린다 (#35) (#36)
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>
2026-08-20 11:37:25 +09:00
..

apisix-ingress-controller — 자체 빌드

apisix-ingress-controller 바이너리를 업스트림 소스에서 직접 컴파일한다. manifests/helm/apisix/{2.14.0,2.16.0}/custom-values.yamlingress-controller.deployment.image.repository/tag 가 이 산출물을 가리킨다.

자체 빌드는 대응 우선순위 3번이다. 상위 태그 교체·베이스 OS 교체로 목표를 만족할 수 있으면 그 쪽을 쓴다. 자체 빌드는 업스트림 서명·provenance·SBOM attestation 을 잃고 재빌드 책임을 지는 선택이다.

왜 자체 빌드하나

apisix 카탈로그 CVE 조치(2026-08-11) 중 발견 — apache/apisix-ingress-controller:2.1.0 (2026-05-29 빌드, 이 시점 기준 최신 태그)가 게이트에서 차단하는 CRITICAL/HIGH 25건 중 21건이 OS 패키지가 아니라 바이너리에 정적 링크된 Go 모듈 버전이 원인이다:

모듈 설치 버전 필요 버전 관련 CVE(대표)
stdlib (Go 툴체인) go1.24.7 go1.25.12/1.26.5+ CVE-2026-39822 외 다수
golang.org/x/net v0.47.0 v0.56.0 CVE-2026-25681, 27136, 39821
golang.org/x/text v0.31.0 v0.39.0 CVE-2026-56852
google.golang.org/grpc v1.71.1 v1.82.1 CVE-2026-33186, GHSA-hrxh-6v49-42gf
go.opentelemetry.io/otel v1.40.0 v1.43.0 CVE-2026-29181
go.opentelemetry.io/otel/sdk v1.40.0 v1.43.0 CVE-2026-39883

이미 최신 태그라 상위 태그 교체가 불가능하다. 나머지 4건(libc6 3건, libssl3 1건)은 업스트림 베이스(gcr.io/distroless/cc-debian12)의 OS 패키지지만, 그 4건만 잡자고 베이스를 바꿔도 위 21건은 그대로 남는다 — 자체 빌드(소스 재컴파일)가 유일한 대응이다.

업스트림과 다르게 하는 부분

애플리케이션 코드는 v2.1.0 태그 그대로다. cloudnative-pg 처럼 "release 브랜치 HEAD 로 옮겨서 백포트된 수정을 받는" 방식을 먼저 검토했지만, 이 프로젝트는 그런 유지보수 브랜치가 없다 — master 하나뿐이고, 2026-08-11 확인 시점 master HEAD 는 v2.1.0 태그 대비 커밋 35개 앞서 있으며 그 사이에 Gateway API 1.6.0 지원·L4RoutePolicy·mTLS 지원 같은 신규 기능 커밋이 다수 섞여 있다. 최소 diff 원칙(.claude/image-authoring.md)에 맞지 않아 쓰지 않았다.

대신 위 표의 취약 모듈만 go get(+go mod tidy)로 최소 호환 버전까지 끌어올린다. go.work 워크스페이스가 없는 단일 모듈 프로젝트라 etcd 처럼 전역 replace 한 줄로 끝나지 않고 여러 모듈을 한 번에 지정해야 하지만(otel/otel-sdk 는 버전이 서로 맞아야 해서 반드시 함께 지정), 로컬에서 go mod tidy + go build(linux/amd64, CGO_ENABLED=0)가 정상 종료하는 것을 실측 확인했다(2026-08-11) — 의존성 그래프가 서로 충돌하지 않는다.

gcr.io/distroless/cc-debian12registry.suse.com/bci/bci-micro:15.7(카탈로그는 SUSE BCI 하나만 쓴다 — .claude/image-authoring.md 원칙 2). 바이너리가 CGO_ENABLED=0 정적 링크라 애초에 cc(glibc 포함) 변형이 필요 없었다 — 업스트림이 왜 cc 를 쓰는지는 불명 (안전 마진으로 추정), static 대비 이점이 없어 우리는 bci-micro 로 통일한다.

소스·버전 관리

항목
소스 https://github.com/apache/apisix-ingress-controller.git
pinned commit source.build.envSOURCE_COMMIT2.1.0 태그가 가리키는 실제 커밋
빌더 공식 golang 이미지(source.build.envGO_BUILDER_TAG) — 의존성 업그레이드 후 go mod tidygo.modgo 1.25/toolchain go1.26.5 로 자동 상향했다(2026-08-11 실측)
최종 베이스 registry.suse.com/bci/bci-micro:15.7

SOURCE_COMMIT·의존성 최소 버전은 자동 추적하지 않는다. 다른 자체 빌드 이미지와 동일하게, 사람이 업스트림 새 태그(또는 이 CVE 세트를 이미 해소한 커밋)를 보고 source.build.env 를 고쳐 PR 을 여는 것 자체가 갱신 트리거다.

권장 점검 주기: 카탈로그 게이트가 이 이미지의 차단 CVE 를 다시 보고할 때, 또는 업스트림이 2.1.1(혹은 그 이상, 위 표의 CVE 를 이미 해소한 릴리스)을 내놓았을 때 — 릴리스가 나오면 그쪽으로 갈아타는 것(대응 우선순위 a)이 이 자체 빌드를 유지하는 것보다 항상 우선이다.

apisix 카탈로그는 한때 2.14.0/2.16.0 두 버전을 함께 보관해 apisix-ingress-controller 앱 버전(2.0.1/2.1.0)도 갈렸었다 — 2.14.0 은 2026-08-11 삭제되어(구버전 유지 대신 최신 버전 하나로 정리) 지금은 변종이 하나뿐이다.

빌드

# 로컬 빌드 (push 없음)
IMAGE=apisix-ingress-controller BASE_OS=source bash scripts/build/build-hardened-image.sh /tmp/out

# 레지스트리에 push 까지
IMAGE=apisix-ingress-controller BASE_OS=source REGISTRY=docker.io/paasup \
  bash scripts/build/build-hardened-image.sh /tmp/out

수행 순서: 빌드 → 기능 검증(verify.sh) → SBOM → 전 심각도 스캔 → 게이트 판정. verify.sh 는 바이너리가 실행 가능한지, version --long 출력에 pinned commit 이 실제로 반영됐는지(ldflags 주입 확인), --help 가 정상 종료하는지, 이미지가 65532:65532 (nonroot) 로 실행되는지를 확인한다 — 이 컨트롤러는 Kubernetes API 서버 접속이 있어야 실제로 기동한다, 그 부분은 이 스모크 테스트 범위 밖이며 dev 클러스터 배포 테스트 (.claude/deploy-test-procedure.md)가 담당한다. 이 세션에서는 클러스터가 없어 배포 검증을 아직 하지 못했다 — 카탈로그 반영 전 반드시 수행할 것.

파일 구성

파일 역할
source.Dockerfile 빌드 정의 — 소스 컴파일(builder 스테이지) + SUSE BCI(bci-micro) 패키징(final 스테이지)
source.build.env pinned commit·버전·빌더 이미지 태그·취약 모듈 최소 버전. BUILD_ARGS 에 나열한 이름만 --build-arg 로 전달된다
verify.sh 기능 검증. 호스트에서 bash 로 실행되며 docker run --entrypoint sh 로 게스트 셸 스크립트를 주입한다(bci-micro 는 bash·coreutils 가 있다)

베이스 변종이 하나뿐이라 파일명이 source.* 로 고정돼 있다.

태그

docker.io/paasup/apisix-ingress-controller:2.1.0-security-hardened-20260811
                                            └ app ┘└ 슬러그 ┘└ 하드닝 ┘└ 빌드일 ┘

카탈로그 반영

ingress-controller.deployment.image 가 apisix 서브차트 alias 아래 중첩돼 있어 (ingress-controller:deployment:image:), catalog.envTAG_BLOCK 은 점 구분 경로(ingress-controller.deployment.image)를 쓴다. 기존 scripts/build/patch-catalog-tag.py 는 top-level 블록만 지원했어서, 이 이미지를 추가하며 중첩 경로까지 지원하도록 확장했다(첫 세그먼트는 여전히 top-level 강제, 그 뒤부터는 부모 블록 범위 안에서만 찾아 다른 형제 블록의 동명 키를 오인하지 않는다) — 기존 이미지(단일 세그먼트 image)의 동작은 회귀 테스트로 그대로임을 확인했다.