LabHub
배우기 러닝패스 코스

CKS — Kubernetes Security Specialist

Why Auditing Starts With the Config File and Policy With Default Deny

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

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

Concept map: 정적 파드 매니페스트를 고치면 kubelet 이 알아서 다시 띄운다. · 그 컴포넌트가 아예 안 뜨므로 · 자주 나오는 플래그 몇 개는 외워 둔다. · 감사 정책은 파일도 함께 마운트해야 한다.

왜 이게 필요했나

쿠버네티스는 개발자 친화적인 기본값을 골랐습니다. 파드는 아무 파드에게나 말을 걸 수 있고, 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 의 클러스터 셋업 영역은 "설정 파일을 고치고 컴포넌트를 되살리는" 형태로 나온다. 손이 기억해야 하는 것이 몇 개 정해져 있다.

정적 파드 매니페스트를 고치면 kubelet 이 알아서 다시 띄운다. /etc/kubernetes/manifests/ 아래 파일을 바꾸는 순간 반영된다. 문법을 틀리면 그 컴포넌트가 아예 안 뜨므로, 고치기 전에 사본을 남긴다.

cp /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/
# 고친 뒤
crictl ps | grep apiserver          # 새 컨테이너가 떴는지
journalctl -u kubelet -f            # 안 뜨면 이유가 여기 남는다

API 서버를 잘못 고치면 kubectl 자체가 안 되므로, 확인은 crictljournalctl 한다. 이것을 모르면 시험장에서 갇힌다.

자주 나오는 플래그 몇 개는 외워 둔다.

목적 플래그
익명 접근 차단 --anonymous-auth=false
감사 로그 --audit-policy-file, --audit-log-path, --audit-log-maxage
etcd 암호화 --encryption-provider-config
승인 제어 --enable-admission-plugins=NodeRestriction,...

감사 정책은 파일도 함께 마운트해야 한다. 플래그만 넣고 volumes/ volumeMounts 를 안 더하면 API 서버가 파일을 못 찾아 안 뜬다. 정적 파드에서 가장 흔한 실수다.

etcd 암호화는 켠 뒤에 기존 데이터를 다시 써야 적용된다.

kubectl get secrets -A -o json | kubectl replace -f -

kubelet 쪽도 본다. /var/lib/kubelet/config.yamlauthentication.anonymousauthorization.mode 가 문제로 나온다. 여기를 고치면 kubelet 을 직접 재시작해야 한다(systemctl restart kubelet).

바꾼 뒤에는 반드시 확인 한 줄을 돌린다. 문제가 요구한 것이 실제로 적용됐는지 보는 명령까지가 답이다.

다음 실습에서 할 것

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