Skip to content

Releases: hkjang/clustara

v0.9.281

Choose a tag to compare

@hkjang hkjang released this 10 Sep 08:49

다른 클러스터의 worker-1 이 이 클러스터의 용량으로 계산됐습니다

/admin/k8s/capacitycluster_id선택 파라미터입니다. 전 클러스터 보기에서는 인벤토리와 메트릭이 여러 클러스터를 한 번에 담는데, 이 리포트의 교차 참조는 전부 이름만 맞췄습니다. 노드 이름(worker-1 …)과 namespace/name 은 클러스터를 건너 반복되므로 같은 이름이 같은 머신·같은 워크로드라는 뜻이 아닙니다. 같은 패키지의 node_monitoring 은 이미 nodeKey(cluster, node) 로 같은 조인을 하고 있었습니다 — 즉 제품의 두 화면이 같은 노드를 서로 다르게 계산하고 있었습니다. 이번 릴리즈는 SCALE-03/04/05/07/08 의 교차 참조 결함 5종을 고칩니다. 표에 있어야 할 노드가 사라졌고, 정상 크기의 워크로드에 비용 절감 딱지가 붙었으며, 실재하지 않는 노드의 고갈 예측이 그려지고 있었습니다.

① 노드 두 개가 한 행으로 합쳐지고, 다른 하나는 표에서 사라졌습니다

SCALE-07 노드 패킹과 SCALE-08 GPU 표는 노드를 이름으로만 키잉했습니다. 그래서 두 클러스터의 동명 노드가 한 행을 공유했고, 그 행의 allocatable 은 인벤토리에서 마지막에 온 클러스터 것이 남았습니다. 그 위에서 .spec.nodeName 매칭이 이름만 보고 두 클러스터의 Pod 를 모두 그 한 행에 더했습니다.

각각 1000m 과 5000m 을 쓰는 4코어 노드 두 개가 "75% 패킹된 노드 하나" 로 보고됐습니다. 실제로는 과소 사용 중인 노드 하나와 초과 커밋된 노드 하나입니다. 다른 클러스터의 노드는 표에서 통째로 사라졌고, GPU 표에서도 마찬가지였습니다.

이제 노드는 기존 nodeKey(클러스터 + 이름)로 키잉하고, Pod 는 자신의 클러스터에 있는 노드에만 더해집니다.

② 정상 크기의 Pod 가 비용 절감 후보로 올라왔습니다

SCALE-03/04 는 Pod 별 최신 메트릭 샘플을 namespace/name 키의 맵으로 모읍니다. newest-first 중복 제거는 한 클러스터의 샘플 하나만 남기고, 그 값이 동명 Pod 전부의 request 와 비교됐습니다.

request 1000m 중 600m 을 쓰는 Pod — 정상적으로 사이징된 워크로드 — 가 다른 클러스터의 동명 Pod 가 250m 를 쓴다는 이유로 over_provisioned(비용 절감 후보)로 보고됐습니다. 반대 방향으로도 같은 일이 일어났습니다. 이제 Pod 메트릭은 클러스터를 포함한 podMetricKey 로 키잉합니다.

③ 어느 실제 노드도 설명하지 않는 days-to-full 예측

SCALE-05 노드 예측은 두 클러스터의 샘플을 한 추세선에 섞었습니다. 추세를 잡는 가장 오래된 샘플과 가장 최신 샘플이 서로 다른 머신의 것일 수 있었고, 나누는 데 쓰는 allocatable 은 마지막에 본 클러스터 노드의 값이었습니다. 그렇게 나온 고갈 예측일은 실재하는 어떤 노드에도 해당하지 않았습니다. 이제 예측은 클러스터별 노드마다 따로 계산됩니다.

④ 행이 어느 클러스터인지 말합니다

용량 리포트의 다섯 결과 타입에 cluster_idomitempty 로 추가됐습니다 — v0.9.278 에서 SecFinding 에 적용한 것과 같은 방식입니다. 전 클러스터 보기에서 동명 노드가 두 행으로 정상 분리되므로, 행이 자신의 클러스터를 말할 수 있어야 합니다.

어드민 UI 도 같이 맞췄습니다. 노드 표는 packing 을 이름만으로 조인하고 있었고(바로 옆의 Pod 조인은 이미 클러스터를 넘기고 있었습니다), 용량 페이지의 YAML 딥링크는 전 클러스터 보기에서 빈 cluster_id 를 써 엉뚱한 리소스를 열 수 있었습니다. 둘 다 이제 행 자신의 클러스터를 씁니다.

⑤ 노드 표의 순서가 요청마다 달랐습니다

두 노드 표가 map 순회로 만들어져 같은 데이터에도 요청마다 순서가 바뀌었습니다. 이제 cluster → node 순으로 정렬됩니다.

검증

신규 테스트 5개를 고치기 전 키잉으로 되돌려 붙여 각 결함을 지목하며 실패하는 것을 확인했습니다 — 합쳐진 단일 노드 행, 사라진 GPU 노드, 잘못된 over_provisioned, 섞인 예측, 세 번 실행 모두 다른 순서. go build ./..., go vet ./..., go test ./... 전부 통과(20 패키지).

배포 파일

파일 설명
clustara-v0.9.281.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.281.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.281.md 오프라인 배포 가이드

빠른 시작

# 무결성 확인
sha256sum -c clustara-v0.9.281.tar.gz.sha256

# 이미지 로드
gunzip -c clustara-v0.9.281.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.281

v0.9.280

Choose a tag to compare

@hkjang hkjang released this 10 Sep 00:11

Pod 에 runAsUser: 0 을 적고 컨테이너는 비워 둔 워크로드가 "root 아님" 으로 보고됐습니다

spec.securityContext.runAsUser: 0Pod 레벨에 적고 컨테이너는 아무것도 적지 않는 것이 워크로드를 root 로 돌리는 가장 흔한 형태입니다 — Pod 값은 덮어쓰지 않은 모든 컨테이너의 기본값이기 때문입니다. 그런데 세 보안 화면이 컨테이너 securityContext 만 읽어 그 워크로드를 root 아님으로 보고했습니다. 이번 릴리즈는 그 판정 결함 4종을 고칩니다. 있는 위험이 보안 화면에서 사라졌고, 없는 위험 하나는 정상 구성에 붙었으며, 제품의 두 부분이 같은 Pod 를 두고 반대 판정을 내놓고 있었습니다.

① SEC-01 Pod Security 가 runAsUser=0 위반을 아예 만들지 않았습니다

classifyPodSecurity 는 컨테이너의 securityContext.runAsUser 만 봤습니다. 그래서 Pod 레벨에 0 을 적고 컨테이너는 선언하지 않은 — 즉 모든 컨테이너가 실제로 UID 0 으로 뜨는 — Pod 에 baseline 위반이 하나도 붙지 않았습니다.

이제 컨테이너 → Pod 우선순위로 실제 UID 를 해석해 상속된 root 도 위반으로 적고, 상속인 경우 사유에 runAsUser=0 (Pod securityContext 상속) 으로 출처를 밝힙니다.

② 반대 방향: 컨테이너가 실제 UID 로 덮어쓴 Pod 가 root 로 채점됐습니다

Runtime Security Profile(CLU-OCP-03)의 podSecurityInput 은 Pod 레벨을 읽기는 했지만 무조건 적용했습니다. 그래서 Pod 가 0 을 적어도 컨테이너가 non-root UID 로 덮어쓴 정상 구성 — 실제로는 root 로 돌지 않는 워크로드 — 이 root 로 올라왔습니다.

이제 우선순위가 양방향으로 적용됩니다. 명시적 null 은 미설정으로 봅니다: 이미지의 USER 가 적용되므로 spec 은 어느 쪽도 주장하지 않습니다.

③ 지금 붙어 있는 privileged 디버그 컨테이너가 "위험 설정 없음" 으로 채점됐습니다

같은 화면이 containers 만 순회했습니다. 그래서 privileged init 컨테이너, 그 컨테이너가 추가한 capability, 그리고 디버그 세션이 방금 붙인 privileged ephemeral 컨테이너가 전부 채점에서 빠졌습니다. 같은 Pod 를 정책 엔진과 SEC-01 은 이미 위반으로 적고 있었으므로, 제품의 두 부분이 한 Pod 를 두고 반대 판정을 내놓고 있었습니다.

이제 포스처·정책 엔진이 이미 쓰던 analyzer.SecurityRelevantContainers(regular + init + ephemeral)를 순회합니다.

④ Workspace 건강도가 런타임 보안 화면과 같은 Pod 를 두고 다르게 답했습니다

podHasRuntimeSecurityRisk 는 Pod 레벨 securityContext 를 통째로 무시하고 역시 containers 만 봤습니다. 그래서 두 화면이 같은 Pod 에 서로 다른 답을 내놓았습니다.

판정을 한 곳으로 모았습니다 — analyzer.EffectiveRunAsUser 가 컨테이너 → Pod 우선순위를 양방향으로 적용하고, analyzer.PodRunsAsRoot 가 세 호출자 모두에게 한 번만 답합니다.

검증

신규 테스트 8개 중 6개를 고치기 전 코드에 되돌려 붙여 각 결함을 지목하며 실패하는 것을 확인했습니다. 나머지 2개는 컨테이너가 Pod 의 0 을 덮어쓴 경우와 Pod 의 non-root 를 컨테이너가 덮어쓴 경우를 지키는 오탐 회귀입니다. go build ./..., go vet ./..., go test ./... 전부 통과(20 패키지).

배포 파일

파일 설명
clustara-v0.9.280.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.280.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.280.md 오프라인 배포 가이드

빠른 시작

# 무결성 확인
sha256sum -c clustara-v0.9.280.tar.gz.sha256

# 이미지 로드
gunzip -c clustara-v0.9.280.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  -e GATEWAY_SECRET=$(openssl rand -hex 32) \
  clustara:v0.9.280

v0.9.279

Choose a tag to compare

@hkjang hkjang released this 09 Sep 04:30

레지스트리 포트가 태그로 읽혀, 태그 없는 이미지가 "고정됨" 으로 통과했습니다

하나의 이미지 참조를 네 곳이 읽습니다 — SEC-02 포스처(이미지 태그 정책), SEC-10 정책 게이트, Dockerfile 빌드 게이트, 이미지 사용 인벤토리·원장. 그런데 올바른 파서를 쓰는 곳은 정책 게이트 하나뿐이었습니다. 이번 릴리즈는 그 파싱 결함 다섯 지점을 고칩니다. 있는 위험이 포스처 리포트에서 사라졌고, 없는 위험은 정상 구성에 붙었습니다.

registry.corp.local:5000/team/app 이 태그가 없는데도 고정된 것으로 통과했습니다

imageTagAndDigest마지막 / 뒤에 오는 : 태그로 읽습니다. 그런데 SEC-02 imageFindings 와 Dockerfile 게이트는 문자열에 : 가 있는지만 보는 substring 검사였습니다. 그래서 registry.corp.local:5000/team/app — 태그가 없으므로 풀 시점에 :latest 가 되는 참조 — 가 레지스트리 포트의 콜론 덕분에 고정된 것으로 통과했습니다.

포트를 명시한 사설 레지스트리는 이 제품이 겨냥하는 폐쇄망의 기본형입니다. 그 결과 같은 이미지를 두고 포스처 리포트는 "정상", 정책 게이트는 "위반" 이라고 서로 다르게 판정하고 있었습니다.

이제 네 곳 모두 정책 게이트와 같은 파서를 씁니다. 같은 레지스트리 포트 뒤에 있는 태그 있는 참조와 digest 고정 참조는 종전대로 깨끗하게 남습니다.

FROM --platform=linux/amd64 base:1.0 에서 진짜 base 가 검사되지 않았습니다

Dockerfile 게이트는 FROM [--flag...] <ref> [AS <stage>] 에서 Fields()[1] 을 이미지 참조로 썼습니다. 그래서 플래그가 붙으면 "--platform=linux/amd64" 라는 이름의 이미지가 태그 미고정 이라고 보고되고, 정작 검사해야 할 base:1.0 은 아예 검사되지 않았습니다.

같은 이유로 반대 방향의 오탐도 있었습니다 — 앞선 stage 를 가리키는 FROM build 는 고정할 태그가 존재하지 않는데도 mutable base 로 잡혔습니다. 이제 플래그를 문법대로 건너뛰고, stage 이름은 base 이미지로 보지 않습니다.

③ 고정되지 않은 debug 이미지를 돌리는 Pod 가 "고정된 이미지만 쓴다" 로 표시됐습니다

이미지 사용 인벤토리가 ephemeralContainers 를 건너뛰었습니다. ExtractImages 는 정책 룰이 debug 이미지를 봐야 한다고 이미 주석으로 명시하고 있었는데, 인벤토리 쪽만 빠져 있었습니다. 이제 포함합니다.

④ 미러 레지스트리를 두면 멀쩡한 두 이미지가 "태그가 옮겨졌다" 로 보고됐습니다

원장의 tag drift 키가 registry 를 빼고 repository 만 썼습니다. 그래서 harbor.corp/app:1.0docker.io/app:1.0한 항목으로 합쳐지고, 서로 다른 두 digest 가 "같은 태그가 다른 digest 를 가리킨다" 로 잡혔습니다. 업스트림 옆에 미러 레지스트리를 두는 것은 정확히 이 제품이 보는 폐쇄망 구성입니다.

이제 registry 를 포함한 참조로 묶습니다. 겸사겸사 init 컨테이너 이미지도 spec·status 양쪽에서 원장에 담습니다.

검증

신규 테스트 4개를 고치기 전 코드에 되돌려 붙여 네 개가 각 결함을 지목하며 실패하는 것을 확인했습니다. 여기에는 같은 레지스트리 포트 뒤의 태그 있는·digest 고정 참조가 계속 깨끗한지 지키는 오탐 회귀와, 포스처와 정책 게이트의 판정이 일치하는지 대조하는 검사가 포함됩니다. go build ./..., go vet ./..., go test ./... 전부 통과(20 패키지).

배포 파일

파일 설명
clustara-v0.9.279.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.279.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.279.md 오프라인 배포 가이드

빠른 시작

# 무결성 확인
sha256sum -c clustara-v0.9.279.tar.gz.sha256

# 이미지 로드
gunzip -c clustara-v0.9.279.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.279

v0.9.278

Choose a tag to compare

@hkjang hkjang released this 08 Sep 20:02

다른 클러스터의 prod 가 이 클러스터의 검사를 대신 통과시켰습니다

/admin/k8s/connectivity/admin/k8s/securitycluster_id선택 파라미터입니다. 전 클러스터 보기에서는 인벤토리와 이벤트가 여러 클러스터를 한 번에 담는데, 이 두 분석의 교차 참조는 전부 네임스페이스 이름만 맞췄습니다. 네임스페이스 이름은 클러스터를 건너면 같은 네임스페이스가 아닙니다 — 그래서 다른 클러스터의 동명 네임스페이스가 이쪽 검사를 대신 통과시키고 있었습니다. 이번 릴리즈는 그 교차 참조 결함 다섯 지점과, 같은 함수에서 발견된 endpoint 판정 결함 하나를 고칩니다. 있는 위험이 리포트에서 사라졌고, 없는 위험 하나는 정상 구성에 붙었습니다.

① 비어 있는 Service 가 목록에서 사라졌습니다

analyzeServices 는 Service 의 selector 를 네임스페이스만 맞춰 Pod 와 대조했습니다. 그래서 dr 클러스터의 prod/api Pod 가 prod 클러스터 prod/api Service 의 endpoint 로 계산됐고, 실제로 트래픽을 받을 Pod 가 하나도 없는 Service 가 전 클러스터 보기에서 정상으로 넘어갔습니다. 이제 Pod 를 클러스터+네임스페이스로 묶어 자기 클러스터 안에서만 찾습니다.

② 존재하지 않는 Ingress backend 가 조용히 통과했습니다

Ingress backend 조회도 같은 네임스페이스-이름 대조였습니다. 이 클러스터에 없는 backend Service 를 가리키는 Ingress 가, 다른 클러스터에 같은 이름의 Service 가 있다는 이유로 IngressBackendMissing 없이 지나갔습니다. backend 도 소유 클러스터 안에서만 찾습니다.

③ 액티브/스탠바이 DR 구성이 라우팅 충돌로 잡혔습니다

IngressDuplicateHost 는 반대 방향의 오탐이었습니다. 두 클러스터의 Ingress 가 같은 host 를 서비스하는 것은 DR 구성의 정상 형태지 충돌이 아닌데, 중복 host 로 보고됐습니다. 이제 host 는 클러스터 안에서만 비교합니다.

④ A 클러스터의 이벤트가 B 클러스터 PVC 의 증적으로 인용됐습니다

이벤트 피드도 전 클러스터를 담습니다. PVC Pending 증적이 네임스페이스 이름만 맞췄기 때문에, 다른 클러스터의 같은 네임스페이스에서 난 프로비저닝 오류가 이 청구의 근거로 그대로 붙었습니다. 이제 같은 클러스터의 이벤트만 증적이 됩니다.

⑤ 보호되지 않은 네임스페이스가 SEC-06 리포트에서 통째로 빠졌습니다 (fail-open)

영향이 가장 큰 지점입니다. NetworkPolicy 공백 점검(SEC-06)은 "네임스페이스 집합 − 정책이 있는 네임스페이스 집합" 이라는 차집합이라, 한 클러스터의 prod 에 NetworkPolicy 가 하나 있으면 다른 모든 클러스터의 prod 가 덮였습니다. 정책이 하나도 없는 네임스페이스가 리포트에 아예 나타나지 않았다는 뜻입니다.

이제 클러스터별로 판정합니다. 어느 클러스터인지 알 수 있도록 SecFindingcluster_id 를 담고(빈 값은 omitempty 라 기존 응답 모양은 그대로), map 순회라 실행마다 달라지던 findings 순서를 정렬로 고정했습니다.

⑥ 완료된 Job Pod 만 남은 Service 가 "endpoint 있음" 이었습니다

같은 함수의 endpoint 판정이 종료된 Pod 까지 셌습니다. API 서버는 Succeeded/Failed Pod 를 GC 전까지 인벤토리에 남기지만 endpoints 컨트롤러는 그 Pod 를 뺍니다. 그래서 완료된 Job Pod 만 남은 Service 가 정상으로 통과했습니다. 아직 정상이 아닐 뿐인 Pod(Pending·CrashLoopBackOff)는 not-ready 주소로 게시되므로 종전대로 셉니다.

검증

신규 테스트 6개 중 5개를 고치기 전 코드에 되돌려 붙여 각 결함을 지목하며 실패하는 것을 확인했고, 6번째는 정상이 아닐 뿐인 Pod 가 계속 endpoint 로 남는지 지키는 오탐 회귀입니다. go build ./..., go vet ./..., go test ./... 전부 통과(20 패키지).

배포 파일

파일 설명
clustara-v0.9.278.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.278.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.278.md 오프라인 배포 가이드

빠른 시작

# 무결성 확인
sha256sum -c clustara-v0.9.278.tar.gz.sha256

# 이미지 로드
gunzip -c clustara-v0.9.278.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  -e GATEWAY_SECRET=$(openssl rand -hex 32) \
  clustara:v0.9.278

v0.9.277

Choose a tag to compare

@hkjang hkjang released this 08 Sep 14:21

와일드카드 인증서로 정상 서비스되는 Ingress 가 "평문 노출" 로 보고됐습니다

같은 Ingress 를 두 화면이 서로 다르게, 그리고 둘 다 틀리게 읽고 있었습니다 — 외부 노출 점검(Exposure Center)과 Ingress/PVC 연결성 점검(K8S-23/24)입니다. 이번 릴리즈는 그 오탐·집계 결함 다섯 지점을 고칩니다. 없는 위험이 목록 맨 앞에 올라왔고, 진짜 위험 하나는 아예 검사되지 않았습니다.

*.example.com 인증서를 쓰는 host 가 "TLS 미적용(평문 노출)" +30점이었습니다

analyzer.AnalyzeExposure 는 host 가 TLS 로 덮이는지를 spec.tls[].hosts문자열 그대로 대조했습니다. 그런데 Kubernetes 는 이 필드에 와일드카드를 허용하고, *.example.com 은 DNS 라벨 하나를 덮습니다. 그래서 와일드카드 인증서로 정상 서비스되는 app.example.com 이 평문 노출로 채점돼, 노출 위험 목록에서 진짜 평문 Ingress 와 나란히 올라왔습니다.

이제 라벨 하나를 덮는 규칙으로 대조하고, DNS 이름이므로 대소문자를 무시합니다. a.b.example.com*.example.com 으로 덮이지 않으므로 종전대로 미커버로 남습니다(오탐 회귀 테스트로 고정).

② 와일드카드 host 3개짜리 Ingress 하나가 Total=1 인데 Wildcard=3 을 만들었습니다

SummarizeExposurePlaintext·Wildcard 가 finding 이 아니라 RiskReasons 를 셌습니다. host 를 여러 개 가진 Ingress 하나가 요약에서는 여러 건으로 잡혀, 총계와 내역이 서로 맞지 않았습니다. 이제 finding(Ingress) 개수를 셉니다.

같이 끊은 결합이 하나 더 있습니다 — 집계가 사유 문구를 문자열로 찾고 있었기 때문에, 문구를 다듬는 순간 집계가 조용히 0이 됐습니다. 사유 문자열을 상수로 묶어 그 경로를 없앴습니다.

③ 하나의 Ingress 가 자기 자신과 host 충돌로 잡혔습니다

IngressDuplicateHost 가 rule 단위로 owners 에 append 해서, 같은 host 아래 path 를 rule 두 개로 나눠 적은 하나의 Ingress 가 중복 host 로 보고됐습니다(ingresses: default/solo, default/solo). 이제 host 를 Ingress 단위로 한 번만 셉니다.

겸사겸사 이 finding 만 cluster_id 가 비어 있던 것과, map 순회라 실행마다 findings 순서가 달라지던 것을 고쳤습니다(host 정렬).

④ 일치하지 않는 모든 요청을 받는 spec.defaultBackend 를 아무도 검사하지 않았습니다

IngressBackendMissing 은 rule 의 backend 만 봤습니다. rule 에 일치하지 않는 모든 요청이 도달하는 defaultBackend 가 존재하지 않는 Service 를 가리켜도 검사를 통과했고, 노출 분석의 TargetServices 에서도 빠져 있었습니다. 이제 둘 다 defaultBackend 를 포함합니다.

또 같은 Service 를 여러 path 가 참조하면 동일한 finding 이 그 수만큼 반복돼 응답의 count 를 부풀렸습니다 — 이제 Service 당 한 번만 보고합니다. 존재하는 defaultBackend 는 깨끗하게 통과하는지도 회귀 테스트로 고정했습니다.

⑤ A 의 증적에 B 의 ProvisioningFailed 가 그대로 붙었습니다

PVC Pending 의 증적 조건이 (볼륨 실패 reason) 또는 (message 에 이름 포함) 이었습니다. 네임스페이스에 PVC 가 여러 개면 다른 PVC 가 낸 프로비저닝 실패가 이 PVC 의 근거로 인용됐습니다.

이제 이벤트의 involved object 로 대상을 확인합니다 — PVC 자신에 기록되는 provisioning/binding 이벤트와, 청구 이름을 message 에 적는 Pod 의 mount/attach 실패만 붙습니다.

검증

신규 테스트 11개(analyzer 10 + proxy 1) 중 9개를 고치기 전 코드에 되돌려 붙여 각 결함을 지목하며 실패하는 것을 확인했습니다. 나머지 2개는 와일드카드가 깊은 host 를 덮지 않는지·존재하는 defaultBackend 가 깨끗한지 지키는 오탐 회귀입니다. go build ./..., go vet ./..., go test ./... 전부 통과.

배포 파일

파일 설명
clustara-v0.9.277.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.277.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.277.md 오프라인 배포 가이드

빠른 시작

# 무결성 확인
sha256sum -c clustara-v0.9.277.tar.gz.sha256

# 이미지 로드
gunzip -c clustara-v0.9.277.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.277

v0.9.276

Choose a tag to compare

@hkjang hkjang released this 08 Sep 09:10

6시간 전에 만료된 인증서가 "유효" 로 보고됐습니다

analyzer.AnalyzeTLSkubernetes.io/tls Secret 의 공개 인증서를 읽어 만료를 판정하고, 그 결과가 보안 화면의 SEC-07 표와 멀티 클러스터 보안 posture 에 그대로 쓰입니다. 이번 릴리즈는 그 판정이 틀려 있던 다섯 지점을 고칩니다. 방향은 양쪽입니다 — 이미 못 쓰는 인증서가 정상으로 보였고, 멀쩡한 인증서는 전부 "만료 임박" 으로 집계됐습니다.

① 24시간 이내에 만료된 인증서가 critical 이 아니라 high + "0일 후 만료" 였습니다

남은 일수를 int(dur.Hours()/24) 로 계산했습니다. Go 의 int() 변환은 0 쪽으로 잘라내므로, 만료까지 20시간 남은 인증서도 6시간 전에 만료된 인증서도 똑같이 daysLeft = 0 이 됩니다. 등급 판정은 그 잘라낸 값을 보기 때문에 daysLeft < 0critical 에 걸리지 않고 high 로 떨어졌고, UI 는 그 값을 "0일 후 만료" 로 그렸습니다 — 장애가 이미 시작된 그 구간이 정확히 여기인데, 화면에는 아직 하루치 여유가 남은 것처럼 보였습니다.

이제 남은 일수는 −∞ 방향으로 내림하고(만료된 인증서는 음수 일수), 만료 여부는 잘라낸 일수가 아니라 실제 시각으로 판정합니다. 만료 경과는 하루 미만이면 시/분 단위로 적습니다.

② 체인 쪽이 먼저 만료된 Secret 이 "유효 (300일 남음)" 이었습니다

tls.crt 는 leaf 인증서 하나가 아니라 leaf + 발급 체인 번들인데, parseFirstCert 가 첫 인증서만 파싱했습니다. 이 제품이 겨냥하는 폐쇄망의 내부 CA 처럼 체인 쪽 유효기간이 leaf 보다 먼저 끝나는 구성에서는 Secret 이 이미 handshake 를 하지 못하는데, 리포트는 leaf 의 남은 일수를 그대로 말했습니다.

이제 번들에서 가장 먼저 만료되는 인증서로 만료를 판정합니다. CN/SAN 은 운영자가 식별하는 값이므로 leaf 것을 유지하고, 체인 쪽이 먼저 만료되는 경우에는 메시지에 그 인증서의 CN 을 함께 적습니다.

③ 아직 유효하지 않은 인증서가 만료 인증서와 같은 이유로 실패하면서 "유효" 로 남았습니다

notBefore 를 아무도 읽지 않았습니다. 발급 시각이 미래이거나 클러스터 시계가 어긋난 인증서는 만료된 인증서와 똑같이 handshake 를 실패시키는 상태인데, 점검은 notAfter 만 보고 정상으로 보고했습니다. 이제 아직 유효하지 않은 인증서를 별도로 보고합니다.

④ 파싱 실패한 Secret 이 인증서가 전부 정상인 클러스터와 구분되지 않았습니다

tls.crt 를 인증서로 읽지 못하면 continue 로 조용히 건너뛰었습니다. 그래서 "읽을 수 없는 Secret 만 있는 클러스터" 와 "인증서가 전부 정상인 클러스터" 가 둘 다 finding 0건으로 같아 보였습니다. v0.9.274 가 취약점 스캔 import 에서 정한 원칙 — 읽히지 않은 아티팩트를 깨끗함으로 저장하지 않는다 — 과 같은 문제이며, 이제 확인 불가로 보고합니다.

⑤ 모든 인증서가 1년 남은 클러스터에도 "TLS 인증서 갱신 일정 확인" 이 붙었습니다

posture 롤업이 TLSExpiring: len(tls) 였습니다. TLS Secret 이 하나라도 있으면 전부 만료 임박으로 집계됐다는 뜻이고, 권고 사다리가 TLSExpiring > 0 을 조건으로 쓰기 때문에 인증서가 모두 넉넉한 클러스터에도 갱신 권고가 붙어 "현재 주요 보안 조치 없음" 상태가 사실상 도달 불가였습니다.

이제 analyzer.TLSAttentionCounts조치가 필요한 건수만 세고, 지금 사용할 수 없는 인증서(만료됨 · 아직 유효 전)는 tls_expired 로 분리해 권고 상단과 needs_attention 에 반영합니다.

덤: 목록 정렬

UI 는 리포트를 입력 순서 그대로 그립니다. 리포트를 심각도·남은 일수 순으로 정렬해 만료된 인증서가 목록 맨 앞에 오도록 했습니다.

검증

신규 테스트 5개를 추가했습니다. 앞 4개를 고치기 전 코드에 되돌려 붙여 각 결함을 지목하며 실패하는 것을 확인했습니다(각각 high/"0일 후 만료", low/"유효 (400일 남음)", low/300일, finding 0건). 5번째는 새 집계(TLSAttentionCounts)와 정렬을 지킵니다. go build ./..., go vet ./..., go test ./... 전부 통과(19 패키지).

배포 파일

파일 설명
clustara-v0.9.276.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.276.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.276.md 오프라인 배포 가이드

빠른 시작

# 이미지 로드
gunzip -c clustara-v0.9.276.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.276

v0.9.275

Choose a tag to compare

@hkjang hkjang released this 08 Sep 01:56

r"m" -rf / 라고 적힌 루트 삭제가 모든 게이트를 통과하고 실행됐습니다

하나의 명령 문자열을 두 파서가 읽습니다 — 게이트(Command Risk Parser·터미널 denylist·access mode 분류기)와 Kubernetes exec argv 를 조립하는 executor 입니다. 게이트는 원본 바이트를 보면서 토큰의 바깥쪽 따옴표만 떼어냈고, executor 는 따옴표·백슬래시를 제대로 해석해 실제 프로그램을 실행했습니다. 그래서 셸에서 흔한 표기 하나로 프로그램 이름이 모든 게이트에서 동시에 가려졌습니다. 이번 릴리즈는 그 불일치 다섯 지점을 고칩니다.

critical 이어야 할 명령이 승인조차 필요 없는 low 로 채점됐습니다

r"m" -rf /, \rm -rf /, re"boot", shut'down' -h now, mkfs".ext4" /dev/sda 가 전부 critical 이 아니라 low 로 채점됐습니다. critical 은 정책을 한 줄도 읽기 전에 걸리는 하드 블록(실행 핸들러에서 한 번 더)이고 low 는 승인이 필요 없는 read_only 티어입니다. 즉 첫 토큰을 받아주는 allowlist 하나만 있으면 evaluateTerminalPolicy 가 루트 삭제에 대해 Allowed=true, RequireApproval=false, access_mode=read_only 를 돌려줬습니다. 신규 종단 테스트가 이 응답을 그대로 재현합니다.

② denylist 의 "rm -rf" 는 원본 문자열 substring 검사였습니다

r"m" -rf /data 에는 그 부분문자열이 없으므로 차단 목록도 같은 표기를 놓쳤습니다. 이제 deny 쪽은 executor 가 해석한 형태로도 대조합니다. allow 쪽은 종전대로 원본 문자열만 읽습니다 — 정규화하면 allowlist 를 더 쉽게 만족시키는 반대 방향이 되기 때문입니다.

③ 항상 승인을 강제하는 full TTY 티어를 "bash" 가 빠져나갔습니다

isInteractiveShell 이 원본 Fields 를 써서 "bash"·\bash·'sh' 를 인터랙티브 셸로 보지 못하고 read_only 로 분류했습니다. 이제 같은 분해기를 쓰므로 표기와 무관하게 전체 TTY 로 분류되어 승인을 거칩니다.

④ executor 의 분해기가 승인된 텍스트와 두 곳에서 어긋났습니다

명시적으로 빈 인자(sh -c "" ls)를 버려 뒤 인자가 한 칸씩 당겨졌고 — 실제로 실행된 것은 sh -c ls 입니다 — 작은따옴표 안의 백슬래시를 이스케이프로 처리해 grep 'a\.b' fa.b 를 찾았습니다(셸은 문자 그대로 둡니다).

podExecArgs 가 호출자 argv 의 빈 요소를 버리고 각 요소를 trim 했습니다

sh -c <script> <argv0> <path> <query> <n> 처럼 위치 파라미터를 넘기는 호출(증적 검색 경로)에서 인자가 밀렸습니다. 이제 호출자가 준 argv 를 그대로 전달합니다.


따옴표 해석은 이제 analyzer.ShellWords 한 곳에 POSIX 규칙으로 문서화되어 있고, executor 의 분해기가 같은 규칙을 따릅니다.

검증: 신규 테스트 8개(analyzer 5 + kube 2 + proxy 3)를 고치기 전 코드에 되돌려 붙여 각 결함을 지목하며 실패하는 것을 확인했습니다(analyzer 16건·kube 5건·proxy 8건 실패). 정상 조회가 계속 low·미차단으로 남는지 지키는 오탐 회귀와, allow 쪽이 넓어지지 않았는지 지키는 테스트를 포함합니다. go build ./..., go vet ./..., go test ./... 전부 통과.

배포 파일

파일 설명
clustara-v0.9.275.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.275.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.275.md 오프라인 배포 가이드

빠른 시작

# 이미지 로드
gunzip -c clustara-v0.9.275.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.275

v0.9.274

Choose a tag to compare

@hkjang hkjang released this 06 Sep 22:07
6d2c579

스캔 원장이 실행된 적 없는 파서를 말했고, 벤더가 High 로 매긴 CVE 는 게이트를 그냥 지나갔습니다

취약점·CIS 스캔 import 는 CI 나 Trivy Operator 가 만든 JSON 을 그대로 받아, Admission 게이트·컴플라이언스 화면·DW export 가 근거로 삼는 원장으로 바꾸는 경로입니다. 이번 릴리즈는 그 정규화(analyzer.NormalizeVulnerabilityScan·NormalizeKubeBench)에서 다섯 지점을 고칩니다. 앞의 셋은 게이트가 내리는 판단 자체를 바꾸고, 뒤의 둘은 CIS 결과를 읽을 수 있게 만듭니다.

scanner="unknown" 이라고 저장한 스캔을 실제로는 Trivy 리더가 읽었습니다

detectScanner 가 아무 형식에도 맞히지 못하면 "unknown" 을 돌려주는데, 그 값이 그대로 switchdefault 로 떨어져 Trivy 파서가 실행되고 스캔 행의 scanner 컬럼에는 unknown 이 저장됐습니다. 스캔 목록·감사 로그·DW export 가 한 번도 실행된 적 없는 파서 이름을 말한 셈이고, 호출자가 scanner: "snyk" 처럼 지원하지 않는 이름을 명시한 경우도 똑같이 그 이름으로 저장되면서 Trivy 리더로 읽혔습니다.

이제 형식이 무엇이든 실제로 돌아간 리더(trivy)로 라벨링하고, 요청에 적힌 이름은 summary.requested_scanner 로 따로 남깁니다. 더 중요한 것은 그 다음입니다 — 읽히지 않은 아티팩트와 정말로 깨끗한 이미지는 둘 다 findings 0건이라 Admission 게이트가 구분할 수 없었습니다. 그래서 형식 미인식 + 0건이면 summary.parse_notice 로 "인식되지 않아 Trivy 리더로 읽었고 한 건도 찾지 못했다" 를 명시하고, 인식 여부는 항상 summary.scanner_detected 로 남깁니다.

② Grype 의 Negligible 과 RPM 권고의 Important·Moderate 가 전부 Unknown 으로 접혔습니다

NormalizeSeverity 의 매핑표에 이 세 등급이 없어서 default: "Unknown"(랭크 0)으로 떨어졌습니다. Negligible 은 Low 미만이라는 스캐너가 매긴 등급인데 "심각도 정보 없음" 과 같은 칸에 들어갔고, 더 나쁜 쪽으로 Red Hat·SUSE 계열 권고가 쓰는 Important(= High)·Moderate(= Medium)도 랭크 0이 되어 Admission 게이트의 승인 임계(SeverityRank >= 3)에 걸리지 않았습니다 — 벤더가 High 로 매긴 CVE 가 게이트를 그냥 통과했다는 뜻입니다.

이제 Important→High, Moderate→Medium 이고 Negligible 은 자기 등급을 유지합니다. 게이트 임계값은 저장된 계약이므로 등급을 다시 번호 매기지 않았습니다 — Negligible 은 Low 와 같은 랭크 1을 공유하며(둘 다 모든 임계 아래), Medium 이 승인 게이트로 밀려 올라가는 일은 없습니다.

③ severity 카운트 맵이 새 등급 하나에 핸들러를 죽일 수 있었습니다

워크로드 롤업과 요약이 map[string]any{"Critical":0, …} 를 손으로 적어 두고 m[sev].(int) + 1 로 증가시켰기 때문에, 정규화가 표에 없는 등급을 하나라도 내보내면 nil 에 대한 타입 단언 = 패닉이었습니다(요약 API 500). 두 맵 모두 정규화기의 analyzer.SeverityLevels() 에서 키를 만들도록 바꿔 등급 목록이 한 곳에만 존재하게 했습니다. 취약점 화면의 severity 필터에도 Negligible 을 넣었습니다.

④ kube-bench 결과의 Section 이 전부 상위 control 의 설명 문구였습니다

kube-bench JSON 은 Controls[{id,text}] > tests[{section:"1.1",desc}] > results[{test_number}] 로 중첩되는데, 수집기가 상속받은 값을 먼저 채택해서(firstNonEmptyV(section, x["section"], …)) 가장 바깥 노드의 자유 문구("Master Node Security Configuration")가 먼저 잡히고 그 아래 모든 control 에 그대로 눌러앉았습니다. CIS Benchmark 화면의 Section 열이 1.1 대신 상위 control 텍스트를 표시했고, 섹션 단위로 결과를 묶을 수 없었습니다. 이제 노드 자신의 section 이 상속값을 이깁니다(섹션이 아예 없는 문서는 종전대로 가장 가까운 라벨을 물려받습니다).

scored: false 인 control 이 전부 scored 로 기록됐습니다

kube-bench 의 scored 는 JSON bool 인데 문자열로 읽어서(strV(true) = "") !EqualFold("", "false") 가 언제나 참이었습니다. CIS 점수에 반영되는 control 과 참고용 control 의 구분이 사라졌습니다. bool·문자열 양쪽을 읽고, 필드가 없으면 종전대로 scored 로 봅니다. 함께, 핸들러는 읽고 있는데 정규화기가 한 번도 설정한 적 없던 BenchmarkVersion 을 채워 cis-1.7 과 cis-1.23 결과를 구분할 수 있게 했습니다.

검증

신규 테스트 8개(analyzer 7 + proxy 1)를 고치기 전 코드에 되돌려 붙여 다섯 결함을 각각 지목하며 실패함을 확인했습니다 — 인식된 형식의 라벨과 section 없는 평평한 문서의 기존 동작을 지키는 오탐 회귀 테스트를 포함합니다. go build ./...·go vet ./...·go test ./... 전부 통과합니다.

배포 파일

파일 설명
clustara-v0.9.274.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.274.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.274.md 오프라인 배포 가이드

빠른 시작

# 이미지 로드
gunzip -c clustara-v0.9.274.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.274

v0.9.273

Choose a tag to compare

@hkjang hkjang released this 06 Sep 12:31
3a064e2

가드레일이 "restricted" 라고 부른 워크로드가 사실은 가장 안 굳혀진 워크로드였습니다

Pod Security Standards 는 누적입니다 — Restricted 는 Baseline 을 포함해 그 위에 얹힙니다. 이번 릴리즈는 그 누적 관계가 코드에서 끊겨 있던 지점 네 곳을 고칩니다. 네 결함 모두 방향이 같습니다: 하드닝되지 않은 워크로드가 "준수" 로 보고됐습니다.

① 등급 판정이 자기가 방금 계산한 restricted 위반을 무시했습니다

classifyPodSecurity 의 등급 판정은 privileged·baseline 위반만 봤습니다 — switch { case len(priv)>0 … case len(baseline)>0 … default: "restricted" }. host namespace 를 안 쓰고 privileged: true 만 아니면 곧바로 최고 등급이 되는데, root 로 뜨고 기본 capability 를 다 들고 있는 것은 baseline 위반이 아닙니다. 그래서 securityContext 가 아예 없는 평범한 Pod 가 전부 restricted 로 분류됐습니다.

이 라벨은 소비자가 보는 유일한 값입니다. Pod Security 표는 level !== 'restricted' 로 걸러 그리고, 데이터 웨어하우스 export 도 restricted 행을 건너뛰며, 요약은 그것을 목표 상태로 셉니다. 즉 하드닝이 가장 안 된 워크로드가 화면에서 사라졌고, 이미 계산돼 Violations 에 들어 있던 "runAsNonRoot 미설정 · allowPrivilegeEscalation!=false · capabilities drop ALL 아님" 이 아무 데서도 표시되지 않았습니다. 클러스터 전체가 unhardened 여도 화면에는 "restricted 미만 워크로드 없음" 이 떴습니다.

이제 restricted 위반이 있으면 등급은 baseline 입니다. 위반이 없는 워크로드는 그대로 restricted 입니다.

enforce_pss_restricteddeny_privileged_runtime 의 복사본이었습니다

두 룰이 case 를 공유해서, "PSS Restricted 강제" 라는 이름의 Deny 게이트가 정작 Restricted 프로파일 항목은 하나도 검사하지 않았습니다 — host namespace·hostPath·privileged/privesc 만 봤고, 그건 이름 그대로 더 약한 deny_privileged_runtime 이 이미 하는 일입니다.

그래서 securityContext 가 통째로 없는 Pod(= root, 기본 capability, 권한 상승 허용)가 이 게이트를 그냥 통과했고, 같은 Pod 를 보안 포스처 리포트는 바로 그 세 가지 Restricted 위반으로 적고 있었습니다 — 제품의 두 부분이 같은 Pod 에 정반대 판정을 내리고 있었던 셈입니다. 이제 두 곳이 같은 헬퍼(restrictedProfileViolations)를 쓰므로 화면과 게이트가 어긋날 수 없고, 위반 문구에 어떤 항목이 걸렸는지 함께 남습니다. 컨테이너가 없는 리소스(Service 등)에는 여전히 발화하지 않습니다.

③ 컨테이너의 runAsNonRoot=false 가 Pod 설정에 가려졌습니다

포스처 리포트가 두 값을 !컨테이너 && !Pod 로 읽어서, Pod 가 runAsNonRoot: true 면 컨테이너가 명시적으로 false 여도(= 그 컨테이너는 root 로 뜹니다) 위반이 아니었습니다. 컨테이너 securityContext 는 Pod 것을 덮어씁니다. v0.9.268 에서 require_run_as_non_root 룰에 대해 고친 것과 같은 결함이 포스처 경로에 남아 있었습니다.

require_resource_limits 가 CPU limit 만 있으면 통과시켰습니다

검사가 len(limits) == 0 하나뿐이라, limits: {cpu: "500m"} 처럼 메모리 상한이 없는 컨테이너가 준수로 판정됐습니다 — 메모리 무제한은 노드를 먹고 다른 Pod 를 evict 시키는, 이 가드레일이 막으려는 바로 그 실패 형태이자 실무에서 가장 흔한 형태입니다. 내보내는 Kyverno 패턴은 이미 memory: "?*"cpu: "?*" 를 둘 다 요구하고 룰 설명도 "CPU·메모리 limits 누락 탐지" 였으므로, 세 표현 중 구현만 어긋나 있었습니다.

값이 비어 있는 키(memory: "", memory: null)도 limit 이 아닙니다(Kyverno 의 "?*" 가 거절하는 형태). 위반 문구는 빠진 키를 이름으로 지목합니다(app: resources.limits.memory 미설정). YAML 숫자로 쓴 수량(cpu: 1)은 그대로 수량으로 인정합니다. 내보내는 Rego/Kyverno 본문도 함께 맞췄습니다.

검증

신규 테스트 6개를 고치기 전 코드에 되돌려 붙여 네 결함을 각각 지목하며 실패하는 것을 확인했습니다(하드닝된 Pod 가 계속 restricted 인지, 컨테이너가 없는 리소스에 발화하지 않는지 지키는 오탐 회귀 포함). go build ./..., go vet ./..., go test ./... 전부 통과.

배포 파일

파일 설명
clustara-v0.9.273.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.273.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.273.md 오프라인 배포 가이드

빠른 시작

# 무결성 확인
sha256sum -c clustara-v0.9.273.tar.gz.sha256

# 이미지 로드
gunzip -c clustara-v0.9.273.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.273

v0.9.271

Choose a tag to compare

@hkjang hkjang released this 05 Sep 08:13
8f09d19

승인한 리소스와 실행한 리소스가 달랐습니다

Action Center 의 실행기는 액션마다 하나의 고정된 리소스 종류를 addressing 합니다 — delete_podDELETE /api/v1/namespaces/{ns}/pods/{resource_name}, cordon·uncordonPATCH /api/v1/nodes/{resource_name} 입니다. 그런데 요청에 적히는 resource_kind 는 아무도 검사하지 않는 자유 입력이었습니다. 검토·승인 화면이 무엇을 보여주든, 실행되는 대상은 이름 하나로만 정해졌습니다.

① "Deployment/web 에 delete_pod" 가 승인되고, Pod web 이 지워졌습니다

resource_kind: Deployment, resource_name: web, action: delete_pod 인 요청은 아무 데서도 막히지 않고 승인까지 간 다음, 실행 시점에 /api/v1/namespaces/{ns}/pods/web 로 갔습니다. 워크로드와 같은 이름의 Pod 가 있으면 승인 화면에 한 번도 나온 적 없는 객체가 사라지고, 감사 로그에는 그 삭제가 Deployment 이름으로 남습니다. 같은 형태의 요청이 cordon 이면 Pod 이름과 같은 이름의 노드를 차단합니다.

이제 세 지점에서 대조합니다.

  • 영향도 산출기 가 kind 불일치를 승인 사유로 올립니다 — 승인자가 화면에서 먼저 봅니다.
  • 실행 디스패치 가 Kubernetes API 로 아무것도 보내기 전에 거절합니다.
  • kube.DeletePod 이 Scale·RolloutRestart 와 마찬가지로 kind 를 인자로 받아 자기 앞에서 확인합니다.

표기 차이는 그대로 통과합니다 — Pod·pods·po·v1/Pod, deploy·sts·ds 는 운영자와 Ops Agent 가 실제로 쓰는 이름이므로 모두 같은 것으로 봅니다. kind 가 비어 있으면 주장하는 바가 없으므로 판정하지 않습니다(기존 요청이 깨지지 않습니다). scale 대상이 DaemonSet 인 경우(= /scale 서브리소스가 없음)도 같은 자리에서 함께 걸립니다.

② 인벤토리에 없는 대상의 "현재 상태"가 지어낸 값이었습니다

두 요청 핸들러 모두 GetK8sInventoryItem 이 실패하면 zero value 를 그대로 영향도 산출기에 넘겼습니다. 없는 값이 "0" 으로 읽히면서, 관측한 적 없는 상태가 승인 기록에 사실처럼 적혔습니다.

  • 아직 수집되지 않은(또는 이름이 틀린) 워크로드에 대한 scale 요청이 replicas 0 → 5 (+5) 라는 diff 로 남았습니다. 실제로 5개가 돌고 있어도 승인자는 "0 에서 올리는 요청" 으로 읽습니다.
  • delete_pod 은 라벨이 하나도 없다는 이유(zero value 에는 라벨이 없습니다)로 "standalone Pod 이라 자동 재생성되지 않습니다" 라고 단정했습니다.

이제 대상을 인벤토리에서 찾지 못하면 현재 상태를 미확인으로 표시하고(current_replicas·controller_owned 는 비워 둡니다) 그 사실과 함께 승인 대상으로 넘깁니다. 인벤토리에 있는 정상 경로의 문구와 판정은 그대로입니다.

검증

신규 테스트 6개(action 4 + kube 1 + proxy 종단 1, 오탐 회귀 포함)를 고치기 전 코드에 되돌려 붙여 여섯 개가 각 결함을 정확히 지목하며 실패하는 것을 확인했습니다. go build ./..., go vet ./..., go test ./... 전부 통과합니다.

배포 파일

파일 설명
clustara-v0.9.271.tar.gz Docker 이미지 패키지 (linux/amd64)
clustara-v0.9.271.tar.gz.sha256 SHA256 체크섬
README-offline-v0.9.271.md 오프라인 배포 가이드

빠른 시작

# 이미지 로드
gunzip -c clustara-v0.9.271.tar.gz | docker load

# 실행
docker run -d --name clustara --restart=always \
  -p 9090:9090 \
  -v /opt/clustara/data:/data \
  -e UPSTREAM_BASE_URL=https://api.openai.com \
  -e UPSTREAM_API_KEY=sk-... \
  -e ADMIN_TOKEN=change-me \
  clustara:v0.9.271