Certified Kubernetes Security Specialist (CKS) #3 CIS benchmark (kube-bench), 컴포넌트 보안, Ingress TLS, 바이너리 검증
지난 글에서 NetworkPolicy로 Pod 사이의 통신을 default deny로 잠갔다면, 이번에는 클러스터를 떠받치는 control plane 자체를 단단하게 만드는 일로 넘어갑니다. apiserver가 익명 요청을 받아들이거나, kubelet이 인증 없는 read-only 포트를 열어 두면 NetworkPolicy를 아무리 잘 짜도 apiserver와 kubelet 자체가 열려 있으면 효과가 없습니다. 이번 글은 Cluster Setup도메인의 나머지 절반인 컴포넌트 보안과 클러스터 점검을 다룹니다.
핵심 도구는 kube-bench입니다. CIS Kubernetes benchmark라는 업계 표준 점검표를 클러스터에 자동으로 돌려서, 어떤 설정이 안전하지 않은지를 PASS/FAIL/WARN으로 알려 줍니다. 시험에서는 kube-bench가 뱉은 FAIL을 보고 해당 컴포넌트의 플래그를 고치는 작업이 단골로 나옵니다.
CIS Kubernetes benchmark란 #
CIS(Center for Internet Security) benchmark는 특정 소프트웨어를 안전하게 설정하기 위한 항목별 점검표입니다. 쿠버네티스용 CIS benchmark는 control plane 컴포넌트(apiserver, controller-manager, scheduler, etcd)와 노드 컴포넌트(kubelet, kube-proxy)의 설정 파일,플래그,파일 권한을 수백 개 항목으로 나누어, 각 항목이 어떤 값이어야 안전한지를 규정합니다.
각 항목은 두 단계로 나뉩니다.
- Automated(자동): 도구가 설정 파일을 읽어 값을 기계적으로 판정할 수 있는 항목입니다.
- Manual(수동): 정책이나 운영 맥락이 필요해 사람이 직접 확인해야 하는 항목입니다.
이 점검표를 손으로 일일이 확인하는 것은 비현실적이므로, Aqua Security가 만든 kube-bench가 점검을 자동화합니다. kube-bench는 실행되는 노드에서 설정 파일과 프로세스 플래그를 읽어, CIS benchmark의 각 항목과 대조한 뒤 결과를 PASS/FAIL/WARN으로 출력합니다.
kube-bench 실행 #
kube-bench는 보통 노드에서 직접 바이너리로 실행하거나, Job 형태로 클러스터 안에서 돌립니다. 시험 환경에는 보통 바이너리가 설치되어 있으므로, control plane 노드와 워커 노드에서 각각 적절한 대상을 지정해 실행합니다.
control plane 노드에서 control plane 항목을 점검하려면 master 타깃을, 워커 노드에서 kubelet 같은 노드 항목을 점검하려면 node 타깃을 줍니다. 한 번에 둘을 지정할 수도 있습니다.
# control plane 노드에서: apiserver, controller-manager, scheduler, etcd 점검
kube-bench run --targets=master
# 워커 노드에서: kubelet, kube-proxy 점검
kube-bench run --targets=node
# 둘을 한 번에 지정 (해당 노드에 있는 컴포넌트만 점검됨)
kube-bench run --targets=master,node특정 벤치마크 버전을 명시할 수도 있습니다. 클러스터 버전에 맞는 benchmark가 자동 선택되지만, 어긋날 때는 직접 지정합니다.
# CIS benchmark 버전을 직접 지정
kube-bench run --targets=master --benchmark cis-1.23결과가 길어 특정 항목만 보고 싶을 때는 출력을 grep으로 거르거나, --check로 항목 번호를 지정합니다.
# FAIL 항목만 추려서 보기
kube-bench run --targets=master | grep -E '\[FAIL\]'
# 특정 항목 번호만 점검
kube-bench run --targets=master --check 1.2.1kube-bench 결과 읽기 #
kube-bench의 출력은 항목 번호, 상태, 설명으로 이루어집니다. 상태는 세 가지입니다.
- [PASS]: 항목이 안전한 값으로 설정되어 있습니다. 손댈 필요가 없습니다.
- [FAIL]: 항목이 안전하지 않은 값이거나 누락되어 있습니다. 반드시 고쳐야 하는 대상입니다.
- [WARN]: 자동 판정이 어려운 항목입니다. 사람이 직접 확인하라는 뜻이며, Manual 항목이 주로 여기에 해당합니다.
출력의 핵심은 각 FAIL 항목 아래에 따라오는 remediation 섹션입니다. kube-bench는 무엇이 잘못됐는지뿐 아니라, 어떻게 고치는지까지 명령이나 설정 변경 지침으로 알려 줍니다. 시험에서는 이 remediation을 그대로 읽고 적용하면 됩니다.
[INFO] 1 Control Plane Security Configuration
[INFO] 1.2 API Server
[FAIL] 1.2.1 Ensure that the --anonymous-auth argument is set to false (Manual)
...
== Remediations master ==
1.2.1 Edit the API server pod specification file
/etc/kubernetes/manifests/kube-apiserver.yaml on the control plane node
and set the below parameter.
--anonymous-auth=false
== Summary master ==
42 checks PASS
8 checks FAIL
12 checks WARN
0 checks INFO위 출력의 흐름을 읽는 순서는 이렇습니다. 먼저 Summary에서 FAIL이 몇 개인지 보고, 본문에서 [FAIL] 항목을 찾아, 그 항목 번호에 해당하는 remediation을 본 뒤, 지시된 파일을 열어 플래그를 고칩니다.
control plane 컴포넌트 보안 설정 #
control plane 컴포넌트는 kubeadm 클러스터에서 static Pod로 실행됩니다. 따라서 이들의 설정은 일반 kubectl이 아니라 노드의 매니페스트 파일을 직접 편집해서 바꿉니다. 매니페스트 위치는 다음과 같습니다.
| 컴포넌트 | static Pod 매니페스트 경로 |
|---|---|
| kube-apiserver | /etc/kubernetes/manifests/kube-apiserver.yaml |
| kube-controller-manager | /etc/kubernetes/manifests/kube-controller-manager.yaml |
| kube-scheduler | /etc/kubernetes/manifests/kube-scheduler.yaml |
| etcd | /etc/kubernetes/manifests/etcd.yaml |
이 디렉터리의 파일을 저장하면 kubelet이 변경을 감지해 해당 static Pod를 자동으로 재생성합니다. 별도 apply가 필요 없습니다. 인증서 구조와 kubeconfig의 기초는 CKA #8 인증서 관리에서 다룬 PKI 구조를 전제로 합니다.
apiserver 주요 플래그 #
apiserver는 클러스터의 정문이므로 가장 많은 항목이 걸립니다. kube-bench FAIL로 자주 나오는 플래그를 정리하면 다음과 같습니다.
| 플래그 | 안전한 값 | 의미 |
|---|---|---|
--anonymous-auth | false | 익명 요청 거부 |
--authorization-mode | Node,RBAC | AlwaysAllow 금지, RBAC 강제 |
--profiling | false | 프로파일링 엔드포인트 노출 차단 |
--insecure-port | 0 (또는 제거) | 인증 없는 HTTP 포트 비활성 |
--audit-log-path | 경로 지정 | 감사 로그 기록 활성 |
--kubelet-certificate-authority | CA 경로 | kubelet 통신 인증서 검증 |
매니페스트의 command 배열에 플래그를 추가하거나 값을 고칩니다.
# /etc/kubernetes/manifests/kube-apiserver.yaml (발췌)
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --anonymous-auth=false
- --authorization-mode=Node,RBAC
- --profiling=false
- --audit-log-path=/var/log/kubernetes/audit.log
# ... 기존 플래그 유지 ...저장 후 apiserver가 재시작되며 잠깐 API 응답이 끊깁니다. 다시 응답할 때까지 기다린 뒤 확인합니다.
# apiserver static Pod가 다시 떴는지 확인
kubectl -n kube-system get pod -l component=kube-apiserver
# 매니페스트 편집에 오타가 있으면 Pod가 안 뜬다. crictl로 컨테이너 로그 확인
sudo crictl ps -a | grep apiserver
sudo crictl logs <container-id>매니페스트 편집 시 가장 흔한 사고는 들여쓰기 오류나 잘못된 플래그 값으로 apiserver가 아예 안 뜨는 것입니다. 이때는 kubectl 자체가 먹지 않으므로, 위처럼 crictl로 컨테이너 로그를 봐서 원인을 찾습니다.
kubelet 보안 설정 #
kubelet은 각 노드에서 도는 에이전트로, 노드의 정문에 해당합니다. kubelet도 익명 요청과 인증 없는 read-only 포트를 막아야 합니다. kube-bench의 node 타깃에서 자주 나오는 항목입니다.
| 설정 | 안전한 값 | 의미 |
|---|---|---|
anonymous.enabled (--anonymous-auth) | false | 익명 요청 거부 |
authorization.mode (--authorization-mode) | Webhook | AlwaysAllow 금지 |
readOnlyPort | 0 | 인증 없는 10255 포트 비활성 |
protectKernelDefaults | true | 커널 파라미터 변경 거부 |
kubelet은 보통 config 파일로 설정합니다. kubeadm 노드에서는 /var/lib/kubelet/config.yaml이 기본 위치이며, 실제 실행 인자는 /var/lib/kubelet/kubeadm-flags.env에서도 확인합니다.
# /var/lib/kubelet/config.yaml (발췌)
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false
webhook:
enabled: true
authorization:
mode: Webhook
readOnlyPort: 0
protectKernelDefaults: truekubelet은 static Pod가 아니라 systemd 서비스이므로, config를 고친 뒤에는 수동으로 재시작해야 적용됩니다.
# kubelet config 수정 후 재시작
sudo systemctl restart kubelet
# 재시작 후 상태 확인
sudo systemctl status kubelet플래그와 config 파일이 같은 항목을 동시에 지정하면 보통 명령행 플래그가 우선합니다. 어느 쪽이 실제로 먹는지 헷갈릴 때는 kubeadm-flags.env에 같은 플래그가 있는지부터 확인합니다.
Ingress TLS #
Cluster Setup도메인은 외부에서 들어오는 트래픽을 암호화하는 일도 포함합니다. Ingress에 TLS를 붙이면 클라이언트와 클러스터 사이 구간이 HTTPS로 보호됩니다. 절차는 두 단계입니다. 인증서를 담은 Secret을 만들고, Ingress의 tls 섹션에서 그 Secret을 참조합니다.
먼저 인증서와 키로 tls 타입 Secret을 만듭니다.
# 인증서(tls.crt)와 키(tls.key)로 TLS Secret 생성
kubectl create secret tls web-tls \
--cert=tls.crt \
--key=tls.key \
-n default그다음 Ingress에서 이 Secret을 tls 섹션으로 연결합니다. hosts에 적은 도메인과 Secret 인증서의 CN/SAN이 일치해야 브라우저가 신뢰합니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
namespace: default
spec:
tls:
- hosts:
- app.example.com
secretName: web-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80적용한 뒤 TLS가 실제로 붙었는지 확인합니다.
kubectl apply -f web-ingress.yaml
# Ingress에 TLS host가 잡혔는지 확인
kubectl describe ingress web-ingress
# 핸드셰이크와 인증서 확인 (Ingress 컨트롤러 주소로)
curl -vk https://app.example.com/ --resolve app.example.com:443:<ingress-ip>describe 출력의 TLS 섹션에 host와 Secret 이름이 보이면 연결이 된 것입니다. Secret 이름을 잘못 적거나 Secret이 다른 네임스페이스에 있으면 TLS가 적용되지 않으니, Ingress와 Secret이 같은 네임스페이스에 있는지를 꼭 확인합니다.
바이너리 검증 #
공급망 보안의 가장 기초는 다운로드한 바이너리가 변조되지 않았는지 확인하는 것입니다. kubectl, kubeadm, kube-bench 같은 도구를 인터넷에서 받을 때, 배포처가 공개한 체크섬과 직접 계산한 체크섬을 대조합니다. 값이 다르면 전송 중 손상되었거나 변조된 것이므로 그 바이너리는 버립니다.
쿠버네티스 바이너리는 배포처가 .sha256 파일을 함께 제공합니다. 받은 뒤 sha256sum으로 직접 계산해 대조합니다.
# 바이너리와 공식 체크섬 파일을 함께 받음
curl -LO "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl.sha256"
# 방법 1: 계산값과 공개값을 사람이 눈으로 대조
sha256sum kubectl
cat kubectl.sha256
# 방법 2: --check로 기계적으로 대조 (OK가 나와야 함)
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check--check가 kubectl: OK를 출력하면 무결성이 확인된 것입니다. FAILED가 나오면 바이너리를 신뢰하지 말고 다시 받습니다. 시험에서는 “이 바이너리의 sha256 체크섬이 주어진 값과 일치하는지 확인하라” 같은 형태로 나오며, 일치 여부를 답하거나 일치하지 않는 파일을 골라내는 작업이 됩니다.
이미지 서명(cosign)과 SBOM 같은 더 깊은 공급망 검증은 #15에서 다루며, 이번 글의 sha256 검증은 그 출발점입니다.
시험 포인트 #
- kube-bench FAIL 고치기가 단골입니다.
kube-bench run --targets=master를 돌려 FAIL 항목을 찾고, 그 아래 remediation을 그대로 읽어 해당 매니페스트나 config를 고치는 흐름을 손에 익혀 두겠습니다. - control plane은 static Pod입니다.
/etc/kubernetes/manifests/의 매니페스트를 편집하면 kubelet이 자동 재생성합니다. apply는 필요 없습니다. - kubelet은 systemd 서비스입니다.
/var/lib/kubelet/config.yaml을 고친 뒤systemctl restart kubelet으로 직접 재시작해야 적용됩니다. - 매니페스트 편집 후 apiserver가 안 뜨면 kubectl도 먹지 않습니다.
crictl ps -a와crictl logs로 원인을 찾는 절차를 미리 외워 두겠습니다. - 외워 둘 안전 값: apiserver
--anonymous-auth=false,--authorization-mode=Node,RBAC,--profiling=false. kubeletanonymous.enabled=false,authorization.mode=Webhook,readOnlyPort=0. - Ingress TLS는 Secret과 Ingress가 같은 네임스페이스에 있어야 합니다.
kubectl create secret tls로 만들고tls섹션의secretName으로 참조합니다. - 바이너리 검증은
sha256sum --check로 기계적으로 대조합니다.OK가 나오는지를 확인합니다.
정리 #
이번 글에서 잡은 것:
- CIS Kubernetes benchmark는 컴포넌트별 안전 설정 점검표이며, kube-bench가 이를 자동으로 점검해 PASS/FAIL/WARN으로 출력합니다.
- kube-bench 결과의 핵심은 FAIL 아래의 remediation입니다. 무엇을 어떤 파일에서 어떻게 고칠지를 그대로 알려 줍니다.
- control plane 컴포넌트는
/etc/kubernetes/manifests/의 static Pod 매니페스트로, kubelet은/var/lib/kubelet/config.yaml로 설정을 바꿉니다. - apiserver와 kubelet의 익명 인증,인가 모드,read-only 포트,프로파일링을 안전한 값으로 잠급니다.
- Ingress TLS는
tls타입 Secret을 만들고 Ingress의tls섹션에서 참조합니다. - 바이너리는
sha256sum으로 공개 체크섬과 대조해 무결성을 확인합니다.
다음: RBAC 최소 권한 #
Cluster Setup도메인의 두 글로 네트워크와 컴포넌트를 잠갔습니다. 이제 두 번째 도메인인 Cluster Hardening으로 넘어가, 클러스터 안에서 누가 무엇을 할 수 있는지를 좁힙니다.
#4 RBAC 최소 권한 깊이에서는 과도하게 넓은 ClusterRole을 어떻게 찾아내는지, Role과 RoleBinding으로 네임스페이스 단위의 최소 권한을 어떻게 설계하는지, kubectl auth can-i로 권한을 검증하는 법, 그리고 시험에서 자주 나오는 “이 ServiceAccount에 정확히 이 동작만 허용하라” 유형까지 직접 만들어 보며 정리하겠습니다.