요약
QdrantClusterSpec.apiKey(*SecretKeyRef)는 CRD·Go 타입에 선언만 돼 있고 컨트롤러가 이 필드를 전혀 읽지 않는다. CR에 spec.apiKey를 지정해도 StatefulSet에 QDRANT__SERVICE__API_KEY가 주입되지 않아 Qdrant는 계속 무인증으로 기동한다. 사용자에게는 "API 키를 걸었다"는 착각을 주는 silent no-op이며, v0.2.1에서 고쳤던 retentionPolicy unwired(API 선언만 있고 STS 매핑 0)와 동일 계열의 함정이다.
실측 근거 (main 77eafb0, v0.6.0)
- 참조 부재 —
rg 'APIKey' --glob '*.go' 결과는 api/v1alpha1/qdrantcluster_types.go(선언)와 zz_generated.deepcopy.go(생성물)뿐. internal/ 컨트롤러·리소스 빌더에서 소비하는 곳 0건.
- env 미주입 —
internal/resources/statefulset.go:88 컨테이너 Env는 QDRANT_INIT_FILE_PATH 하나뿐. QDRANT__SERVICE__API_KEY 없음.
- 라이브 대조 —
data/keiailab-qdrant-cluster STS env = [QDRANT_INIT_FILE_PATH], 라이브 CR spec.apiKey = null. (라이브는 애초에 apiKey 미지정이라 회귀가 아니라 기능 자체의 부재)
현재 완화(mitigation): NetworkPolicy / Cilium 실효 통제
이 갭이 지금 사고로 이어지지 않는 이유는 네트워크 계층이 접근을 막고 있기 때문이며, 이는 실측으로 확인했다.
- Cilium
enable-non-default-deny-policies=true, default-ns-zerotrust CNP 가동. 실측: 허용 목록 밖(default ns) 임시 파드에서 keiailab-qdrant-cluster.data.svc:6333 → 연결 timeout(차단).
- qdrant 접근은 소비자 측 egress 화이트리스트로만 열린다:
services/aethelgard-zerotrust, services/toonmixer-publication, traders/traders-scheduler (→ 6333/TCP).
- ⇒ 무단 네임스페이스로부터의 노출은 실효적으로 막혀 있어 긴급도는 낮다(P2).
잔여 리스크 (네트워크 통제로 못 막는 것)
- 허용된 소비자 ns의 워크로드는 qdrant에 무인증 전체 접근 — 컬렉션 삭제·스냅샷·설정 변경 등 파괴적 REST/gRPC까지 제한이 없다.
- 소비자 앱 하나가 침해되면 그 egress 경로를 타고 qdrant 전체가 lateral movement 표적이 된다.
- NetworkPolicy는 L3/L4(누가 연결 가능한가) 계층이라 application-level authz(무엇을 할 수 있는가)를 대체하지 못한다. apiKey는 다중 소비자·최소권한을 위한 별개 계층이며, network policy와 상호 배타가 아니라 보완 관계다.
구현 시 결정 포인트
- 키 종류 — qdrant 1.18은 단일
service.api_key와 별도 service.read_only_api_key를 지원한다. 소비자별 최소권한(읽기 전용 소비자 vs 쓰기 소비자)을 노출하려면 SecretKeyRef를 2개(rw/ro)로 확장할지 결정 필요.
- TLS 병행 —
spec.config.tlsEnabled가 이미 존재. 평문 위에 API 키만 얹으면 토큰이 스니핑에 노출되므로, TLS와 함께 문서화/게이팅할지 결정.
- 배선 위치 —
statefulset.go의 컨테이너 Env에 QDRANT__SERVICE__API_KEY(+ 필요 시 __READ_ONLY_API_KEY)를 SecretKeySelector로 주입. config/initialize.sh의 env 소비 방식 확인(readOnlyRootFilesystem=true·config passthrough 제약 하에서). production.yaml service.api_key와 env 간 우선순위 정합.
- 관측 —
status에 authEnabled류 필드 노출 여부.
수용 기준(제안)
- CR에
apiKey 지정 → STS env에 QDRANT__SERVICE__API_KEY 주입 → 무인증 요청이 401.
- envtest는 PVC GC 케이스처럼 이 갭을 못 잡는다(선언 ≠ 배선). 따라서 격리 스테이징 실전 검증(무인증 401 실측)을 수용 기준에 포함.
발견 경위: data ns 라이브 qdrant의 인증 상태 점검 중 CRD 선언과 컨트롤러 배선 불일치를 확인. 완화책인 NetworkPolicy의 실효성(default ns 차단)은 실측으로 검증함.
요약
QdrantClusterSpec.apiKey(*SecretKeyRef)는 CRD·Go 타입에 선언만 돼 있고 컨트롤러가 이 필드를 전혀 읽지 않는다. CR에spec.apiKey를 지정해도 StatefulSet에QDRANT__SERVICE__API_KEY가 주입되지 않아 Qdrant는 계속 무인증으로 기동한다. 사용자에게는 "API 키를 걸었다"는 착각을 주는 silent no-op이며, v0.2.1에서 고쳤던retentionPolicyunwired(API 선언만 있고 STS 매핑 0)와 동일 계열의 함정이다.실측 근거 (main
77eafb0, v0.6.0)rg 'APIKey' --glob '*.go'결과는api/v1alpha1/qdrantcluster_types.go(선언)와zz_generated.deepcopy.go(생성물)뿐.internal/컨트롤러·리소스 빌더에서 소비하는 곳 0건.internal/resources/statefulset.go:88컨테이너Env는QDRANT_INIT_FILE_PATH하나뿐.QDRANT__SERVICE__API_KEY없음.data/keiailab-qdrant-clusterSTS env =[QDRANT_INIT_FILE_PATH], 라이브 CRspec.apiKey = null. (라이브는 애초에 apiKey 미지정이라 회귀가 아니라 기능 자체의 부재)현재 완화(mitigation): NetworkPolicy / Cilium 실효 통제
이 갭이 지금 사고로 이어지지 않는 이유는 네트워크 계층이 접근을 막고 있기 때문이며, 이는 실측으로 확인했다.
enable-non-default-deny-policies=true,default-ns-zerotrustCNP 가동. 실측: 허용 목록 밖(default ns) 임시 파드에서keiailab-qdrant-cluster.data.svc:6333→ 연결 timeout(차단).services/aethelgard-zerotrust,services/toonmixer-publication,traders/traders-scheduler(→ 6333/TCP).잔여 리스크 (네트워크 통제로 못 막는 것)
구현 시 결정 포인트
service.api_key와 별도service.read_only_api_key를 지원한다. 소비자별 최소권한(읽기 전용 소비자 vs 쓰기 소비자)을 노출하려면SecretKeyRef를 2개(rw/ro)로 확장할지 결정 필요.spec.config.tlsEnabled가 이미 존재. 평문 위에 API 키만 얹으면 토큰이 스니핑에 노출되므로, TLS와 함께 문서화/게이팅할지 결정.statefulset.go의 컨테이너Env에QDRANT__SERVICE__API_KEY(+ 필요 시__READ_ONLY_API_KEY)를SecretKeySelector로 주입.config/initialize.sh의 env 소비 방식 확인(readOnlyRootFilesystem=true·config passthrough 제약 하에서). production.yamlservice.api_key와 env 간 우선순위 정합.status에authEnabled류 필드 노출 여부.수용 기준(제안)
apiKey지정 → STS env에QDRANT__SERVICE__API_KEY주입 → 무인증 요청이 401.발견 경위:
datans 라이브 qdrant의 인증 상태 점검 중 CRD 선언과 컨트롤러 배선 불일치를 확인. 완화책인 NetworkPolicy의 실효성(default ns 차단)은 실측으로 검증함.