argocd: 자체 빌드 하드닝 이미지 추가, 게이트 차단 0건
업스트림 quay.io/argoproj/argocd 의 차단 CVE 는 대부분 OS 패키지가 아니라 바이너리에
정적 링크된 Go 모듈이라, 상위 태그 교체(이미 최신)로도 베이스 OS 교체로도 잡히지 않는다.
부분 조치는 효과가 없었다 — 같은 stdlib·x/crypto 취약점이 다섯 바이너리(argocd·helm·
kustomize·git-lfs·pebble)에 각각 들어있어 본체만 고치면 거의 줄지 않는다. 업스트림
Dockerfile 이 helm/kustomize/git-lfs 를 hack/install.sh 로 **미리 빌드된 릴리스 바이너리**
로 내려받기 때문에 그 바이너리의 Go 버전을 우리가 통제할 수도 없다. 그래서 넷 다 소스에서
다시 컴파일한다.
- images/argocd/ 신규 (Go 4개 + C 2개 + Node UI — 기존 자체 빌드 중 가장 크다)
- 최종 베이스 ubuntu → SUSE BCI(원칙 2). ubuntu 가 딸려오던 pebble 도 함께 사라진다
- tini·connect-proxy 는 SLE_BCI 에 없어 소스 빌드 — 기능을 빼지 않기 위해
- UI 는 업스트림과 동일하게 node 로 빌드(Go embed 라 생략 불가)
- manifests/helm/argo-cd/10.4.0/custom-values.yaml 이 이 이미지를 가리킨다
global.image 하나로 argocd 바이너리를 쓰는 5개 컴포넌트가 공유한다
재사용 설계 — Dockerfile 에 버전을 박지 않는다
---------------------------------------------
이 이미지는 앞으로도 CVE 조치를 반복해서 받는다. 모듈 목록·패키지 목록을 전부 값으로 빼서
다음 조치에서 바뀌는 것이 source.build.env 한 곳뿐이게 했다. 이전 방식이라면 모듈 하나
추가에 build.env·ARG 선언·go get 목록·BUILD_ARGS 네 곳을 고쳐야 했다.
- scripts/build/suggest-go-upgrades.py 신규 — 스캔 리포트의 FixedVersion 에서
GO_MODULE_UPGRADES / GO_BUILDER_TAG 를 산출한다. 사람이 CVE 를 훑어 최대값을 고르지
않는다. 빌드 시점에 최신을 당기는 방식(go get -u)은 재현성을 버리므로 택하지 않았다 —
값은 제안만 하고 채택은 사람이 커밋한다.
- images/argocd/go-mod-upgrade.sh 신규 — 목록 하나를 네 프로젝트에 재사용한다.
go get 은 의존성에 없는 모듈도 go.mod 에 추가하므로 그래프에 있는 것만 골라 적용한다.
실측 함정 — verify.sh 에 반영했다
--------------------------------
차트의 repo-server init 컨테이너가 `cp --update=none` 을 쓴다. 이 형식은 GNU coreutils
9.3+ 에서만 되는데, BCI 15.7 로 빌드한 이미지가 게이트도 verify.sh 도 통과하고 **배포
시점에** Init:CrashLoopBackOff 로 죽었다. BCI 16.0(coreutils 9.6)으로 올려 해소했고,
verify.sh 가 이 명령과 copyutil 흐름을 직접 재현하도록 해서 다음엔 빌드 단계에서 걸린다.
16.0 의 패키지 가용성과 trivy 커버리지(CoverageProbe=ok, EOSL 아님)도 확인했다.
검증
----
게이트 PASS — 실효 CRITICAL/HIGH 0건, CoverageProbe ok
기능 VERIFY-OK (심볼릭 링크 9개 · 번들 도구 3종 · LFS 필터 · copyutil 흐름)
배포 docker.io/paasup 에 push 후 운영 argocd 교체 — 파드 8종 Running,
admin 로그인, Application 2건 Synced/Healthy, repo-server 렌더링 확인
(서버가 go1.26.6 · helm v4.2.4 로 보고 — 우리 빌드가 맞다)
카탈로그 차단은 574 → 239 건이 됐고(7.7.0 삭제 · dex 비활성 · 이 커밋), 남은 239 건은
전부 동결된 7.8.11 몫이다. 운영에 쓰는 10.4.0 은 0 건이다. 경과·미결은 MEMORY.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
# argocd 이미지 기능 검증 — 호스트에서 bash 로 실행된다
|
||||
# (build-hardened-image.sh 가 `env TAG=... PLATFORM=... <build.env 변수들> bash verify.sh`
|
||||
# 형태로 호출한다).
|
||||
#
|
||||
# 이 이미지의 핵심 위험은 "번들 도구를 릴리스 바이너리 다운로드에서 소스 컴파일로 바꾼 것"과
|
||||
# "ubuntu → SUSE BCI 베이스 교체" 두 가지다. 그래서 argocd 본체뿐 아니라 helm·kustomize·
|
||||
# git-lfs·tini·connect-proxy 가 전부 실제로 실행되는지, 업스트림이 만들던 런타임 구조
|
||||
# (심볼릭 링크 9개, /etc/gitconfig 의 LFS 필터, /app/config 권한)가 그대로인지 확인한다.
|
||||
#
|
||||
# 실제 기동(서버·컨트롤러)은 Kubernetes API 가 필요해 이 스모크 범위 밖이다 —
|
||||
# dev 클러스터 배포 검증(.claude/deploy-test-procedure.md, scripts/deploy-test/deploy-test-argo-cd.sh)이
|
||||
# 담당한다.
|
||||
#
|
||||
# 마지막 줄에 VERIFY-OK 를 출력하면 통과다. build-hardened-image.sh 가 그것으로 판정한다.
|
||||
set -e
|
||||
|
||||
TAG="${TAG:?TAG 환경변수가 필요하다}"
|
||||
PLATFORM="${PLATFORM:-linux/amd64}"
|
||||
APP_VERSION="${APP_VERSION:?APP_VERSION 환경변수가 필요하다 (build.env 에서 전달)}"
|
||||
SOURCE_COMMIT="${SOURCE_COMMIT:?SOURCE_COMMIT 환경변수가 필요하다}"
|
||||
HELM_VERSION="${HELM_VERSION:?HELM_VERSION 환경변수가 필요하다}"
|
||||
KUSTOMIZE_VERSION="${KUSTOMIZE_VERSION:?KUSTOMIZE_VERSION 환경변수가 필요하다}"
|
||||
GIT_LFS_VERSION="${GIT_LFS_VERSION:?GIT_LFS_VERSION 환경변수가 필요하다}"
|
||||
|
||||
echo "== 이미지 메타데이터 =="
|
||||
USER_CFG="$(docker inspect --format '{{.Config.User}}' "$TAG")"
|
||||
[ "$USER_CFG" = "999" ] || { echo "FAIL: Config.User 가 999 가 아니다 (실제: $USER_CFG) — 업스트림 ARGOCD_USER_ID"; exit 1; }
|
||||
ENTRY="$(docker inspect --format '{{json .Config.Entrypoint}}' "$TAG")"
|
||||
case "$ENTRY" in
|
||||
*tini*) ;;
|
||||
*) echo "FAIL: ENTRYPOINT 에 tini 가 없다 (실제: $ENTRY)"; exit 1 ;;
|
||||
esac
|
||||
echo " Config.User=$USER_CFG Entrypoint=$ENTRY"
|
||||
|
||||
docker run --rm -i --platform "$PLATFORM" \
|
||||
-e APP_VERSION="$APP_VERSION" \
|
||||
-e SOURCE_COMMIT="$SOURCE_COMMIT" \
|
||||
-e HELM_VERSION="$HELM_VERSION" \
|
||||
-e KUSTOMIZE_VERSION="$KUSTOMIZE_VERSION" \
|
||||
-e GIT_LFS_VERSION="$GIT_LFS_VERSION" \
|
||||
--entrypoint sh "$TAG" <<'GUEST'
|
||||
set -e
|
||||
|
||||
echo "== argocd 본체 =="
|
||||
OUT="$(argocd version --client --short)"
|
||||
echo " $OUT"
|
||||
case "$OUT" in
|
||||
*"v$APP_VERSION"*) ;;
|
||||
*) echo "FAIL: 버전 출력에 v$APP_VERSION 이 없다 — Makefile ldflags 주입 확인 필요"; exit 1 ;;
|
||||
esac
|
||||
case "$OUT" in
|
||||
*"$SOURCE_COMMIT"*) ;;
|
||||
*) echo "WARN: 버전 출력에 pinned commit 이 안 보인다 (short 출력이라 생략됐을 수 있음)" ;;
|
||||
esac
|
||||
|
||||
echo "== 업스트림 심볼릭 링크 9개 =="
|
||||
for n in argocd-server argocd-repo-server argocd-cmp-server argocd-application-controller \
|
||||
argocd-dex argocd-notifications argocd-applicationset-controller argocd-k8s-auth \
|
||||
argocd-commit-server; do
|
||||
[ -x "/usr/local/bin/$n" ] || { echo "FAIL: /usr/local/bin/$n 없음"; exit 1; }
|
||||
done
|
||||
echo " 9개 모두 존재·실행 가능"
|
||||
|
||||
echo "== 번들 도구 (소스 컴파일로 교체한 것들) =="
|
||||
H="$(helm version --short)"
|
||||
echo " helm $H"
|
||||
case "$H" in *"$HELM_VERSION"*) ;; *) echo "FAIL: helm 버전이 $HELM_VERSION 이 아니다"; exit 1 ;; esac
|
||||
|
||||
K="$(kustomize version)"
|
||||
echo " kustomize $K"
|
||||
case "$K" in *"$KUSTOMIZE_VERSION"*) ;; *) echo "FAIL: kustomize 버전이 $KUSTOMIZE_VERSION 이 아니다"; exit 1 ;; esac
|
||||
|
||||
L="$(git-lfs version)"
|
||||
echo " git-lfs $L"
|
||||
case "$L" in *"$GIT_LFS_VERSION"*) ;; *) echo "FAIL: git-lfs 버전이 $GIT_LFS_VERSION 이 아니다"; exit 1 ;; esac
|
||||
|
||||
echo "== SLE_BCI 에 없어 소스 빌드한 C 도구 =="
|
||||
[ -x /usr/bin/tini ] || { echo "FAIL: /usr/bin/tini 없음"; exit 1; }
|
||||
/usr/bin/tini --version
|
||||
[ -x /usr/bin/connect-proxy ] || { echo "FAIL: /usr/bin/connect-proxy 없음"; exit 1; }
|
||||
# connect 는 인자 없이 부르면 usage 를 내고 비정상 종료한다 — 실행 가능 여부만 본다.
|
||||
/usr/bin/connect-proxy >/dev/null 2>&1 || true
|
||||
echo " tini · connect-proxy 실행 가능"
|
||||
|
||||
echo "== 런타임 필수 도구 (업스트림 apt 목록 대응) =="
|
||||
git --version >/dev/null || { echo "FAIL: git 없음"; exit 1; }
|
||||
gpg --version >/dev/null || { echo "FAIL: gpg 없음"; exit 1; }
|
||||
ssh -V 2>/dev/null || ssh -V || { echo "FAIL: ssh 없음"; exit 1; }
|
||||
echo " git · gpg · ssh 정상"
|
||||
|
||||
echo "== 차트가 실제로 실행하는 명령 =="
|
||||
# argo-cd 차트의 repo-server init 컨테이너(copyutil)가 그대로 쓰는 형식이다.
|
||||
# /bin/cp --update=none /usr/local/bin/argocd /var/run/argocd/argocd
|
||||
# `--update=none` 은 GNU coreutils 9.3+ 에서만 된다. 베이스 OS 의 coreutils 가 낮으면
|
||||
# 이미지는 멀쩡히 빌드·스캔을 통과하고 **배포 시점에** Init:CrashLoopBackOff 로 죽는다
|
||||
# (실측). 그래서 여기서 직접 확인한다 — 베이스를 바꿀 때 이 검사가 걸러준다.
|
||||
: > /tmp/_vsrc
|
||||
if ! /bin/cp --update=none /tmp/_vsrc /tmp/_vdst 2>/dev/null; then
|
||||
echo "FAIL: 'cp --update=none' 이 동작하지 않는다 — 베이스 OS 의 coreutils 가 9.3 미만이다."
|
||||
echo " argo-cd 차트의 repo-server init 컨테이너가 이 형식을 쓰므로 배포가 실패한다."
|
||||
cp --version 2>/dev/null | head -1
|
||||
exit 1
|
||||
fi
|
||||
echo " cp --update=none 동작 ($(cp --version 2>/dev/null | head -1))"
|
||||
|
||||
# argocd 바이너리를 공유 볼륨에 복사하는 흐름 자체를 그대로 재현해 본다.
|
||||
mkdir -p /tmp/_vrun
|
||||
/bin/cp --update=none /usr/local/bin/argocd /tmp/_vrun/argocd || { echo "FAIL: argocd 복사 실패"; exit 1; }
|
||||
/bin/ln -sf /tmp/_vrun/argocd /tmp/_vrun/argocd-cmp-server || { echo "FAIL: cmp-server 심볼릭 링크 실패"; exit 1; }
|
||||
[ -x /tmp/_vrun/argocd-cmp-server ] || { echo "FAIL: 복사된 argocd 가 실행 가능하지 않다"; exit 1; }
|
||||
echo " copyutil 흐름(복사 + cmp-server 링크) 재현 성공"
|
||||
|
||||
echo "== git-lfs 시스템 설정 (업스트림 git lfs install --system) =="
|
||||
git config --system --get filter.lfs.clean >/dev/null || { echo "FAIL: /etc/gitconfig 에 LFS 필터가 없다"; exit 1; }
|
||||
echo " filter.lfs.clean 등록됨"
|
||||
|
||||
echo "== 업스트림 런타임 디렉토리 구조 =="
|
||||
[ -L /etc/ssh/ssh_known_hosts ] || { echo "FAIL: /etc/ssh/ssh_known_hosts 심볼릭 링크 없음"; exit 1; }
|
||||
[ -d /app/config/tls ] || { echo "FAIL: /app/config/tls 없음"; exit 1; }
|
||||
[ -d /app/config/gpg/keys ] || { echo "FAIL: /app/config/gpg/keys 없음"; exit 1; }
|
||||
[ -x /usr/local/bin/entrypoint.sh ] || { echo "FAIL: entrypoint.sh 없음"; exit 1; }
|
||||
[ -x /usr/local/bin/uid_entrypoint.sh ] || { echo "FAIL: uid_entrypoint.sh 없음"; exit 1; }
|
||||
[ -x /usr/local/bin/gpg-wrapper.sh ] || { echo "FAIL: gpg-wrapper.sh 없음"; exit 1; }
|
||||
echo " ssh_known_hosts 링크 · /app/config/{tls,gpg/keys} · 래퍼 스크립트 정상"
|
||||
|
||||
echo "VERIFY-OK"
|
||||
GUEST
|
||||
Reference in New Issue
Block a user