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:
wbsong111
2026-08-19 15:04:05 +09:00
parent 7957c6a9eb
commit 33e91c0536
10 changed files with 965 additions and 1 deletions
+35
View File
@@ -129,6 +129,41 @@ COPY --from=builder /rootfs/ /
업스트림 Dockerfile 의 패키지 목록을 그대로 옮기면 `No provider of '...' found`
빌드가 실패한다. `zypper -n search -t package '<패턴>'` 로 먼저 확인한다.
## Go 모듈 CVE — 업그레이드 버전을 손으로 찾지 않는다
Go 바이너리에 정적 링크된 모듈의 차단 CVE 는 `build.env``GO_MODULE_UPGRADES`
`<module>@<version>` 을 적어 해소한다. 그 버전은 게이트 리포트의 `FixedVersion` 에 이미
들어 있으므로 스크립트가 뽑는다 — 사람이 CVE 를 하나씩 훑어 최대값을 고르지 않는다.
```sh
python3 scripts/build/suggest-go-upgrades.py --reports <trivy-reports 디렉토리> [--image <필터>]
```
붙여넣을 수 있는 `GO_MODULE_UPGRADES="..."` 한 줄과, 모듈별 근거(설치된 버전 → 목표 버전,
관련 CVE 목록)를 주석으로 낸다. `stdlib` 은 모듈이 아니라 툴체인 문제이므로 따로
`GO_BUILDER_TAG` 후보를 계산해 알려준다.
**빌드 시점에 최신을 당기지 않는 이유**`go get -u` 로 매번 최신을 끌면 같은 소스로
빌드해도 이미지가 달라진다. 이 문서가 "롤링 태그를 쓰지 않는다" 고 정한 것과 같은 이유다.
버전 핀은 **우리가 무엇을 검증했는지의 기록**이고, `git diff` 에 무엇이 왜 올라갔는지
남는다. 스크립트는 값을 제안만 하고 채택은 사람이 커밋한다.
두 가지 실측 함정이 있다.
- **`FixedVersion` 의 여러 값은 "더 높은 버전"이 아니라 브랜치별 대안이다.**
stdlib 의 `1.25.13, 1.26.6, 1.27.0-rc.3` 은 세 브랜치 각각에서 고쳐진 지점이다.
전체 최대값을 고르면 프리릴리스를 정식 버전으로 오독한다(실제로 그 버그를 냈다).
스크립트가 이 규칙을 처리한다.
- **제안값은 CVE 요건의 최소치다.** 모듈 간 제약으로 더 올려야 할 수 있다 — 실측:
`go-git 5.19.2``x/net 0.56.0``x/crypto 0.53.0` 을 요구해 제안값 `0.52.0` 으로는
빌드가 `requires golang.org/x/crypto@v0.53.0, not v0.52.0` 로 실패했다. 그 메시지가
가리키는 버전으로 올린다.
한 이미지가 여러 Go 프로젝트를 빌드하면(예: `argocd` 는 argocd·helm·kustomize·git-lfs 를
함께 빌드한다) **목록 하나를 전부에 재사용한다**`images/argocd/go-mod-upgrade.sh` 처럼
"그 프로젝트의 의존성 그래프에 있는 모듈만" 골라 적용하는 헬퍼를 두면 된다. `go get`
의존성에 없는 모듈도 `go.mod` 에 추가해버리므로 그냥 넘기면 안 된다.
## 두 가지 유형 (둘 다 같은 스크립트를 쓴다)
| 유형 | Dockerfile 이 하는 일 | 셸 유무 |
+71 -1
View File
@@ -5,12 +5,82 @@
- 프로젝트 개요·설계 원칙 → [CLAUDE.md](CLAUDE.md)
- SBOM·CVE 게이트 메커니즘 → [doc/sbom-pipeline.md](doc/sbom-pipeline.md)
최종 갱신: 2026-08-12
최종 갱신: 2026-08-19
---
## 다음 작업
**argo-cd@10.4.0 이 게이트 차단 0건으로 완결됐다(2026-08-19).** ArgoCD 가 k8s 1.35 를
지원하지 못해 시작한 업그레이드가 CVE 조치까지 이어진 건이다.
**발단 — k8s 1.33 신규 필드로 ArgoCD 가 전멸했다.** `.status.terminatingReplicas`(k8s 1.33
도입)를 ArgoCD v2.14.5(k8s 라이브러리 0.31/0.32)가 몰라서, `ServerSideApply=true`
Application 이 전부 `ComparisonError: field not declared in schema` 로 죽었다. 차트
7.8.11 → 10.4.0(ArgoCD v3.5.1, k8s 라이브러리 0.36.1)으로 해소했고 dev 클러스터
(1.35.7+rke2r1)에서 발생·해소를 모두 실측했다.
**CVE 조치 — 레버를 순서대로 적용해 574 → 239건.**
| 조치 | 차단 |
|---|---:|
| 시작(7.7.0·7.8.11·10.4.0 전부) | 574 |
| `argo-cd/7.7.0` 삭제(구버전, EOSL 이미지 2종 포함) | 337 |
| `dex.enabled=false` — 쓰지도 않는데 떠 있었다 | 280 |
| `images/argocd/` 자체 빌드 | **239** |
남은 239건은 전부 **동결된 7.8.11**(직전 버전) 몫이고, 운영에 쓰는 **10.4.0 은 0건**이다.
7.8.11 을 언제 지울지가 미결이다 — 지우면 게이트가 PASS 로 떨어진다.
**dex 를 끈 것이 가장 값싼 레버였다.** ArgoCD 는 `oidc.config` 가 있으면 dex 를 우회하는데
차트 기본값이 `enabled: true` 라 유휴 파드가 떠서 차단의 절반 이상을 만들고 있었다.
dipup 도 항상 `configs.cm.oidc.config` 로 Keycloak 에 직접 붙으므로 dex 를 쓰지 않는다.
**대응 레버를 보기 전에 "이 컴포넌트가 필요하긴 한가" 를 먼저 묻는다** 는 교훈이다.
**`images/argocd/` 는 기존 자체 빌드 중 가장 크다** — Go 프로젝트 4개(argocd·helm·
kustomize·git-lfs) + C 2개(tini·connect-proxy) + Node UI. 업스트림이 번들 도구를 **미리
빌드된 릴리스 바이너리로 내려받기** 때문에 그 바이너리의 Go 버전을 우리가 통제할 수 없고,
같은 CVE 가 여러 바이너리에 공유돼 **부분 조치가 무효**였다(본체만 고치면 거의 줄지 않았다).
실측 함정 셋:
- **`cp --update=none` 이 베이스 OS 를 갈랐다.** 차트의 repo-server init 컨테이너가 쓰는
이 형식은 GNU coreutils 9.3+ 에서만 된다. BCI 15.7 로 빌드한 이미지가 게이트도
`verify.sh` 도 통과하고 **배포 시점에** `Init:CrashLoopBackOff` 로 죽었다. BCI 16.0
(coreutils 9.6)으로 올려 해소했고, `verify.sh` 가 이 명령을 직접 실행해 다음엔 빌드
단계에서 걸리게 했다. **게이트 PASS 는 동작을 증명하지 않는다** 의 실제 사례다.
- **같은 날 재빌드는 태그가 겹친다.** 태그에 빌드일을 넣어도 같은 날 다시 빌드하면 태그가
같아, 노드가 캐시한 옛 digest 가 그대로 쓰인다(`imagePullPolicy: IfNotPresent`).
이번엔 검증용으로 `Always` 를 넣어 우회했다 — 프레임워크 차원의 미결 사항이다.
- **모듈 업그레이드 제안값은 최소치다.** 모듈 간 제약으로 더 올려야 할 수 있다
(`requires <module>@vX, not @vY` 로 빌드가 실패한다).
**Go 모듈 CVE 조치를 자동화했다**`scripts/build/suggest-go-upgrades.py` 가 스캔
리포트의 `FixedVersion` 에서 `GO_MODULE_UPGRADES` / `GO_BUILDER_TAG` 를 산출한다. 사람이
CVE 를 훑어 최대값을 고르지 않는다. 다음 조치에서 바뀌는 것은 `build.env` 뿐이고
Dockerfile 은 그대로다(모듈 목록·패키지 목록을 전부 값으로 뺐다).
### 남은 것 (argo-cd)
- `argo-cd/7.8.11` 삭제 여부 — 지우면 게이트 PASS
- 자체 빌드 이미지의 정식 반영 경로: CI `build-image.yml`(`workflow_dispatch`) 로 다시 태워
볼지, 로컬 push 로 끝낼지. 이번엔 로컬에서 `docker.io/paasup` 에 push 하고 운영 argocd 를
교체해 동작 확인까지 마쳤다.
### 별건으로 발견한 airflow 문제 2건
- **매니페스트가 비결정적이다.** `webserver-secret-key-secret.yaml``randAlphaNum` 으로
매 렌더마다 새 값을 만들어, repo-server 의 매니페스트 캐시가 날아갈 때마다(ArgoCD 재시작·
hard refresh) webserver 가 롤링되고 웹 세션이 끊긴다. reconcile 만으로는 재렌더가 일어나지
않아 상시 루프는 아니다(4시간 19분 동안 롤아웃 0회로 확인). `webserverSecretKey` 또는
`webserverSecretKeySecretName` 을 지정하면 해소된다. 카탈로그 기본값에 없어 모든 테넌트가
해당된다.
- **migrations job 이 5분마다 재실행된다.** `migrateDatabaseJob.useHelmHooks: false`
hook 이 아닌 추적 리소스가 됐는데 `ttlSecondsAfterFinished: 300` 이 그대로라, TTL 삭제 →
ArgoCD selfHeal 재생성이 반복된다. `ttlSecondsAfterFinished` 를 끄거나 hook 으로 되돌린다.
---
**apisix@2.16.0 전체가 게이트 PASS 로 완결됐다(2026-08-12).** `images/adc/` 신규 추가로
마지막 남은 이미지까지 해소 — 차단 55건(2026-08-12 초 기준) → **0건**. adc·
apisix-ingress-controller·apisix(paasup/apisix) 세 자체 빌드 이미지 전부 게이트
+104
View File
@@ -0,0 +1,104 @@
# argocd (자체 빌드)
`quay.io/argoproj/argocd` 를 대체하는 하드닝 이미지. 업스트림 소스를 pinned commit 으로
직접 컴파일하고, 업스트림이 릴리스 바이너리로 내려받던 번들 도구(helm·kustomize·git-lfs)까지
같은 Go 툴체인으로 다시 만든다.
```sh
IMAGE=argocd BASE_OS=source bash scripts/build/build-hardened-image.sh /tmp/out
```
## 왜 자체 빌드인가
이 이미지의 차단 CVE 는 대부분 OS 패키지가 아니라 **바이너리에 정적 링크된 Go 모듈**이다.
그래서 앞선 두 레버가 모두 통하지 않는다.
- **상위 태그 교체 불가** — 이미 최신 릴리스다. 번들 도구도 각각 최신이고, 낡은 Go 는
각 업스트림 프로젝트의 선택이라 버전을 올려도 바뀌지 않는다.
- **베이스 OS 교체로는 거의 안 잡힌다** — 모듈은 컴파일된 바이너리 안에 있다.
## 왜 번들 도구까지 다시 만드는가
업스트림 Dockerfile 은 helm/kustomize/git-lfs 를 `hack/install.sh` 로 **미리 빌드된 릴리스
바이너리를 내려받아** 넣는다. 그 바이너리의 Go 버전은 우리가 통제할 수 없다.
그리고 **부분 조치는 효과가 없다.** 같은 `stdlib`·`x/crypto` 취약점이 다섯 바이너리에 각각
들어있어서, 한 바이너리만 고치면 나머지에 그대로 남는다. 고유 CVE 는 소수이고 대부분이
공유분이라 전부 다시 만들어야 게이트가 0 이 된다(실측 확인 — argocd 본체만 조치했을 때는
거의 줄지 않았다).
`pebble` 은 우리가 넣은 것이 아니라 ubuntu 베이스에 딸려온 것이다 — 베이스를 SUSE BCI 로
바꾸면 함께 사라진다.
## 다음 CVE 조치 — Dockerfile 은 건드리지 않는다
버전도 모듈 목록도 Dockerfile 에 박혀 있지 않다. 새 차단 CVE 가 나오면 **`source.build.env`
한 곳만** 바뀐다.
```sh
# 1) 게이트 리포트에서 업그레이드 값을 산출한다 (손으로 찾지 않는다)
python3 scripts/build/suggest-go-upgrades.py --reports sbom-out/trivy-reports --image argocd
# 2) 출력된 GO_MODULE_UPGRADES / GO_BUILDER_TAG 를 source.build.env 에 반영
# 3) 재빌드 — 게이트까지 한 번에 돈다
IMAGE=argocd BASE_OS=source bash scripts/build/build-hardened-image.sh /tmp/out
```
| 새 CVE 유형 | 바꾸는 값 |
|---|---|
| Go `stdlib` | `GO_BUILDER_TAG` |
| Go 모듈 | `GO_MODULE_UPGRADES``<module>@<version>` 추가 |
| OS 패키지 | `RUNTIME_BASE` 또는 `RUNTIME_PACKAGES` |
| 구성요소 새 릴리스 | 해당 `*_VERSION` |
`GO_MODULE_UPGRADES` 는 argocd·helm·kustomize·git-lfs **네 프로젝트에 공통 적용**된다.
[go-mod-upgrade.sh](go-mod-upgrade.sh) 가 각 프로젝트의 의존성 그래프에 있는 것만 골라
적용하므로 목록 하나를 그대로 재사용한다 — `go get` 은 의존성에 없는 모듈도 `go.mod`
추가해버리기 때문에 필요한 필터다.
> 제안값은 CVE 요건의 **최소치**다. 모듈 간 제약으로 더 올려야 할 수 있다 — 빌드가
> `requires <module>@vX, not @vY` 로 실패하면 그 버전으로 올린다.
## 업스트림과 다르게 한 부분
| 항목 | 업스트림 | 이 이미지 | 이유 |
|---|---|---|---|
| 최종 베이스 | `ubuntu` | `registry.suse.com/bci/bci-base` | 카탈로그는 SUSE BCI 하나만 쓴다(`.claude/image-authoring.md` 원칙 2). `pebble` 도 함께 소멸 |
| helm / kustomize / git-lfs | 릴리스 바이너리 다운로드 | 소스 컴파일 | 다운로드 바이너리의 Go 를 통제할 수 없다 |
| helm 버전 | 업스트림 pin | 최신 | kustomize·git-lfs 는 pin 이 이미 최신이라 그대로 |
| `tini` · `connect-proxy` | apt 패키지 | 소스 빌드 | SLE_BCI 에 둘 다 없다. 기능을 빼지 않기 위해 빌드 |
| Go 툴체인 | 업스트림 pin | `GO_BUILDER_TAG` | stdlib CVE 해소 |
| 취약 모듈 | 그대로 | `GO_MODULE_UPGRADES` 로 강제 업그레이드 | |
| `BUILD_DATE` | 빌드 시각 | 고정값 | 빌드 재현성 |
| UI | node 빌드 | **동일** | Go embed 로 바이너리에 들어가 생략 불가 |
애플리케이션 코드 자체는 pinned 태그 그대로다 — 최소 diff 원칙.
## SLE 패키지 매핑
업스트림 apt 목록을 SLE_BCI 이름으로 옮겼다(`.claude/image-authoring.md` 참고).
목록 자체는 `source.build.env``RUNTIME_PACKAGES` 에 있다.
| ubuntu | SLE_BCI |
|---|---|
| `git` | `git-core` |
| `tzdata` | `timezone` |
| `gpg` · `gpg-agent` | `gpg2` |
| `openssh-client` | `openssh-clients` |
| `ca-certificates` | `ca-certificates` (동일) |
| `tini` | **없음** → 소스 빌드 |
| `connect-proxy` | **없음** → 소스 빌드 |
## 검증
`verify.sh` 가 보는 것 — argocd 버전 문자열, 업스트림 심볼릭 링크 9개, 번들 도구 3개의
버전, tini·connect-proxy 실행 가능, git/gpg/ssh 존재, `/etc/gitconfig` 의 LFS 필터,
`/app/config` 디렉토리 구조와 래퍼 스크립트.
**게이트 PASS 는 "동작한다" 를 증명하지 않는다.** 실제 기동은 Kubernetes API 가 필요해
스모크 범위 밖이다 — dev 클러스터 배포 검증을 반드시 한다.
```sh
VALUES_FILE=<values> bash scripts/deploy-test/deploy-test-argo-cd.sh /tmp/deploy-test-argocd
```
+18
View File
@@ -0,0 +1,18 @@
# build-image.yml 이 읽는 카탈로그 반영 메타데이터. 빌드 정의(<variant>.build.env)와는
# 별개다 — 이건 "이 이미지가 어느 차트의 어느 필드를 가리키는가" 만 담는다.
#
# argo-cd 카탈로그는 10.4.0(현재 운영)과 7.8.11(직전)을 보관한다. 자체 빌드 이미지는
# 운영 버전에만 반영한다 — 7.8.11 은 released 로 동결하고 손대지 않는다
# (7.7.0 은 2026-08-19 삭제).
CHART_DIRS="manifests/helm/argo-cd/10.4.0"
# 태그 표기 스타일 — imageName(단일 필드 문자열) | split(registry/repository/tag 분리)
TAG_STYLE=split
# argo-cd 차트는 모든 컴포넌트(server/repo-server/controller/applicationset/notifications)가
# global.image.{repository,tag} 하나를 공유한다 — 컴포넌트별 image 블록은 기본이 비어 있고
# 비면 global 을 상속한다. 그래서 이 한 곳만 갱신하면 전부 바뀐다.
TAG_BLOCK=global.image
# base_os 입력을 안 주면 쓸 기본 변종 (images/<image>/<DEFAULT_BASE_OS>.build.env)
DEFAULT_BASE_OS=source
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env sh
# go-mod-upgrade — CVE 대응으로 강제 업그레이드할 Go 모듈 목록을 현재 모듈에 적용한다.
# 빌더 스테이지에서 각 Go 프로젝트 디렉토리에 들어간 뒤 인자 없이 호출한다.
#
# 왜 스크립트로 빼는가
# --------------------
# 업그레이드 대상을 Dockerfile 에 직접 나열하면, 새 CVE 가 하나 나올 때마다
# ① build.env 에 <MOD>_FIX_VERSION 추가 ② Dockerfile 의 ARG 선언 추가(스테이지마다)
# ③ go get 목록에 추가(빌드 대상마다) ④ BUILD_ARGS 에 이름 추가
# 네 곳을 고쳐야 한다. 목록을 GO_MODULE_UPGRADES 값 하나로 받으면 **build.env 한 줄만**
# 바뀌고 Dockerfile 은 그대로다 — 이 이미지는 앞으로도 CVE 조치를 반복해서 받는다.
#
# 목록에 있으나 이 프로젝트가 쓰지 않는 모듈은 건너뛴다
# ----------------------------------------------------
# 한 이미지 안에서 여러 Go 프로젝트(argocd·helm·kustomize·git-lfs)를 빌드하는데 각자
# 의존성이 다르다. `go get` 은 의존성 그래프에 없는 모듈도 go.mod 에 요구사항으로
# **추가**하므로(뒤이은 go mod tidy 가 지우긴 하지만) 애초에 넣지 않는 편이 의도가 분명하고,
# 목록 하나를 네 프로젝트에 그대로 재사용할 수 있다.
#
# 형식: 공백으로 구분한 `<module path>@<version>` 목록.
# GO_MODULE_UPGRADES="golang.org/x/net@v0.56.0 google.golang.org/grpc@v1.82.1"
set -eu
if [ -z "${GO_MODULE_UPGRADES:-}" ]; then
echo "go-mod-upgrade: GO_MODULE_UPGRADES 가 비어 있다 — 업그레이드 없이 진행"
exit 0
fi
targets=""
for spec in $GO_MODULE_UPGRADES; do
path="${spec%@*}"
if [ "$path" = "$spec" ]; then
echo "go-mod-upgrade: ::error:: '$spec' 에 @<version> 이 없다"
exit 2
fi
if go list -m "$path" >/dev/null 2>&1; then
targets="$targets $spec"
echo "go-mod-upgrade: 적용 $spec"
else
echo "go-mod-upgrade: 건너뜀 $path (이 프로젝트의 의존성 그래프에 없음)"
fi
done
if [ -z "$targets" ]; then
echo "go-mod-upgrade: 적용 대상 없음"
exit 0
fi
# shellcheck disable=SC2086 # targets 는 공백 구분 목록이라 의도적으로 분리한다
go get $targets
go mod tidy
+263
View File
@@ -0,0 +1,263 @@
# argocd — 업스트림 소스를 pinned commit 으로 직접 컴파일하고, 번들되는 Go 도구
# (helm/kustomize/git-lfs)까지 같은 툴체인으로 다시 만든다.
#
# 왜 자체 빌드인가
# ----------------
# 이 이미지의 차단 CVE 는 대부분 OS 패키지가 아니라 **바이너리에 정적 링크된 Go 모듈**이다.
# 상위 태그 교체(이미 최신)로도, 베이스 OS 교체(모듈은 바이너리 안에 있다)로도 잡히지 않아
# 자체 빌드만 남는다.
#
# 왜 번들 도구까지 다시 만드는가
# ------------------------------
# 업스트림 Dockerfile 은 helm/kustomize/git-lfs 를 hack/install.sh 로 **미리 빌드된 릴리스
# 바이너리를 내려받아** 넣는다. 그 바이너리의 Go 버전은 각 프로젝트가 정한 것이라 우리가
# 태그를 올려도 바뀌지 않는다.
#
# 그리고 부분 조치는 효과가 없다 — 같은 stdlib·x/crypto 취약점이 다섯 바이너리에 각각
# 들어있어서, 한 바이너리만 고치면 나머지에 그대로 남는다. 고유 CVE 는 소수이고 대부분이
# 공유분이다. 전부 다시 만들어야 게이트가 0 이 된다(실측 확인).
#
# 재사용 설계 — Dockerfile 에 버전을 박지 않는다
# ---------------------------------------------
# 이 이미지는 앞으로도 CVE 조치를 반복해서 받는다. 다음 조치에서 바뀌는 것은 전부
# source.build.env 값이다:
#
# GO_MODULE_UPGRADES 강제 업그레이드할 Go 모듈 목록 (네 프로젝트에 공통 적용)
# GO_BUILDER_TAG Go 툴체인 (stdlib CVE 해소)
# *_VERSION 각 구성요소 버전
# RUNTIME_PACKAGES 최종 이미지에 설치할 SLE 패키지
#
# 값은 scripts/build/suggest-go-upgrades.py 가 스캔 리포트에서 산출한다 — 사람이 CVE 를
# 훑어 최대값을 고르지 않는다. 모듈 목록은 go-mod-upgrade.sh 가 프로젝트마다 "그 프로젝트가
# 실제로 쓰는 것만" 골라 적용하므로 목록 하나를 네 프로젝트에 그대로 재사용한다.
#
# 업스트림과 다르게 하는 부분
# ---------------------------
# ubuntu → SUSE BCI(bci-base). 카탈로그는 SUSE BCI 하나만 쓴다
# (.claude/image-authoring.md 원칙 2). argocd 는 git/gpg/ssh 런타임이 필요해 패키지
# 매니저가 있는 base 를 쓴다(micro 씨앗 방식의 복잡도를 감수할 이점이 없었다).
# ubuntu 베이스가 딸려오던 pebble 바이너리도 함께 사라진다.
# tini·connect-proxy 는 SLE_BCI 에 패키지가 없어 소스에서 빌드한다.
# helm/kustomize/git-lfs 를 릴리스 바이너리 다운로드 → 소스 컴파일로 교체.
# UI 는 업스트림과 동일하게 node 로 빌드한다 — Go embed 로 바이너리에 들어가 생략 불가.
# syntax=docker/dockerfile:1
# FROM 에서 쓰는 ARG 는 반드시 첫 FROM 이전(전역 스코프)에 선언해야 한다 —
# 스테이지 안에 두면 지역 변수가 되어 이후 FROM 의 이미지명이 빈 값이 된다
# (.claude/image-authoring.md).
ARG GO_BUILDER_TAG=1.26.6-trixie
ARG NODE_BUILDER_TAG=24.14.1
ARG RUNTIME_BASE=registry.suse.com/bci/bci-base:15.7
####################################################################################################
# UI — 업스트림 argocd-ui 스테이지를 그대로 재현한다. 산출물이 Go embed 로 바이너리에 들어간다.
####################################################################################################
FROM --platform=$BUILDPLATFORM docker.io/library/node:${NODE_BUILDER_TAG} AS argocd-ui
ARG SOURCE_COMMIT
ARG APP_VERSION
ADD https://github.com/argoproj/argo-cd.git#${SOURCE_COMMIT} /src
# 업스트림과 동일: corepack 으로 pnpm 을 켜고 lockfile 그대로 설치한다.
WORKDIR /src/ui
RUN npm install -g corepack@0.34.6 && corepack enable && pnpm install --frozen-lockfile
# UI 번들은 아키텍처 무관이다 — 업스트림 주석대로 TARGETARCH 를 쓰지 않는다.
ENV ARGO_VERSION=$APP_VERSION
RUN NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OPTIONS=--max_old_space_size=8192 pnpm build
####################################################################################################
# 번들 Go 도구 — 업스트림이 릴리스 바이너리로 받아오던 것을 소스에서 다시 컴파일한다.
#
# 새 툴체인으로 다시 컴파일하면 stdlib 은 해소되지만 각 프로젝트가 go.mod 에 pin 한 모듈은
# 그대로 낡아 있다 — 도구 쪽 모듈을 빠뜨려 게이트가 실패한 적이 있다(실측). 그래서 argocd
# 본체와 같은 GO_MODULE_UPGRADES 를 세 도구에도 적용한다.
####################################################################################################
FROM --platform=$BUILDPLATFORM golang:${GO_BUILDER_TAG} AS go-tools
ARG TARGETARCH
ARG HELM_VERSION
ARG KUSTOMIZE_VERSION
ARG GIT_LFS_VERSION
ARG GO_MODULE_UPGRADES
ENV CGO_ENABLED=0 GOOS=linux GO_MODULE_UPGRADES=${GO_MODULE_UPGRADES}
COPY go-mod-upgrade.sh /usr/local/bin/go-mod-upgrade
RUN chmod 0755 /usr/local/bin/go-mod-upgrade
WORKDIR /out
# helm — 업스트림 Makefile 의 ldflags(version/gitTreeState)를 재현한다.
ADD https://github.com/helm/helm.git#v${HELM_VERSION} /src/helm
RUN --mount=type=cache,target=/root/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
cd /src/helm && go-mod-upgrade && \
P=helm.sh/helm/v4/internal/version && \
GOARCH=${TARGETARCH} go build -trimpath \
-ldflags "-X ${P}.version=v${HELM_VERSION} -X ${P}.gitTreeState=clean" \
-o /out/helm ./cmd/helm
# kustomize — 리포 안의 kustomize/ 서브모듈이 CLI 다.
ADD https://github.com/kubernetes-sigs/kustomize.git#kustomize/v${KUSTOMIZE_VERSION} /src/kustomize
RUN --mount=type=cache,target=/root/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
cd /src/kustomize/kustomize && go-mod-upgrade && \
P=sigs.k8s.io/kustomize/api/provenance && \
GOARCH=${TARGETARCH} go build -trimpath \
-ldflags "-X ${P}.version=v${KUSTOMIZE_VERSION}" \
-o /out/kustomize .
# git-lfs — Makefile 이 ldflags 로 버전을 심는다.
ADD https://github.com/git-lfs/git-lfs.git#v${GIT_LFS_VERSION} /src/git-lfs
RUN --mount=type=cache,target=/root/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
cd /src/git-lfs && go-mod-upgrade && \
GOARCH=${TARGETARCH} go build -trimpath \
-ldflags "-X github.com/git-lfs/git-lfs/v3/config.GitCommit=v${GIT_LFS_VERSION}" \
-o /out/git-lfs .
####################################################################################################
# argocd 본체 — 업스트림 argocd-build 스테이지 + 취약 모듈 강제 업그레이드
####################################################################################################
FROM --platform=$BUILDPLATFORM golang:${GO_BUILDER_TAG} AS argocd-build
ARG TARGETOS
ARG TARGETARCH
ARG SOURCE_COMMIT
ARG APP_VERSION
ARG GO_MODULE_UPGRADES
ENV GO_MODULE_UPGRADES=${GO_MODULE_UPGRADES}
COPY go-mod-upgrade.sh /usr/local/bin/go-mod-upgrade
RUN chmod 0755 /usr/local/bin/go-mod-upgrade
WORKDIR /go/src/github.com/argoproj/argo-cd
ADD https://github.com/argoproj/argo-cd.git#${SOURCE_COMMIT} .
RUN --mount=type=cache,target=/root/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
go-mod-upgrade
COPY --from=argocd-ui /src/ui/dist/app ./ui/dist/app
# 업스트림 Dockerfile 과 동일하게 Makefile 의 argocd-all 타겟을 부른다
# (단일 `go build -o dist/argocd ./cmd`). 버전 심볼은 Makefile 이 ldflags 로 심는다.
# BUILD_DATE 를 고정해 빌드 재현성을 확보한다(업스트림은 date 를 그때그때 넣는다).
RUN --mount=type=cache,target=/root/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
GIT_TAG=v${APP_VERSION} \
GIT_COMMIT=${SOURCE_COMMIT} \
GIT_TREE_STATE=clean \
BUILD_DATE=1970-01-01T00:00:00Z \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
make argocd-all
####################################################################################################
# C 도구 — SLE_BCI 에 패키지가 없어 소스에서 빌드한다(2026-08-19 zypper 실측)
####################################################################################################
FROM ${RUNTIME_BASE} AS c-builder
ARG TINI_VERSION
ARG SSH_CONNECT_VERSION
ARG BUILDER_PACKAGES
# SLE_BCI 미러가 간헐적으로 끊긴다(실측: curl error 56 / SSL unexpected eof). 재시도한다.
RUN for i in 1 2 3 4 5; do \
zypper --non-interactive --gpg-auto-import-keys refresh && break; \
echo "zypper refresh 실패 — 재시도 $i"; sleep 10; \
done && \
zypper --non-interactive install -y --no-recommends ${BUILDER_PACKAGES} && \
zypper --non-interactive clean --all
# tini — 업스트림 argocd 의 ENTRYPOINT(["/usr/bin/tini", "--"]).
# 동적 링크로 빌드한다. 이 스테이지와 final 이 같은 BCI 베이스라 glibc ABI 가 동일하고,
# 업스트림이 쓰는 apt tini 도 동적 링크다. 정적 링크는 SLE_BCI 에 정적 glibc 가 없어
# 애초에 불가능하다(2026-08-19 실측: "cannot find -lc ... static version of the c library").
ADD https://github.com/krallin/tini.git#v${TINI_VERSION} /src/tini
RUN cd /src/tini && \
cmake -DCMAKE_BUILD_TYPE=Release . && \
make tini && \
install -m 0755 tini /out-tini
# connect-proxy — Debian 의 connect-proxy 패키지 원본(gotoh/ssh-connect 의 connect.c).
# SSH-over-proxy 환경에서 git 이 쓴다. 기능을 빼지 않기 위해 빌드한다.
ADD https://github.com/gotoh/ssh-connect.git#${SSH_CONNECT_VERSION} /src/connect
RUN cd /src/connect && \
gcc -O2 -o /out-connect connect.c
####################################################################################################
# 최종 이미지 — 업스트림 argocd-base + final 을 SUSE BCI 위에서 재현한다
####################################################################################################
FROM ${RUNTIME_BASE} AS final
LABEL org.opencontainers.image.source="https://github.com/argoproj/argo-cd"
ARG RUNTIME_PACKAGES
ENV ARGOCD_USER_ID=999
# RUNTIME_PACKAGES 는 업스트림 apt 목록을 SLE 이름으로 옮긴 것이다. 이름이 다른 것들:
# git → git-core tzdata → timezone gpg/gpg-agent → gpg2 openssh-client → openssh-clients
# tini·connect-proxy 는 SLE_BCI 에 없어 c-builder 에서 빌드해 COPY 한다.
# SLE_BCI 미러가 간헐적으로 끊긴다(실측). 재시도한다.
RUN for i in 1 2 3 4 5; do \
zypper --non-interactive --gpg-auto-import-keys refresh && break; \
echo "zypper refresh 실패 — 재시도 $i"; sleep 10; \
done && \
zypper --non-interactive update -y && \
zypper --non-interactive install -y --no-recommends ${RUNTIME_PACKAGES} && \
zypper --non-interactive clean --all && \
rm -rf /var/log/zypp /usr/share/doc/packages/*
RUN groupadd -g $ARGOCD_USER_ID argocd && \
useradd -r -u $ARGOCD_USER_ID -g argocd argocd && \
mkdir -p /home/argocd && \
chown argocd:0 /home/argocd && \
chmod g=u /home/argocd
COPY --from=c-builder /out-tini /usr/bin/tini
COPY --from=c-builder /out-connect /usr/bin/connect-proxy
COPY --from=go-tools /out/helm /usr/local/bin/helm
COPY --from=go-tools /out/kustomize /usr/local/bin/kustomize
COPY --from=go-tools /out/git-lfs /usr/local/bin/git-lfs
# 업스트림이 소스에서 넣는 래퍼 스크립트들 — argocd 가 gpg 검증·entrypoint 에 쓴다.
COPY --from=argocd-build \
/go/src/github.com/argoproj/argo-cd/hack/gpg-wrapper.sh \
/go/src/github.com/argoproj/argo-cd/hack/git-verify-wrapper.sh \
/go/src/github.com/argoproj/argo-cd/entrypoint.sh \
/usr/local/bin/
RUN chmod 0755 /usr/local/bin/gpg-wrapper.sh /usr/local/bin/git-verify-wrapper.sh /usr/local/bin/entrypoint.sh
# git-lfs 시스템 설정(/etc/gitconfig) 을 만들어 LFS 필터를 활성화한다 — 업스트림과 동일.
RUN git lfs install --system
# 하위 호환용 심볼릭 링크 — 업스트림과 동일.
RUN ln -s /usr/local/bin/entrypoint.sh /usr/local/bin/uid_entrypoint.sh
# configmap 마운트 지원 — 업스트림과 동일한 경로/권한.
WORKDIR /app/config/ssh
RUN touch ssh_known_hosts && \
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
WORKDIR /app/config
RUN mkdir -p tls && \
mkdir -p gpg/source && \
mkdir -p gpg/keys && \
chown argocd gpg/keys && \
chmod 0700 gpg/keys
ENV USER=argocd
# dual-stack 환경에서 _grpc_config DNS TXT 조회가 타임아웃을 유발하는 문제 회피 — 업스트림과 동일.
# argocd-cmd-params-cm 으로 덮어쓸 수 있다.
ENV GRPC_ENABLE_TXT_SERVICE_CONFIG=false
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd /usr/local/bin/argocd
# 업스트림과 동일한 심볼릭 링크 9개 — 차트가 이 이름들로 컨테이너 command 를 지정한다.
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-repo-server && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-cmp-server && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-application-controller && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-dex && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-notifications && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-applicationset-controller && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-k8s-auth && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-commit-server
ENTRYPOINT ["/usr/bin/tini", "--"]
USER $ARGOCD_USER_ID
WORKDIR /home/argocd
+77
View File
@@ -0,0 +1,77 @@
# argocd — 소스 컴파일형 자체 빌드 (유일한 변종)
#
# build-hardened-image.sh 가 source 한다. BUILD_ARGS 에 나열한 이름만 --build-arg 로 넘어간다.
# 소스를 직접 컴파일하므로 "베이스 OS" 선택지가 없다 — BASE_OS=source 로 호출한다:
# IMAGE=argocd BASE_OS=source bash scripts/build/build-hardened-image.sh <OUT_DIR>
#
# 왜 자체 빌드하는가 → source.Dockerfile 상단 주석 / README.md.
# argo-cd 10.4.0 CVE 조치(2026-08-19)로 도입 — 근거·경과는 MEMORY.md.
#
# ── 다음 CVE 조치에서 바뀌는 것은 이 파일뿐이다 ────────────────────────────────
# Dockerfile 에는 버전도 모듈 목록도 박혀 있지 않다. 새 차단 CVE 가 나오면:
# Go stdlib CVE → GO_BUILDER_TAG 를 올린다
# Go 모듈 CVE → GO_MODULE_UPGRADES 에 `<module>@<version>` 을 추가한다
# OS 패키지 CVE → RUNTIME_BASE 를 올리거나 RUNTIME_PACKAGES 를 조정한다
# 구성요소 새 릴리스 → 해당 *_VERSION 을 올린다
DOCKERFILE=source.Dockerfile
TARGET=final
TAG_SLUG=security
# build-hardened-image.sh 가 태그·verify.sh 전달용으로 요구하는 범용 필수값.
# 스톡 v3.5.1 과는 태그 슬러그(TAG_SLUG=security)로 구분되므로 버전 문자열은 그대로 둔다.
APP_VERSION=3.5.1
# v3.5.1 태그가 가리키는 커밋(lightweight 태그라 태그 객체 없이 커밋 직접 참조), 2026-08-19 확인.
SOURCE_COMMIT=109ca7ca71139e514114499d294a492e7910a965
# stdlib 차단 CVE 를 해소하는 최소 Go 버전. suggest-go-upgrades.py 가 산출한다.
# 빌더 스테이지에만 쓰이고 최종 이미지에는 남지 않는다.
GO_BUILDER_TAG=1.26.6-trixie
# UI 빌드용. 업스트림 Dockerfile 의 node pin 과 동일하게 맞춘다 — UI 번들은 스캔에서
# 취약점이 잡히지 않으므로 올릴 이유가 없고, lockfile 과의 정합을 지키는 쪽이 안전하다.
NODE_BUILDER_TAG=24.14.1
# 최종 런타임 베이스 — 카탈로그는 SUSE BCI 하나만 쓴다(.claude/image-authoring.md 원칙 2).
# argocd 는 git/gpg/ssh 런타임이 필요해 패키지 매니저가 있는 bci-base 를 쓴다.
# base 와 micro 를 스캔해 비교했고 차이가 없어, micro 씨앗 방식(.claude/image-authoring.md)의
# 복잡도를 감수할 이점이 없었다.
#
# **16.0 을 쓰는 이유 — coreutils 버전이 차트 요구사항을 가른다.**
# argo-cd 차트의 repo-server init 컨테이너(copyutil)가 `cp --update=none` 을 쓴다. 이
# 형식은 GNU coreutils 9.3+ 에서만 되는데 15.7 은 그보다 낮아
# "option '--update' doesn't allow an argument" 로 죽는다(실측 — 15.7 로 빌드해 배포했다가
# repo-server 가 Init:CrashLoopBackOff 로 실패했다). 16.0 은 coreutils 9.6 이라 동작한다.
# 16.0 의 패키지 가용성과 trivy 커버리지(CoverageProbe=ok, EOSL 아님)도 확인했다.
# verify.sh 가 이 명령을 직접 검사하므로 다음에 베이스를 바꿔도 빌드 단계에서 걸린다.
RUNTIME_BASE=registry.suse.com/bci/bci-base:16.0
# 번들 Go 도구 — 업스트림은 hack/install.sh 로 릴리스 바이너리를 내려받지만, 그 바이너리의
# Go 버전이 낡아(git-lfs 1.25.3 · kustomize 1.24.0) CVE 의 주범이었다. 소스에서 다시 만든다.
# helm 만 업스트림 pin(4.2.1) 대신 최신을 쓴다 — 나머지 둘은 pin 이 이미 최신이다.
HELM_VERSION=4.2.4
KUSTOMIZE_VERSION=5.8.1
GIT_LFS_VERSION=3.7.1
# SLE_BCI 에 패키지가 없어 소스 빌드한다(2026-08-19 zypper 실측).
# tini 는 업스트림 ENTRYPOINT, connect-proxy 는 SSH-over-proxy 용이다.
TINI_VERSION=0.19.0
SSH_CONNECT_VERSION=1.106
# 강제 업그레이드할 Go 모듈 — argocd·helm·kustomize·git-lfs 네 프로젝트에 **공통 적용**된다.
# go-mod-upgrade.sh 가 각 프로젝트의 의존성 그래프에 있는 것만 골라 적용하므로 하나의
# 목록을 그대로 재사용할 수 있다.
#
# 값은 suggest-go-upgrades.py 로 산출한다. 단, 제안값은 CVE 요건의 **최소치**라 모듈 간
# 제약으로 더 올려야 할 수 있다 — 빌드가 "requires <module>@vX, not @vY" 로 실패하면 그
# 버전으로 올린다(x/crypto 가 이 경우였다).
GO_MODULE_UPGRADES="golang.org/x/crypto@v0.53.0 golang.org/x/net@v0.56.0 golang.org/x/text@v0.39.0 google.golang.org/grpc@v1.82.1 github.com/go-git/go-git/v5@v5.19.2 oras.land/oras-go/v2@v2.6.2"
# c-builder 가 tini(cmake)·connect(gcc) 를 컴파일하는 데 필요한 것들.
BUILDER_PACKAGES="gcc make cmake git-core tar gzip"
# 최종 이미지 런타임 패키지 — 업스트림 apt 목록의 SLE 대응(README.md 매핑표 참고).
RUNTIME_PACKAGES="git-core ca-certificates gpg2 timezone openssh-clients"
BUILD_ARGS="SOURCE_COMMIT APP_VERSION GO_BUILDER_TAG NODE_BUILDER_TAG RUNTIME_BASE HELM_VERSION KUSTOMIZE_VERSION GIT_LFS_VERSION TINI_VERSION SSH_CONNECT_VERSION GO_MODULE_UPGRADES BUILDER_PACKAGES RUNTIME_PACKAGES"
+128
View File
@@ -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
@@ -4,6 +4,20 @@
# 이 파일은 "PaaSup 이 실제로 어떤 key 를 쓰는가"의 기준이며(Breaking Change 판정 근거),
# 도메인·시크릿 같은 환경별 값은 placeholder 로 둔다. 비밀값을 커밋하지 않는다.
global:
image:
# 자체 빌드(대응 우선순위 c) — 업스트림 v3.5.1 의 차단 CVE 대부분이 바이너리에 정적
# 링크된 Go 모듈이라 상위 태그 교체·베이스 OS 교체로 해소되지 않았다. 번들 도구
# (helm/kustomize/git-lfs)까지 최신 툴체인으로 다시 컴파일했다.
# 빌드 정의: images/argocd/. 상위 태그가 이 문제를 해결하면(대응 우선순위 a)
# 업스트림으로 되돌리는 것이 우선이다.
#
# 이 값은 argocd 바이너리를 쓰는 5개 컴포넌트(server/repo-server/application-controller/
# applicationset/notifications)가 공유한다 — 컴포넌트별 image 블록이 비면 global 을
# 상속한다. dex·redis 는 각자 별도 이미지라 영향받지 않는다.
repository: docker.io/paasup/argocd
tag: "3.5.1-security-hardened-20260819"
configs:
params:
# ArgoCD v3.0 부터 리소스 health 를 Application CR 에 저장하지 않는다(기본 false).
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""
suggest-go-upgrades.py 스캔 리포트에서 GO_MODULE_UPGRADES 값을 산출한다.
필요한가
-----------
자체 빌드 이미지가 Go 모듈 CVE 해소할 `images/<image>/<variant>.build.env`
`GO_MODULE_UPGRADES` `<module>@<version>` 적는다. 버전을 사람이 게이트 리포트에서
하나씩 찾아 최대값을 고르는 것은 지루하고 틀리기 쉽다 필요한 정보는 이미 trivy 리포트의
`FixedVersion` 있다. 스크립트가 그것을 뽑아 붙여넣을 있는 형태로 낸다.
"완전 자동"(빌드 시점에 최신으로 당기기) 아닌가
--------------------------------------------------
`go get -u` 매번 최신을 끌면 같은 소스로 빌드해도 이미지가 달라진다. 레포는 반대를
택했다(.claude/image-authoring.md "롤링 태그를 쓰지 않는다", SOURCE_COMMIT·BUILD_DATE 고정).
버전 핀은 **우리가 무엇을 검증했는지의 기록**이다. 그래서 스크립트는 값을 제안만 하고,
채택은 사람이 build.env 커밋한다 git diff "무엇이 왜 올라갔는지" 남는다.
심각도 기준은 게이트와 같다
--------------------------
`max(벤더 등급, NVD 등급)` 실효 등급으로 쓴다. 판정 규칙이 곳으로 갈리지 않도록
cve-gate.py 함수를 그대로 가져다 쓴다.
사용
----
python3 scripts/build/suggest-go-upgrades.py --reports <trivy-reports 디렉토리>
python3 scripts/build/suggest-go-upgrades.py --reports sbom-out/trivy-reports \\
--image argocd --min-severity HIGH
종료 코드
0 제안 출력(대상이 없으면 사실을 출력)
2 실행 오류
"""
import argparse
import glob
import importlib.util
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
GATE = os.path.join(HERE, "..", "pipeline", "cve-gate.py")
def load_gate():
"""cve-gate.py 를 모듈로 읽어 심각도 규칙을 재사용한다 — 규칙의 단일 출처를 지킨다."""
spec = importlib.util.spec_from_file_location("cve_gate", GATE)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def version_tuple(v):
"""'0.56.0' → (0,56,0). 비교용. 숫자가 아닌 꼬리는 버린다."""
nums = re.findall(r"\d+", v or "")
return tuple(int(n) for n in nums) or (0,)
def pick_fixed(raw):
"""FixedVersion 문자열에서 후보를 뽑아 최대값을 고른다.
Go 모듈은 보통 단일 값이지만 쉼표로 여러 브랜치를 나열하는 경우가 있다
(stdlib '1.24.13, 1.25.7' 형태). 모듈에 대해서는 가장 높은 값을 쓴다.
"""
cands = [c.strip() for c in re.split(r"[,/]", raw or "") if c.strip()]
cands = [c for c in cands if re.match(r"^v?\d", c)]
if not cands:
return None
return max(cands, key=version_tuple).lstrip("v")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--reports", required=True, help="trivy 리포트(JSON) 디렉토리")
ap.add_argument("--image", default="", help="리포트 파일명에 이 문자열이 든 것만 대상")
ap.add_argument("--min-severity", default="HIGH", choices=["CRITICAL", "HIGH", "MEDIUM", "LOW"],
help="이 등급 이상만 제안 (기본 HIGH — 게이트 차단 기준과 동일)")
args = ap.parse_args()
gate = load_gate()
rank = gate.RANK
floor = rank[args.min_severity]
paths = sorted(glob.glob(os.path.join(args.reports, "*.json")))
if args.image:
paths = [p for p in paths if args.image in os.path.basename(p)]
if not paths:
print(f"::error::리포트를 찾지 못했다: {args.reports} (--image={args.image or '전체'})", file=sys.stderr)
return 2
# module -> {"fixed": 최대 FixedVersion, "cves": {...}, "installed": {...}}
mods = {}
for p in paths:
with open(p) as f:
doc = json.load(f)
for res in doc.get("Results") or []:
# Go 바이너리에 정적 링크된 모듈만 대상이다. OS 패키지는 베이스 교체 영역이라
# 여기서 다루지 않는다.
if res.get("Class") != "lang-pkgs" or res.get("Type") != "gobinary":
continue
for v in res.get("Vulnerabilities") or []:
pkg = v.get("PkgName") or ""
if pkg == "stdlib":
# stdlib 은 모듈 업그레이드가 아니라 GO_BUILDER_TAG 로 해소한다.
continue
vend = v.get("Severity") or "UNKNOWN"
nvd = gate.nvd_severity(((v.get("CVSS") or {}).get("nvd") or {}).get("V3Score"))
eff = vend if rank.get(vend, 0) >= rank.get(nvd or "UNKNOWN", 0) else nvd
if rank.get(eff, 0) < floor:
continue
fx = pick_fixed(v.get("FixedVersion"))
if not fx:
continue
e = mods.setdefault(pkg, {"fixed": fx, "cves": {}, "installed": set()})
if version_tuple(fx) > version_tuple(e["fixed"]):
e["fixed"] = fx
e["cves"][v["VulnerabilityID"]] = eff
if v.get("InstalledVersion"):
e["installed"].add(v["InstalledVersion"])
# stdlib 은 별도 안내한다 — 모듈 목록이 아니라 툴체인으로 푼다.
#
# FixedVersion 의 여러 값은 "더 높은 버전"이 아니라 **브랜치별 대안**이다
# ('1.25.13, 1.26.6, 1.27.0-rc.3' = 세 브랜치 각각에서 고쳐진 지점). Go 는 보안 수정을
# 지원 브랜치에 백포트하므로, 어떤 툴체인 T 는 다음이면 그 CVE 를 해소한다:
# T 와 같은 마이너의 대안이 있고 T 의 패치가 그 이상이거나,
# 대안의 마이너가 T 보다 낮다(이미 이전 브랜치에서 고쳐져 T 에 포함됨).
# 그래서 단순히 전체 최대값을 고르면 안 된다 — 프리릴리스(1.27.0-rc.3)를 정식 1.27.0 으로
# 오독하게 된다(실측 버그).
std_alts = {} # cve -> [(major, minor, patch, is_pre), ...]
for p in paths:
with open(p) as f:
doc = json.load(f)
for res in doc.get("Results") or []:
if res.get("Type") != "gobinary":
continue
for v in res.get("Vulnerabilities") or []:
if v.get("PkgName") != "stdlib":
continue
vend = v.get("Severity") or "UNKNOWN"
nvd = gate.nvd_severity(((v.get("CVSS") or {}).get("nvd") or {}).get("V3Score"))
eff = vend if rank.get(vend, 0) >= rank.get(nvd or "UNKNOWN", 0) else nvd
if rank.get(eff, 0) < floor:
continue
alts = []
for tok in re.split(r"[,\s]+", v.get("FixedVersion") or ""):
m = re.match(r"^(\d+)\.(\d+)\.(\d+)(\S*)$", tok.strip())
if m:
a, b, c, tail = m.groups()
alts.append((int(a), int(b), int(c), bool(tail)))
if alts:
std_alts[v["VulnerabilityID"]] = alts
if std_alts:
# 정식 릴리스 대안만으로 후보 마이너를 만든다(프리릴리스는 툴체인 태그로 못 쓴다).
cand = sorted({(a, b) for alts in std_alts.values() for a, b, _, pre in alts if not pre})
rows = []
for mm in cand:
need = 0
covered = True
for cve, alts in std_alts.items():
same = [c for a, b, c, pre in alts if (a, b) == mm and not pre]
if same:
need = max(need, max(same))
elif not any((a, b) < mm for a, b, _, _ in alts):
covered = False # 이 마이너보다 높은 브랜치에서만 고쳐졌다
break
if covered:
rows.append((mm, need))
print("# stdlib 차단 CVE 가 있다 — GO_MODULE_UPGRADES 가 아니라 툴체인으로 해소한다.")
print(f"# 대상 CVE {len(std_alts)}건. 아래 중 어느 것을 써도 전부 해소된다:")
for (a, b), c in rows:
print(f"# go{a}.{b}.{c}")
if rows:
(a, b), c = rows[-1]
print(f"# GO_BUILDER_TAG={a}.{b}.{c}-trixie # 가장 높은 정식 브랜치 기준")
else:
print("# ::warning:: 정식 릴리스 대안이 없다 — 프리릴리스 툴체인이 필요할 수 있다")
print()
if not mods:
print("# Go 모듈 업그레이드 제안 없음 (해당 등급 이상 findings 이 없다)")
return 0
print(f"# {args.min_severity} 이상 Go 모듈 CVE 로부터 산출 — build.env 의 GO_MODULE_UPGRADES 에 반영한다.")
for name in sorted(mods):
e = mods[name]
inst = ", ".join(sorted(e["installed"])) or "?"
cves = ", ".join(sorted(e["cves"]))
print(f"# {name} {inst}{e['fixed']}")
print(f"# {cves}")
specs = " ".join(f"{n}@v{mods[n]['fixed']}" for n in sorted(mods))
print(f'GO_MODULE_UPGRADES="{specs}"')
print()
print("# 주의 — 이 값은 CVE 요건의 최소치다. 모듈 간 제약으로 더 올려야 할 수 있다")
print("# (실측: go-git 5.19.2 와 x/net 0.56.0 이 x/crypto 0.53.0 을 요구해 0.52.0 으로는 빌드 실패).")
print("# 빌드가 'requires ...@vX, not ...@vY' 로 실패하면 그 버전으로 올린다.")
return 0
if __name__ == "__main__":
sys.exit(main())