보안 점검에서 흔히 요구되는 세 항목 — API 서버 감사 로그 활성화, PodSecurity admission 설정, kubelet 익명 접근 차단 — 을 kubeadm 으로 구성한 클러스터에 적용하는 절차다. 세 항목 모두 kubectl 로는 설정할 수 없다. 컨트롤 플레인 컴포넌트의 기동 인자와 노드 로컬 설정 파일을 직접 고쳐야 한다.
감사 정책은 어떤 요청을 어느 수준으로 기록할지 정의한다. 수준은 아래로 갈수록 자세하다.
| level | 기록 내용 |
|---|---|
None |
기록하지 않음 |
Metadata |
요청자 · 동사 · 리소스 · 응답 코드 |
Request |
위에 더해 요청 본문 |
RequestResponse |
위에 더해 응답 본문 |
규칙은 위에서부터 평가되어 처음 일치하는 것이 적용된다. 따라서 제외 규칙을 위에 두고 포괄 규칙을 맨 아래에 둔다.
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
- level: None
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: Metadata
verbs: ["get", "list", "watch"]
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: ""
resources: ["pods"]
- level: Metadata
점검 기준이 "전체 리소스에 대해 Metadata 이상"인 경우가 많은데, 그때는 위 None 규칙을 넣지 않고 맨 아래 포괄 규칙만 두는 편이 안전하다. Secret 본문이 로그에 남는 것을 막고 싶으면 None 이 아니라 Metadata 로 제한한다. None 은 기록 자체를 없애므로 "전체 리소스 기록" 요건을 깬다.
파일은 컨트롤 플레인 노드의 /etc/kubernetes/audit-policy.yaml 에 둔다.
kubeadm 클러스터의 kube-apiserver 는 systemd 서비스가 아니라 static Pod 다. /etc/kubernetes/manifests/kube-apiserver.yaml 을 고치면 kubelet 이 변경을 감지해 파드를 다시 만든다. systemctl restart kube-apiserver 같은 유닛은 존재하지 않으므로 찾지 않는다.
인자만 추가해서는 안 된다. 정책 파일과 로그 디렉터리를 파드 안으로 마운트해야 한다. 이 단계를 빠뜨리면 apiserver 가 정책 파일을 못 찾아 기동에 실패하고, 그러면 kubectl 자체가 먹통이 되어 되돌리기도 번거로워진다.
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
volumeMounts:
- name: audit-policy
mountPath: /etc/kubernetes/audit-policy.yaml
readOnly: true
- name: audit-log
mountPath: /var/log/kubernetes
volumes:
- name: audit-policy
hostPath:
path: /etc/kubernetes/audit-policy.yaml
type: File
- name: audit-log
hostPath:
path: /var/log/kubernetes
type: DirectoryOrCreate
고치기 전에 매니페스트를 /etc/kubernetes/manifests 밖으로 백업한다. 같은 디렉터리에 .bak 로 두면 kubelet 이 그것까지 읽으려 들어 파드가 중복 생성된다.
적용 확인은 다음 순서로 한다.
crictl ps | grep kube-apiserver
kubectl get pod -n kube-system | grep apiserver
tail -n 20 /var/log/kubernetes/audit.log
파드가 올라오지 않으면 kubelet 로그에서 사유를 본다.
journalctl -u kubelet -n 100 --no-pager | grep -i apiserver
Pod Security Standards 의 기본 수준은 admission 설정 파일로 지정한다. 점검 항목은 보통 audit 과 warn 이 restricted 인지, 그리고 exemptions 가 비어 있는지를 본다.
현재 설정 파일 경로는 프로세스 인자에서 찾는다.
ps -ef | grep kube-apiserver | tr ' ' '\n' | grep admission-control-config-file
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: "baseline"
enforce-version: "latest"
audit: "restricted"
audit-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
usernames: []
runtimeClasses: []
namespaces: []
audit 은 위반을 감사 로그에 남기고 warn 은 kubectl 출력에 경고를 띄운다. 둘 다 파드 생성을 막지 않는다. 실제로 차단하는 것은 enforce 이므로, 운영 중인 클러스터에서 enforce: restricted 로 한 번에 올리면 기존 워크로드가 대거 거부된다. audit·warn 을 먼저 restricted 로 올려 위반 목록을 모은 뒤 enforce 를 단계적으로 올린다.
exemptions.namespaces 를 비우면 kube-system 도 예외가 아니게 된다. CNI 나 CSI 처럼 특권이 필요한 시스템 파드가 있는 네임스페이스는 실무에서는 enforce 대상에서 빼는 것이 보통인데, 점검 기준이 "예외 없음" 이면 enforce 를 낮추는 쪽으로 균형을 맞춘다.
이 파일도 apiserver 파드 안으로 마운트돼 있어야 하며, --admission-control-config-file 로 경로를 준다.
kubelet 은 노드마다 따로 설정한다. kubeadm 클러스터의 설정 파일은 /var/lib/kubelet/config.yaml 이다.
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false
webhook:
enabled: true
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
anonymous.enabled: false 만 끄고 authorization.mode 를 AlwaysAllow 로 둔 상태면 인증된 주체는 무엇이든 할 수 있게 되므로 함께 Webhook 으로 맞춘다.
systemctl restart kubelet
systemctl status kubelet --no-pager
익명 접근이 실제로 막혔는지는 노드 밖에서 확인한다.
curl -k https://<NODE_IP>:10250/pods
401 Unauthorized 가 나오면 정상이다. 설정 전에는 파드 목록이 그대로 나온다.
kubeadm 은 kubeadm upgrade 때 kubelet-config ConfigMap 의 내용을 노드 파일로 다시 내려쓸 수 있다. 업그레이드 후 설정이 되돌아가 있는지 다시 확인한다.