apisix 카탈로그 CVE 게이트 완전 해소 (차단 165건 → 0건)

etcd(bitnamilegacy 동결 미러) → etcd.enabled=false + 카탈로그 자체 etcd 차트를
externalEtcd 기본값으로 연결. adc·apisix-ingress-controller·apisix(paasup/apisix)
세 이미지는 SUSE BCI 자체 빌드로 교체 — 전부 벤더 등급만으로는 안 보이던
벤더 하향 등급 CVE(NVD 재평가 시 드러남)가 원인이었다.

- images/apisix-ingress-controller: 정적 링크 Go 모듈 취약 버전만 강제 업그레이드
- images/apisix: APISIX-Runtime(WASM·dubbo 등 커스텀 모듈 포함) 전체를 SUSE BCI
  위에서 소스로 재현, keycloak-authz 플러그인 오버레이
- images/adc: 업스트림 빌더 스테이지는 그대로 두고 distroless 최종 베이스만
  SUSE BCI+nodejs24 로 교체

scripts/build/patch-catalog-tag.py 의 TAG_BLOCK 이 점 구분 중첩 경로를 지원하도록
확장(apisix 서브차트 alias 때문에 필요).

세 이미지 모두 게이트 PASS(실효 CRITICAL/HIGH 0/0)와 배포 검증(테스트 클러스터)을
마쳤다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wbsong111
2026-08-12 11:37:32 +09:00
parent bc4002f41c
commit 18044210f6
21 changed files with 1577 additions and 45 deletions
+116
View File
@@ -0,0 +1,116 @@
# apisix-ingress-controller — 자체 빌드
apisix-ingress-controller 바이너리를 업스트림 소스에서 직접 컴파일한다.
`manifests/helm/apisix/{2.14.0,2.16.0}/custom-values.yaml`
`ingress-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-debian12``registry.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.env``SOURCE_COMMIT``2.1.0` 태그가 가리키는 실제 커밋 |
| 빌더 | 공식 `golang` 이미지(`source.build.env``GO_BUILDER_TAG`) — 의존성 업그레이드 후 `go mod tidy``go.mod``go 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 삭제되어(구버전 유지 대신 최신
> 버전 하나로 정리) 지금은 변종이 하나뿐이다.
## 빌드
```sh
# 로컬 빌드 (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.env``TAG_BLOCK`
점 구분 경로(`ingress-controller.deployment.image`)를 쓴다. 기존
`scripts/build/patch-catalog-tag.py` 는 top-level 블록만 지원했어서, 이 이미지를
추가하며 중첩 경로까지 지원하도록 확장했다(첫 세그먼트는 여전히 top-level 강제, 그
뒤부터는 부모 블록 범위 안에서만 찾아 다른 형제 블록의 동명 키를 오인하지 않는다) —
기존 이미지(단일 세그먼트 `image`)의 동작은 회귀 테스트로 그대로임을 확인했다.
@@ -0,0 +1,16 @@
# build-image.yml 이 읽는 카탈로그 반영 메타데이터. 빌드 정의(<variant>.build.env)와는
# 별개다 — 이건 "이 이미지가 어느 차트의 어느 필드를 가리키는가" 만 담는다.
#
# apisix 카탈로그는 2.16.0 하나만 보관한다(2.14.0 은 2026-08-11 삭제 — 구버전 유지 대신
# 최신 버전 하나로 정리).
CHART_DIRS="manifests/helm/apisix/2.16.0"
# 태그 표기 스타일 — imageName(단일 필드 문자열) | split(registry/repository/tag 분리)
TAG_STYLE=split
# apisix 서브차트 alias(ingress-controller) 아래 deployment.image 로 중첩돼 있다 — 점
# 구분 경로는 scripts/build/patch-catalog-tag.py 가 지원한다(2026-08-11, 이 이미지
# 추가하며 top-level 전용이던 걸 중첩 경로까지 확장).
TAG_BLOCK=ingress-controller.deployment.image
# base_os 입력을 안 주면 쓸 기본 변종 (images/<image>/<DEFAULT_BASE_OS>.build.env)
DEFAULT_BASE_OS=source
@@ -0,0 +1,98 @@
# apisix-ingress-controller — 업스트림 소스를 pinned commit(v2.1.0 태그)으로 직접
# 컴파일하고, 취약한 전이 의존성만 강제로 올린다(cloudnative-pg/etcd 자체 빌드와 동일한
# 패턴). 업스트림 루트 Dockerfile 은 이미 빌드된 바이너리를 COPY 만 한다 — 컴파일 자체는
# Makefile 의 `build`/`build-multi-arch` 타겟(`CGO_ENABLED=0 go build ...`)이 한다. 그
# 빌드 단계를 Dockerfile 안으로 가져와 재현한다.
#
# 왜 자체 빌드인가 — apache/apisix-ingress-controller:2.1.0(2026-05-29 빌드, 현재 최신
# 태그)이 게이트에서 차단하는 CRITICAL/HIGH 25건 중 21건이 OS 패키지가 아니라 바이너리에
# 정적 링크된 Go 모듈(stdlib, golang.org/x/net, golang.org/x/text, google.golang.org/grpc,
# go.opentelemetry.io/otel[/sdk])이 원인이다(2026-08-11 실측, apisix 카탈로그 CVE 조치).
# 이미 최신 태그라 상위 태그 교체가 불가능하고, 베이스가 distroless(cc-debian12)라
# libc6/libssl3 OS 패키지 4건만 베이스 교체로 잡히고 나머지는 못 잡는다 — 자체 빌드가
# 유일한 대응이다.
#
# 업스트림과 다르게 하는 부분 — 취약 모듈만 강제 업그레이드(go.work 가 없는 단일 모듈
# 프로젝트라 etcd 처럼 워크스페이스 전역 replace 를 못 쓴다. `go get`+`go mod tidy` 로
# 대신한다). 애플리케이션 코드 자체는 v2.1.0 태그 그대로다 — master 는 릴리스 이후 기능
# 커밋이 다수 섞여 있어(2026-08-11 기준 태그 대비 35커밋 앞섬) 최소 diff 원칙에 맞지 않아
# 쓰지 않았다.
# 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, otel 코어와 버전 동기 필요)
# 이 조합은 `go get` 가 알아서 서로 호환되는 최소 버전으로 끌어올린다(go mod tidy 로 확정) —
# 로컬에서 go build 성공 실측 완료(2026-08-11).
#
# gcr.io/distroless/cc-debian12 → SUSE BCI(bci-micro)로 교체. 카탈로그는 SUSE BCI
# 하나만 쓴다(.claude/image-authoring.md 원칙 2) — builder 스테이지는 공식 golang
# 이미지를 그대로 쓴다. CGO_ENABLED=0 정적 바이너리라 cc(glibc 포함) 변형이 애초에
# 필요 없었다 — 업스트림이 왜 cc 를 쓰는지는 불명(안전 마진으로 추정), static 대비
# 이점이 없어 우리는 bci-micro 로 통일한다.
#
# ldflags — 업스트림 Makefile 의 GO_LDFLAGS 를 그대로 재현한다(internal/version 패키지의
# 빌드타임 심볼 4개). GitSHA 자리에는 pinned commit 전체 해시를 심어 verify.sh 가 컨테이너
# 안에서 git 을 실행하지 않고도 버전 문자열로 확인할 수 있게 한다(etcd/cloudnative-pg 와
# 동일한 이유).
# syntax=docker/dockerfile:1
ARG GO_BUILDER_TAG=1.26.5-trixie
# FROM 에서 쓰는 ARG 는 반드시 첫 FROM 이전(전역 스코프)에 선언해야 한다 — .claude/image-authoring.md.
ARG RUNTIME_BASE=registry.suse.com/bci/bci-micro:15.7
FROM --platform=$BUILDPLATFORM golang:${GO_BUILDER_TAG} AS builder
ARG TARGETARCH
ARG SOURCE_COMMIT
ARG APP_VERSION
ARG MIN_K8S_VERSION
ARG XNET_FIX_VERSION
ARG XTEXT_FIX_VERSION
ARG GRPC_FIX_VERSION
ARG OTEL_FIX_VERSION
WORKDIR /src
# BuildKit 의 git context 지원 — tarball+checksum 관리 없이 git 자체가 커밋 무결성을 보장한다.
ADD https://github.com/apache/apisix-ingress-controller.git#${SOURCE_COMMIT} /src
# 취약 전이 의존성만 최소 버전으로 강제 업그레이드한다(위 설명 참고). otel/otel-sdk 는
# 서로 버전이 맞아야 해서 함께 지정한다 — go get 이 나머지 호환 버전을 알아서 정리한다.
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go get \
golang.org/x/net@v${XNET_FIX_VERSION} \
golang.org/x/text@v${XTEXT_FIX_VERSION} \
google.golang.org/grpc@v${GRPC_FIX_VERSION} \
go.opentelemetry.io/otel@v${OTEL_FIX_VERSION} \
go.opentelemetry.io/otel/sdk@v${OTEL_FIX_VERSION} \
&& go mod tidy
# 업스트림 Makefile 의 build 타겟을 그대로 재현한다(GOARCH 는 TARGETARCH 로 대체, GitSHA
# 자리에 pinned commit 전체 해시를 심는다 — 위 설명 참고).
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
mkdir -p /out; \
VERSYM="github.com/apache/apisix-ingress-controller/internal/version._buildVersion"; \
GITSHASYM="github.com/apache/apisix-ingress-controller/internal/version._buildGitRevision"; \
BUILDOSSYM="github.com/apache/apisix-ingress-controller/internal/version._buildOS"; \
MINK8SVERSYM="github.com/apache/apisix-ingress-controller/internal/manager._minK8sVersion"; \
LDFLAGS="-X=${VERSYM}=${APP_VERSION} -X=${GITSHASYM}=${SOURCE_COMMIT} -X=${BUILDOSSYM}=linux/${TARGETARCH} -X=${MINK8SVERSYM}=${MIN_K8S_VERSION}"; \
CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -ldflags="${LDFLAGS}" -o /out/apisix-ingress-controller cmd/main.go
FROM ${RUNTIME_BASE} AS final
# 업스트림과 동일한 경로 — Helm 차트가 별도 경로를 하드코딩하지 않으므로 필수는 아니지만
# 업스트림 Dockerfile 과의 대응을 그대로 유지한다.
WORKDIR /app
COPY --from=builder /out/apisix-ingress-controller ./apisix-ingress-controller
COPY --from=builder /src/LICENSE /licenses/LICENSE
COPY --from=builder /src/NOTICE /licenses/NOTICE
# 업스트림은 distroless `:nonroot` 태그로 65532:65532 를 기본 사용자로 쓴다 — bci-micro 는
# 그런 태그 변형이 없어 명시적으로 지정한다.
USER 65532:65532
ENTRYPOINT ["/app/apisix-ingress-controller"]
CMD ["-c", "/app/conf/config.yaml"]
@@ -0,0 +1,43 @@
# apisix-ingress-controller — 소스 컴파일형 자체 빌드 (유일한 변종)
#
# build-hardened-image.sh 가 source 한다. BUILD_ARGS 에 나열한 이름만 --build-arg 로 넘어간다.
# 이 이미지는 소스를 직접 컴파일하므로 "베이스 OS" 선택지가 없다 — BASE_OS=source 로 호출한다:
# IMAGE=apisix-ingress-controller BASE_OS=source bash scripts/build/build-hardened-image.sh <OUT_DIR>
#
# 왜 자체 빌드하는가 → source.Dockerfile 상단 주석. apisix 카탈로그 CVE 조치(2026-08-11)
# 로 도입 — 근거·경과는 MEMORY.md.
DOCKERFILE=source.Dockerfile
TARGET=final
TAG_SLUG=security
# build-hardened-image.sh 가 태그·verify.sh 전달용으로 요구하는 범용 필수값.
# 스톡 2.1.0 과는 태그 슬러그(TAG_SLUG=security)로 구분되므로 버전 문자열 자체는 그대로 둔다.
APP_VERSION=2.1.0
# v2.1.0 태그가 가리키는 실제 커밋(peeled commit), 2026-08-11 확인.
# apisix-ingress-controller 는 cloudnative-pg 식 "release-X.Y 브랜치"가 없다(master 하나) —
# master HEAD 는 태그 대비 커밋 35개 앞서 있고 신기능 커밋이 섞여 있어(2026-08-11 확인)
# 최소 diff 원칙(.claude/image-authoring.md)에 맞지 않는다. 태그 그대로 두고 의존성만 올린다.
SOURCE_COMMIT=f4a8dd1223573a5b72e1c7b65b37c13b49d98042
# 업스트림 Makefile 의 MIN_K8S_VERSION 기본값(ldflags 로 바이너리에 심긴다).
MIN_K8S_VERSION=1.26.0
# go.mod 의 `go 1.25`/`toolchain go1.26.5` 요구(의존성 업그레이드 후 go mod tidy 가 자동
# 상향한 값, 2026-08-11 로컬 실측)를 만족하는 공식 golang 이미지 태그. 빌더 스테이지에만
# 쓰이고 최종 이미지에는 남지 않는다.
GO_BUILDER_TAG=1.26.5-trixie
# 최종 런타임 베이스 — 카탈로그는 SUSE BCI 하나만 쓴다(.claude/image-authoring.md 원칙 2).
# 정적 링크 바이너리(CGO_ENABLED=0)라 패키지 매니저가 없는 가장 가벼운 bci-micro 로 충분하다.
RUNTIME_BASE=registry.suse.com/bci/bci-micro:15.7
# 차단 CVE 해소에 필요한 최소 버전(2026-08-11 실측, source.Dockerfile 상단 주석 참고).
# go get 이 서로 호환되는 조합으로 정리한다 — 개별 go.sum 버전을 여기 나열하지 않는다.
XNET_FIX_VERSION=0.56.0
XTEXT_FIX_VERSION=0.39.0
GRPC_FIX_VERSION=1.82.1
OTEL_FIX_VERSION=1.43.0
BUILD_ARGS="SOURCE_COMMIT APP_VERSION MIN_K8S_VERSION GO_BUILDER_TAG RUNTIME_BASE XNET_FIX_VERSION XTEXT_FIX_VERSION GRPC_FIX_VERSION OTEL_FIX_VERSION"
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# apisix-ingress-controller 이미지 기능 검증 — 호스트에서 bash 로 실행된다
# (build-hardened-image.sh 가 `env TAG=... PLATFORM=... SOURCE_COMMIT=... bash verify.sh`
# 형태로 호출한다).
#
# 최종 베이스가 bci-micro 라 bash/coreutils 가 있다(패키지 매니저만 없음 —
# .claude/image-authoring.md). 다만 이 바이너리는 컨트롤러라 실제 기동에는 Kubernetes
# API 서버 접속이 필요하다 — 그건 이 스모크 테스트 범위 밖이고 dev 클러스터 배포 검증
# (.claude/deploy-test-procedure.md)이 담당한다. 여기서는 k8s API 없이 확인 가능한 것만
# 본다: 바이너리 존재, 버전 문자열에 pinned commit 반영, --help 정상 종료, 실행 사용자.
#
# 마지막 줄에 VERIFY-OK 를 출력하면 통과다. build-hardened-image.sh 가 그것으로 판정한다.
set -e
TAG="${TAG:?TAG 환경변수가 필요하다}"
PLATFORM="${PLATFORM:-linux/amd64}"
SOURCE_COMMIT="${SOURCE_COMMIT:?SOURCE_COMMIT 환경변수가 필요하다 (build-hardened-image.sh 가 build.env 에서 전달)}"
echo "== 실행 사용자 (nonroot, 이미지 메타데이터) =="
USER_CFG="$(docker inspect --format '{{.Config.User}}' "$TAG")"
[ "$USER_CFG" = "65532:65532" ] || { echo "FAIL: 이미지 Config.User 가 65532:65532 가 아니다 (실제: $USER_CFG)"; exit 1; }
echo " Config.User=$USER_CFG"
docker run --rm -i --platform "$PLATFORM" -e SOURCE_COMMIT="$SOURCE_COMMIT" --entrypoint sh "$TAG" <<'GUEST'
set -e
echo "== 바이너리 탐색 =="
[ -x /app/apisix-ingress-controller ] || { echo "FAIL: /app/apisix-ingress-controller 실행 파일 없음"; exit 1; }
echo " /app/apisix-ingress-controller 존재·실행 가능"
echo "== 버전 (pinned commit 반영 확인) =="
OUT="$(/app/apisix-ingress-controller version --long)"
while IFS= read -r line; do echo " $line"; done <<EOF
$OUT
EOF
case "$OUT" in
*"$SOURCE_COMMIT"*) ;;
*) echo "FAIL: 버전 출력에 pinned commit($SOURCE_COMMIT) 이 없다 — ldflags 주입 확인 필요"; exit 1 ;;
esac
echo "== --help 스모크 (k8s API 없이 도는 유일한 확인 범위) =="
/app/apisix-ingress-controller --help >/dev/null
echo " --help 종료 코드 0"
echo "VERIFY-OK"
GUEST