LabHub

CKS — 쿠버네티스 보안 전문가 · 클러스터 셋업 · 이론

왜 감사는 설정 파일부터, 정책은 기본 거부부터인가

LabHub 에서 이어서 보기

한 줄 요약

CIS 벤치마크가 /etc/kubernetes/manifests 의 텍스트 파일부터 훑는 이유와, 네트워크 정책을
"기본 거부"로 시작해야 하는 이유는 같습니다. 둘 다 기본값이 위험한 쪽으로 열려 있기 때문입니다.

왜 이게 필요했나

쿠버네티스는 개발자 친화적인 기본값을 골랐습니다. 파드는 아무 파드에게나 말을 걸 수 있고,
API 서버는 익명 요청을 system:anonymous 라는 사용자로 받아 주며, kubelet 은 인증 없는
읽기 전용 포트를 열어 둘 수 있었습니다. 이 기본값들은 "설정하지 않으면 열려 있다"는 성질을
공유합니다. 그래서 보안 점검의 첫 질문은 "무엇을 막았는가"가 아니라 "무엇을 안 막았는가"가
됩니다.

CIS 벤치마크와 kube-bench 가 설정 파일 감사부터 시작하는 이유가 여기 있습니다. 컨트롤 플레인
컴포넌트는 대부분 정적 파드 매니페스트 한 장의 command 배열로 동작이 결정됩니다. 그 배열에
--anonymous-auth=false 가 없다는 사실 하나가, 클러스터에 도달할 수 있는 누구나 인증 없이
API 를 두드릴 수 있다는 뜻입니다. 실행 중인 프로세스를 관찰하는 것보다 텍스트 한 줄을 읽는
쪽이 빠르고, 무엇보다 증거로 남습니다.

어떻게 동작하나

NetworkPolicy 는 화이트리스트입니다. 어떤 파드에 대해 정책이 하나도 없으면 그 파드는 전부
허용 상태입니다. 정책이 하나라도 그 파드를 선택하는 순간, 그 정책의 policyTypes 에 적힌
방향은 "명시된 것만 허용"으로 뒤집힙니다. 그래서 실무 순서는 항상 이렇습니다.

| 순서 | 정책 | 효과 |
| --- | --- | --- |
| 1 | podSelector: {} + policyTypes: [Ingress, Egress], 규칙 없음 | 네임스페이스 전체 차단 |
| 2 | DNS egress 만 개방 (kube-dns 로 UDP/TCP 53) | 이름 해석 복구 |
| 3 | 필요한 파드 쌍만 ingress 개방 | 최소 연결 |
| 4 | ipBlockexcept 로 노드 메타데이터 차단 | 자격증명 탈취 경로 차단 |

2번을 빼먹는 것이 가장 흔한 사고입니다. 기본 거부를 걸면 DNS 조회도 egress 이므로 함께 막히고,
애플리케이션은 "커넥션 거부"가 아니라 "이름을 못 찾음"으로 죽습니다. 원인이 네트워크 정책이라는
것을 알아채기까지 시간이 걸리는 이유입니다.

ipBlock.except 는 CKS 에서 반복 출제되는 지점입니다. 클라우드의 인스턴스 메타데이터 주소
169.254.169.254 는 인증 없이 노드의 IAM 자격증명을 돌려줄 수 있습니다. 파드 하나가 뚫리면
그 노드의 클라우드 권한이 통째로 넘어갑니다. egress 를 0.0.0.0/0 으로 열더라도 except
이 한 주소만 도려내는 것이 정석입니다.

현장에서 만나는 모습

저자의 7 노드 홈랩은 Cilium 1.20.1 을 eBPF 모드로 올려 kube-proxy 를 아예 설치하지 않습니다.
여기서 확인한 것이 하나 있습니다. 같은 IP, 같은 80 포트인데 GET 은 200, POST 는 403 으로
갈렸습니다. 사이드카를 주입하지 않았고 애플리케이션 코드도 그대로였습니다. L3/L4 만 보는
기본 NetworkPolicy 로는 불가능한 일이고, 이것이 CNI 선택이 곧 보안 표현력이라는 뜻입니다.
반대로 Flannel 기본 설정처럼 NetworkPolicy 자체를 구현하지 않는 CNI 위에서는, 정책 오브젝트를
아무리 정성껏 써도 아무것도 막히지 않습니다. 오브젝트가 생성됐다는 사실과 트래픽이 막혔다는
사실은 별개입니다.

같은 홈랩에서 더 뼈아팠던 사고는 설정 파일 쪽이었습니다. DHCP 가 컨트롤 플레인 IP 를
10.0.0.111 에서 10.0.0.120 으로 바꿔 버렸고, apiserver 인증서의 SAN 에는 새 주소가 없었습니다.
etcd 와 kube-apiserver 는 존재하지 않는 주소에 바인드하려다 CrashLoopBackOff 에 빠졌지만,
kube-scheduler 와 controller-manager 는 127.0.0.1 에 바인드하므로 프로세스는 멀쩡히 살아
있었습니다. "절반은 살아 있는" 이 상태가 진단을 가장 어렵게 만듭니다. 파일 한 장의 값이
클러스터 전체의 생사를 가른다는 것을 몸으로 배운 사건이었습니다.

다음 실습에서 할 것

cks-net 네임스페이스에 기본 거부 정책을 걸고, DNS 만 예외로 여는 egress 규칙을 얹고,
노드 메타데이터로 나가는 길을 ipBlock.except 로 도려냅니다. Ingress 용 TLS Secret 을
kubernetes.io/tls 타입으로 만들고, 마지막으로 CIS 권고를 반영한 kube-apiserver 매니페스트를
직접 작성해 어떤 플래그가 있어야 하고 어떤 플래그가 없어야 하는지 손으로 확인합니다.