kubelet "서버" 를 위한 인증서 (kubelet.crt, kubelet.key) (feat: Metrics-Server TLS 인증서 오류 조치)
Kubelet 은 kube-apiserver 를 호출하는 그저 클라이언트 아니였나요?
kubelet 은 컨트롤플레인 노드, 워커 노드 관계없이 쿠버네티스의 노드라면 무조건 실행되어야 하는 컴포넌트로, 컨테이너 런타임과 통신하여 컨테이너를 실행하고 관리하는데 이는 static pod 뿐 아니라 API 서버로부터 받은 PodSpec 에 기반해서 컨테이너를 실행하기도 합니다.
이 kubelet 은 기본적으로 kube-apiserver 를 호출하여 노드의 상태를 컨트롤 플레인에 알려주는 역할을 하는데. 이 때 Kubelet 이 kube-apiserver 에 인증하기 위해서는 특별한 인증 모드인 Node Authorizer
라는 모드의 인증을 사용합니다. (Using Node Authorization )
이 때 사용하는 x.509(TLS) 인증서는 CN 에 system:node:<nodename>
형태로 각 kubelet 이 떠있는 노드의 이름이 각 인증서별로 존재합니다.
이렇게 kubelet 이 kube-apiserver 를 호출하고, 인증처리를 하기 위해 사용하는 x.509 인증서는 /var/lib/kubelet/pki
디렉터리 내에 kubelet-client-current.pem (심볼릭 링크)
형태로 존재합니다.
kubelet.conf 등의 kubelet 이 사용할 kubeconfig 파일을 확인해보시면 아시겠지만, kubeconfig 파일은 해당 심볼릭 링크를 인증서로 바라보도록 되어있고, 실제 인증서는 kubelet-client-2024-05-27-17-07-20.pem
과 같은 형태로 datetime 이 뒤쪽에 붙는 형태로 되어있습니다.
이건 실제로 kubelet 이 이 인증서를 필요시에 rotate
하기 위함이고. kubeconfig 는 심볼릭 링크만 바라보도록 해서 실제 심볼릭 링크만을 인증서가 업데이트 되었을 때 바라보는 경로를 업데이트 하여서 kubeconfig 파일의 수정이 필요하지 않도록 설계가 되었습니다.
kubelet
은 기본적으로 로 10250/10255 번 포트로 서버
를 띄우고 Listen 을 하도록 되어있습니다.
이 kubelet 서버의 엔드포인트 정보는 아래와 같습니다.
이 엔드포인트를 호출하는 대표적인 케이스가 metrics-server
입니다. metrics-server 는 K8s 내의 Pod 의 메트릭을 실시간으로 수집해서 kube-apiserver 에 전달하게되는데요, 이것을 통해 HPA(Horizontal Pod Autocaling), VPA(Vertical Pod Autoscaling) 등을 사용가능하게 됩니다.
metrics-server
는 각 노드에 있는 kubelet 의 10250 번 포트로 메트릭을 수집하기위해 kubelet 서버에 HTTP Request 를 하게 됩니다.
kubelet 은 kube-apiserver 에 대한 클라이언트이자, 파드를 관리하는 오퍼레이터이자, metrics 및 기타 기능을 노출하는 서버로서 동작을 하는 셈입니다.
Kubelet 의 서버 인증서 (x.509)
kubelet 서버에 대한 TLS 인증서/키를 따로 지정하지 않았을 경우 self-signed certs 를 생성해서 Kubelet 의 서버(10250 번 포트)를 TLS 를 enable 한 서버로 실행합니다.
/var/lib/kubelet/pki
디렉터리 내의 kubelet.crt
, kubelet.key
는 kubelet self-signed 키, 인증서 입니다.
/var/lib/kubelet/pki
에는 두가지 종류의 인증서가 들어가게 되는데요.
kubelet-client-xxx.pem
은 kubelet
→ kube-apiserver
인증시 Node Authorizer
인증 모드를 사용해 (이건 x509 인증서 내의 CN이 system:nodes:<node-name> 형태로 서명된 인증서를 사용하는거) apiserver 에 인증하기 위한 인증서입니다.
kubelet.crt
는 kubelet
서버가 TLS 서버를 오픈하기 위한 인증서입니다.
이 글에서는 후자를 다루겠습니다.
앞서 이야기 한 것 처럼 kubelet-client-current.pem
의 경우 kubelet -> kube-apiserver
인증을 위한 인증서입니다.
$ openssl x509 -text -noout -in kubelet-client-current.pem
을 해보면
위와 같이 CN
이 system:node:<node-name>
인 node-authorizer
인증을 위한 인증서임을 알 수 있습니다.
그러면 kubelet의 TLS 서버를 위한 kubelet.crt
는 어떨까요?
$ openssl x509 -text -noout -in kubelet.crt
를 해보면
Issuer
의 Common Name을 보면 알겠지만 self-sigend 인증서임을 알 수 있습니다.
이 인증서는 kubelet
을 실행 할 때 kubelet configuration에 tlsPrivateKeyFile
, tlsCertFile
을 지정하지 않은 경우 자동
으로 생성됩니다.
만약 /var/lib/pki/kubelet.crt
, /var/lib/pki/kubelet.key
를 삭제하고 kubelet 을 재실행 하더라도 다시 생성하게 됩니다.
대부분의 케이스에서 이 동작이 크게 문제 될일은 없지만.
metrics-server
의 경우 kubelet
서버의 (10250) 엔드포인트를 찔러서 데이터를 수집하는데, 이 때 10250 포트로 열린 kubelet 의 인증서가 self-signed 인증서 이기 때문에 metrics server 를 설치 한 경우 아래와 같은 에러가 발생하게 될것입니다.
"Failed to scrape node" err="Get \"https://192.168.207.7:10250/metrics/resource\\": x509: cannot validate certificate for 192.168.207.7 because it doesn't contain any IP SANs" node="node5"
kubelet TLS 서버를 띄우기 위한 kubelet self-signed 인증서 내에 SANs 필드에 DNS 정보는 있지만, 해당 kubelet이 떠있는 노드에 대한 IP 정보가 없기 때문에 발생하는 문제입니다. 위쪽에서 kubelet.crt
인증서 내용을 확인해본 내용을 다시 보면. DNS:node1
과 같은 정보는 있지만, IP 정보는 없기 때문에 그렇습니다.
이 때 보통 찾아보면 나오는 방법이. metrics-server
내에 --kubelet-insecure-tls
를 실행인자로 주는 방법이 있습니다.
하지만 더 올바른 접근법은 아래와 같습니다.
kubelet 의 TLS 인증서를 kube-apiserver 로 부터 받아오기 (TLS Bootstrap)
이것은 kubelet 실행 인자, 혹은 kubelet configuration 에서
serverTLSBootstrap: true
을 추가해서 사용 할 수 있는 방법입니다.
kube-apiserver / controller-manager 에게 TLS 인증서를 요청하는 방법이 있습니다. 그것을 TLSBootstrap 이라고 부릅니다.
우선 기존 kubelet configuration 에 serverTLSBootstrap: true 를 추가합니다.
/etc/kubernetes/kubelet-config.yaml
에 보통 KubeletConfiguration
이 설정되어있을것입니다.
수정 한 이후
$ systemctl stop kubelet
$ systemctl restart kubelet
$ kubectl get csr -A
해당 명령어들을 수행해보면 아래와 같을것입니다.
위와 같이 system:node:<node-name>
으로부터 kubernetes.io/kubelet-serving
형태의 서명 요청이 생겼습니다. 여기서 $ kubectl certificate approve csr-bvbnx
와 같이 해당 서명 요청에대해서 approve
해봅니다.
이렇게 하면 이제 kubelet-server-current.pem
이라는 인증서가 보이게 됩니다. (kubelet.crt
, kubelet.key
는 삭제해도 됩니다.)
이제 해당 인증서로 해당 노드의 kubelet
은 10250 번 포트로 TLS 서버를 오픈합니다.
$ openssl x509 -text -noout -in kubelet-server-current.pem
을 확인해보면
위와 같이 Issuer 의 CN 이 kubernetes 임을 알 수 있습니다.
이건 우리가 쿠버네티스 클러스터를 생성하고 인증서를 만들 때, 자체적으로 사용하는 CA 를 의미합니다( /etc/kubernetes/ssl/{ca.crt,ca.key}
). 쿠버네티스 내의 각종 인증서의 CA 는 이것입니다. (etcd 및 front-proxy 는 별도의 CA를 두는 편입니다)
또한 기존에 self-signed 인증서에는 없었던 IP Address 항목이 SAN 필드에 들어가있는것을 확인 할 수 있습니다. 이로써 metrics-server 가 해당 https://해당IP:10250
로 접근 했을 때 클라이언트 측에서 문제가 생길 것은 없어졌습니다. (기존에는 https://해당IP:10250
으로 접근하지만, 정작 인증서 정보에는 DNS
정보만 있어서 문제가 생겼습니다.)
이제 이상태에서 metrics-server 를 insecure
옵션 없이 써보면 오류 없이 metrics-server 가 잘 kubelet 의 메트릭을 수집하는 것을 확인 할 수 있습니다.
여기서 부가적으로 얻을 수 있는 장점은. kubelet 의 rotateCerts 기능에 이제 current-server-pem 이 들어오게 되는것입니다. (self-signed 인증서는 kubelet 의 해당 기능이 통제하지 않음. 관련한 다양한 이슈 또한 존재. kubeadm 도 그것을 처리하지 않음)
이렇게 수동으로 approve 해야 하나요? 노드가 계속 늘어나면 어떻게하죠?
우리가 방금 한 방식은 TLS Bootstrap 요청, CSR 요청에 대해서 수동으로 approve 를 하는 방식입니다.
쿠버네티스는 이런 인증서 서명 요청에 대해 자동화 하기 위해 Approver
라는 개념을 두고있습니다.
우리가 컨트롤플레인/워커노드 join 을 할때, 바닐라 혹은 kubeadm 을 써보면 token
을 만들어서 join 하는 방법을 해본적이 있을것입니다.
이것은 token
을 기반으로 인증된 요청에 대해서 TLS Bootstrap 요청을(CSR) 자동으로 수락하는 Approver 를 설정한 것입니다.
kube-controller-manager
에서 controllers
로 default 로 추가되어있지 않아서 (정확히는 Default 로 disable 되어있어서) 추가해줘야 합니다.
kubeadm 으로 클러스터를 프로비저닝 했다면 자동으로 되어있는 사항입니다. (kube-controller-manager )
bootstrapsigner
, tokencleaner
는 디폴트로 활성화되는 컨트롤러들이 아니기 때문에 추가가 되는것입니다. 이렇게 클러스터 조인시 사용되는 TLS Bootstrap 요청에 대해서 승인 할 수 있는 컨트롤러는 사전에 구현되어있지만.
kubelet
의 Server TLS 인증서 요청에 대한 자동 승인 컨트롤러는 kube-controller-manager 에 구현되어있지는 않습니다. 그래서 과거에는
kubelet-rubber-stamp
라는 것이 사용되었었지만, 현재 유지보수 되고있는 프로젝트가 아니고, 또한 몇가지 보안 허점이 있는것으로 알려져있습니다.
이것을 위한 대안으로 kubelet-csr-approver
라는 것이 있고 kubespray 또한 kubelet 의 serverTLSBootstrap 옵션을 활성화 하면 이것을 함께 배포하는것으로 개선되었습니다.
그럼 kubespray 에서 저친구 어떻게 쓸 수 있어요?
우선 전역변수에 kubelet_rotate_server_certificates: true
를 넣어줘야 하고
여기의 kubelet-csr-approver
의 변수를 설정하는 defaults/main.yml 내에 kubelet_csr_approver_values
를 자신의 상황에 맞게 적절하게 채워줘야합니다.
현재 테스트하는 환경의 경우 모든 노드는 node1
, node2
와 같이 node
라는 이름을 prefix 로 갖게하고, 모든 노드는 192.168.207.0/24
CIDR 블록 안에서 프로비저닝 했었기 때문에 아래와 같이 설정했습니다.
이렇게 하면 우리가 수동으로 approve 하지 않고 kubelet-csr-appover 가 설정에 맞는 요청에 대해서 자동으로 approve 하도록 사용 할 수 있습니다.
Kubespray 안쓰면 어떻게 써요?
kubelet-csr-approver 는 helm 으로 패키징되어있기 때문에 해당 레포를 참조해서 helm 으로 설치하는것을 권장합니다.
helm repo add kubelet-csr-approver https://postfinance.github.io/kubelet-csr-approver
helm install kubelet-csr-approver kubelet-csr-approver/kubelet-csr-approver -n kube-system \
--set providerRegex='^node-\w*\.int\.company\.ch$' \
--set providerIpPrefixes='192.168.8.0/22' \
--set maxExpirationSeconds='86400'
--set bypassDnsResolution='false'
여기서도 역시나 위에서 이야기한대로 providerRegex
, providerIpPrefixes
등을 여러분의 환경에 맞게 설정하면 됩니다.
Wrapping up
Kubelet 은 10250 번 포트에 TLS 기반의 HTTP 서버(HTTPS)를 열어두고 있습니다. Kubelet 은 kube-apiserver 를 호출하고, 컨테이너 런타임 인터페이스를 통해 파드(컨테이너)를 관리하는것 이외에도 metrics, log 등을 위해 10250번 포트로 서버를 열어두고 있습니다.
이때 별다른 설정을 하지 않고 kubelet 을 실행하면 kubelet.crt
, kubelet.key
와 같은 형태의 self-signed
인증서가 /var/lib/kubelet/pki
디렉터리 내에 생성되고, 그것을 기반으로 TLS 서버를 실행합니다.
크게 문제될일은 없지만 metrics-server
를 실행하는 경우에 self-signed
인증서의 문제점으로 인해 metrics-server
가 kubelet metrics 를 긁어올 수 없는 문제가 있습니다. 이 때 보통은 --kubelet-insecure-tls
옵션을 metrics-server
에 추가하여 문제를 해결하기도 하지만 근본적인 문제를 해결한것은 아닙니다.
kubelet
이 kubernetes-ca 를 통해 서명된 인증서를 kubelet 서버에 사용하기 위해서는 serverTLSBootstrap
옵션을 KubeletConfiguration
(혹은 kubelet 실행 인자에도 넣을 수 있지만 depracted 되어가고있습니다.)에 추가할 수 있습니다. 다만 이 때에도 해당 CSR(Certificate Signing Request)를 자동으로 수락하는 별도의 애드온등을 사용하지 않는 경우에 수동으로 해당 요청에 대해서 approve
해줘야 합니다.