security-catalog 프로젝트에서 첫 실사용 자체 빌드 이미지 3종을 포팅한다 — 전부
상위 태그·베이스 OS 교체로 해소 안 되는 CVE(Go 모듈 정적 링크 또는 미수정 CRITICAL/
HIGH)를 자체 빌드(소스 컴파일 또는 SUSE BCI 재설치)로 대응한다:
- images/cloudnative-pg: CNPG operator, release-1.30 소스 컴파일 + bci-micro
- images/cnpg-postgresql: PostgreSQL 18.4, bci-base + zypper 재설치
- images/etcd: etcd v3.7.1, 소스 컴파일(x/text 강제 업그레이드) + bci-micro
함께 추가:
- manifests/helm/{cloudnative-pg,cnpg-cluster,etcd} — 위 이미지를 참조하는 카탈로그 차트
- scripts/deploy-test/*.sh, .claude/deploy-test-procedure.md — CVE 0건과 별개로
"실제로 뜨는가"를 검증하는 배포 스모크 테스트
- .claude/pitfalls.md — 자체 빌드/배포 테스트 중 실측한 함정 모음
검토 중 발견해 반영한 수정:
- cloudnative-pg 차트의 image 블록을 etcd와 동일한 registry/repository/tag 3필드+
따옴표 포맷으로 통일 — 기존 포맷(repository에 registry+repo 결합, 따옴표 없음)은
patch-catalog-tag.py 의 split 패처가 tag만 갱신하고 repository는 그대로 남기는
조용한 부분 치환을 일으켜, 향후 레지스트리 마이그레이션 시 깨진 참조를 만들 수 있었다
- cnpg-cluster 차트의 SLES 커버리지 코멘트를 최신 실측(trivy가 SLES 15.7을 정상
커버함, 2026-07-29 재측정)에 맞게 정정 — 폐기된 "측정 불가/OVAL 우회 필요" 결론이
남아있었다
- CLAUDE.md/MEMORY.md 의 "images/ 디렉토리 없음" 서술을 갱신하고, 레지스트리
마이그레이션(docker.io/wbsong111 → docker.io/paasup)·decisions/analysis 문서 이관·
리소스 프로파일 추가를 다음 작업으로 기록
이 3개 이미지는 아직 dip-catalog 자체 CI(build-image.yml)로 빌드·게이트·push 를
실행해본 적이 없다 — 현재 참조 태그는 security-catalog 쪽에서 이미 검증된 것이다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.3 KiB
CloudNativePG Operator 배포
차트 버전 0.29.0 / operator 버전 1.30.0
CloudNativePG 는 PostgreSQL 을 Kubernetes 오퍼레이터로 운영하는 CNCF 프로젝트다.
이 차트는 오퍼레이터만 설치한다. 실제 DB 클러스터는 cnpg-cluster 차트로 배포한다.
cloudnative-pg (operator) → CRD + controller + webhook
↓ 감시
cnpg-cluster (Cluster CR) → PostgreSQL primary/replica Pod, PVC, Service
1. 배포 방법
1) 배포 시 주의 사항
- CRD 와 webhook 은 cluster-scoped 리소스다. 네임스페이스를 분리해도 클러스터 전체에 하나만 존재한다. 여러 팀이 각자 오퍼레이터를 설치하면 CRD 버전이 충돌한다. 클러스터당 오퍼레이터는 1개만 둔다.
- webhook 의
failurePolicy가Fail이다. 오퍼레이터 Pod 가 없는 상태에서는Cluster리소스의 생성·수정·삭제가 모두 거부된다. 오퍼레이터를 제거할 때는 webhook 설정을 반드시 함께 삭제해야 한다 (아래 4. 제거 참고). helm upgrade는 CRD 를 갱신하지 않는다. Helm 의 CRD 처리 방식 때문에 버전 업그레이드 시 CRD 를 수동 apply 해야 한다.- Kubernetes 1.25+ 필요. 검증 환경은 RKE2 v1.34.1 이다.
2) 배포
git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog/manifests/helm/cloudnative-pg/0.29.0
helm upgrade cnpg ./ -f custom-values.yaml --install -n cnpg-system --create-namespace --wait
3) 확인
kubectl -n cnpg-system get pods
kubectl get crd | grep cnpg # 11개
kubectl get cluster는 쓰지 않는다. Rancher(clusters.management.cattle.io) 와 CAPI(clusters.cluster.x-k8s.io) 가 같은 단축명을 쓰기 때문에 엉뚱한 리소스가 조회된다. 반드시kubectl get clusters.postgresql.cnpg.io로 FQN 을 쓴다.
2. custom-values.yaml 설명
1) 이미지 설정
| Name | 설명 | 기본값 |
|---|---|---|
image.repository |
오퍼레이터 이미지. 오프라인 환경에서는 사내 미러 경로로 변경 | ghcr.io/cloudnative-pg/cloudnative-pg |
image.tag |
미설정 시 차트 appVersion(1.30.0) 사용. 버전 변경은 차트 교체를 우선한다 |
"" |
imagePullSecrets |
사설 레지스트리 인증 시크릿 | [] |
2) 감시 범위 (RBAC 영향)
| Name | 설명 | 기본값 |
|---|---|---|
config.clusterWide |
true = ClusterRole 로 전체 네임스페이스 감시. false = 설치 네임스페이스만 감시하고 RBAC 이 Role 로 축소됨 |
true |
config.data.WATCH_NAMESPACE |
clusterWide: true 상태에서 감시 대상을 특정 네임스페이스로 한정 (쉼표 구분) |
미설정 |
config.maxConcurrentReconciles |
동시 reconcile 수 | 10 |
보안 관점 — 실측 RBAC 비교 (operator 1.30.0)
clusterWide 는 감시 범위와 RBAC 범위를 함께 바꾼다. 실제로 렌더링해 측정한 결과다.
clusterWide: true |
clusterWide: false |
|
|---|---|---|
| ClusterRole 규칙 수 | 다수 (전 리소스) | 3개 |
| ClusterRole 이 다루는 리소스 | secrets, pods, pods/exec, serviceaccounts, roles, rolebindings, deployments, configmaps, PVC, webhook 설정, nodes … |
nodes(RO), clusterimagecatalogs(RO), webhook 설정(get/patch) |
| 네임스페이스 Role | 없음 | 생성됨 (20 규칙, 설치 NS 한정) |
| ClusterRoleBinding | 생성됨 | 생성됨 (축소된 ClusterRole 에 바인딩) |
clusterWide: true 의 실제 위험도: 오퍼레이터 ServiceAccount 가 클러스터 전체에 대해
다음을 갖는다.
secrets전체 CRUD → 모든 네임스페이스의 모든 시크릿 열람 (Harbor·Keycloak·Infisical 토큰 포함)pods/exec→ 임의 네임스페이스의 임의 Pod 에 execroles/rolebindings생성 → 권한 상승 경로serviceaccounts,deployments조작mutatingwebhookconfigurations/validatingwebhookconfigurationspatch → 어드미션 제어 변경
이 조합은 실질적으로 cluster-admin 에 준한다. 오퍼레이터 Pod 가 침해되면 클러스터 전체가 침해된다고 봐야 한다.
WATCH_NAMESPACE 는 보안 경계가 아니다. 이 값은 오퍼레이터가 reconcile 할 대상만
좁힌다. RBAC 은 그대로 cluster-wide 로 남으므로 토큰의 권한은 줄어들지 않는다.
심층 방어(defense-in-depth) 수단일 뿐, 권한 축소로 오해하면 안 된다.
권고
- 보안이 우선이면
clusterWide: false— 테넌트 네임스페이스마다 오퍼레이터를 따로 설치한다. 다만 CRD 와 webhook 설정은 cluster-scoped 싱글턴이므로 모든 오퍼레이터의 버전이 같아야 하고, 각 설치가 동일한 webhook 설정을 patch 하려고 경쟁한다. 운영 복잡도가 크게 올라간다. - 운영 편의가 우선이면
clusterWide: true— 단, 위 권한을 감수하는 결정임을 명시하고 다음 보완책을 함께 적용한다.- 오퍼레이터 네임스페이스에 접근 가능한 주체를 최소화 (
cnpg-system을 별도 관리) - 오퍼레이터 Pod 의 exec/attach 를 Kyverno 등으로 차단
- 감사 로그에서 오퍼레이터 SA 의
secrets접근을 모니터링 clusterimagecatalogs로 허용 이미지를 고정해 임의 이미지 기동을 막는다
- 오퍼레이터 네임스페이스에 접근 가능한 주체를 최소화 (
배포 테스트에서는 격리를 위해 clusterWide: false 를 사용했다.
custom-values.yaml 기본값은 업스트림과 동일한 true 이므로, 도입 시 이 결정을 반드시
검토해야 한다.
3) Webhook
| Name | 설명 | 기본값 |
|---|---|---|
webhook.port |
webhook 서비스 포트 | 9443 |
webhook.mutating.create |
mutating webhook 생성 여부 | true |
webhook.validating.create |
validating webhook 생성 여부 | true |
webhook.*.failurePolicy |
Fail 유지 권장. Ignore 로 바꾸면 검증 없이 잘못된 Cluster 스펙이 통과된다 |
Fail |
4) 모니터링
| Name | 설명 | 기본값 |
|---|---|---|
monitoring.podMonitorEnabled |
Prometheus Operator CRD 필요. 없으면 배포 실패 | false |
monitoring.grafanaDashboard.create |
Grafana 대시보드 ConfigMap 생성 | false |
5) 리소스
| Name | 설명 | 기본값 |
|---|---|---|
resources |
오퍼레이터 Pod 의 cpu/memory. 티어별 값은 dip-resources-quotas.yaml 참고 |
업스트림은 {} (무제한) |
replicaCount |
leader election 기반이라 2 이상은 가용성 목적 (reconcile 은 리더 1개가 수행) | 1 |
3. 업그레이드
# 1) CRD 를 먼저 수동 갱신 (helm upgrade 는 CRD 를 건드리지 않음)
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/v1.30.0/releases/cnpg-1.30.0.yaml \
--dry-run=server # 먼저 dry-run 으로 영향 확인
# 2) 차트 업그레이드
helm upgrade cnpg ./ -f custom-values.yaml -n cnpg-system --wait
오퍼레이터 업그레이드는 실행 중인 Cluster 의 인스턴스를 롤링 재시작시킨다.
primaryUpdateStrategy 설정에 따라 primary 전환이 발생하므로 서비스 영향 시간을 고려해야 한다.
4. 제거
순서가 중요하다. webhook 이 failurePolicy: Fail 이므로 오퍼레이터를 먼저 지우면
Cluster 리소스를 삭제할 수 없게 된다.
# 1) 먼저 모든 Cluster 리소스 삭제
kubectl get clusters.postgresql.cnpg.io -A
kubectl -n <ns> delete clusters.postgresql.cnpg.io <name>
# 2) 오퍼레이터 제거
helm uninstall cnpg -n cnpg-system
# 3) cluster-scoped 잔여물 제거 (helm uninstall 로 남는다)
kubectl delete validatingwebhookconfiguration cnpg-validating-webhook-configuration --ignore-not-found
kubectl delete mutatingwebhookconfiguration cnpg-mutating-webhook-configuration --ignore-not-found
kubectl get crd -o name | grep cnpg.io | xargs -r kubectl delete
CRD 삭제는 해당 CRD 의 모든 리소스를 삭제한다. PVC 는 남지만
Cluster정의는 사라지므로 운영 클러스터에서는 3단계를 실행하기 전에 반드시 백업을 확인한다.
5. 검증 이력
doc/charts/cnpg/deploy-test.md 참고. RKE2 v1.34.1 / Longhorn 환경에서 3-instance 구성,
failover 3초, 데이터 정합성 유지를 확인했다.