kubelet "서버" 를 위한 인증서 (kubelet.crt, kubelet.key) (feat: Metrics-Server TLS 인증서 오류 조치)

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 서버의 엔드포인트 정보는 아래와 같습니다.

kubernetes/pkg/kubelet/server/server.go at 68091805a53c6fdd42553f8ccdbf932ab263fbdd · kubernetes/kubernetes
Production-Grade Container Scheduling and Management - kubernetes/kubernetes

이 엔드포인트를 호출하는 대표적인 케이스가 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)

 kubernetes/cmd/kubelet/app/server.go at 4bb434501d9ee5edda6faf52a9d6d32a969ae183 · kubernetes/kubernetes

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.pemkubeletkube-apiserver 인증시 Node Authorizer 인증 모드를 사용해 (이건 x509 인증서 내의 CN이 system:nodes:<node-name> 형태로 서명된 인증서를 사용하는거) apiserver 에 인증하기 위한 인증서입니다.

kubelet.crtkubelet 서버가 TLS 서버를 오픈하기 위한 인증서입니다.

이 글에서는 후자를 다루겠습니다.

/var/lib/kubelet/pki 디렉터리의 구조

앞서 이야기 한 것 처럼 kubelet-client-current.pem 의 경우 kubelet -> kube-apiserver 인증을 위한 인증서입니다.

$ openssl x509 -text -noout -in kubelet-client-current.pem 을 해보면

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 2796500698555846452 (0x26cf2a919f8d8f34)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = kubernetes
        Validity
            Not Before: May 27 08:02:17 2024 GMT
            Not After : May 27 08:07:19 2025 GMT
        Subject: O = system:nodes, CN = system:node:node1
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:b6:9d:8f:31:03:12:47:b6:b9:83:62:ef:96:ba:
                    ...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Authority Key Identifier:
                8F:3C:69:8E:62:01:95:BF:FB:D8:8B:41:4A:D7:B8:CF:0F:9C:36:9F
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        9d:38:83:5b:78:2c:56:53:90:02:cb:e3:1e:b4:b0:d3:61:07:
        ...

kubelet-client-current.pem

위와 같이 CNsystem:node:<node-name>node-authorizer 인증을 위한 인증서임을 알 수 있습니다.  

그러면 kubelet의 TLS 서버를 위한 kubelet.crt 는 어떨까요?

$ openssl x509 -text -noout -in kubelet.crt 를 해보면

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 8001362879639973410 (0x6f0a8d255fefee22)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = node1-ca@1716797154
        Validity
            Not Before: May 27 07:05:54 2024 GMT
            Not After : May 27 07:05:54 2025 GMT
        Subject: CN = node1@1716797154
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:c3:19:c5:5e:71:3f:3c:0e:8b:dc:56:85:2e:7a:
                    ...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Authority Key Identifier:
                CC:87:5A:94:20:C4:F3:11:C6:06:90:A7:A4:C1:B9:1E:FE:51:CE:9F
            X509v3 Subject Alternative Name:
                DNS:node1
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        60:7d:e4:4e:23:2d:50:92:1d:78:51:22:43:ab:38:75:82:93:
        ...

kubelet.crt

Issuer 의 Common Name을 보면 알겠지만 self-sigend 인증서임을 알 수 있습니다.

이 인증서는 kubelet 을 실행 할 때 kubelet configuration에 tlsPrivateKeyFile , tlsCertFile 을 지정하지 않은 경우 자동 으로 생성됩니다.

https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/#kubelet-config-k8s-io-v1beta1-KubeletConfiguration 참고.

만약 /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

을 추가해서 사용 할 수 있는 방법입니다.

serverTLSBootstrap 인자를 kubelet configuration 에 추가한 모습

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 을 확인해보면

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            21:24:58:0f:29:01:0d:ab:45:c4:10:a1:3b:d3:fc:7d
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = kubernetes
        Validity
            Not Before: May 27 09:19:33 2024 GMT
            Not After : May 27 09:19:33 2025 GMT
        Subject: O = system:nodes, CN = system:node:node1
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:e1:30:d0:21:06:29:4b:0d:5e:4e:45:04:9f:d1:
                    ...
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Authority Key Identifier:
                8F:3C:69:8E:62:01:95:BF:FB:D8:8B:41:4A:D7:B8:CF:0F:9C:36:9F
            X509v3 Subject Alternative Name:
                DNS:node1, IP Address:192.168.207.3
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        ca:a1:b9:79:7a:8e:9e:83:89:ad:01:4b:d7:34:ea:f2:6d:13:
        ...

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 )

kube-controller-manager 의 실행 인자중 --controllers
💡
bootstrapsigner, tokencleaner 는 디폴트로 활성화되는 컨트롤러들이 아니기 때문에 추가가 되는것입니다. 

이렇게 클러스터 조인시 사용되는 TLS Bootstrap 요청에 대해서 승인 할 수 있는 컨트롤러는 사전에 구현되어있지만.

kubelet Server TLS 인증서 요청에 대한 자동 승인 컨트롤러는 kube-controller-manager 에 구현되어있지는 않습니다. 그래서 과거에는

GitHub - kontena/kubelet-rubber-stamp: Simple CSR approver for Kubernetes
Simple CSR approver for Kubernetes. Contribute to kontena/kubelet-rubber-stamp development by creating an account on GitHub.

kubelet-rubber-stamp

kubelet-rubber-stamp 라는 것이 사용되었었지만, 현재 유지보수 되고있는 프로젝트가 아니고, 또한 몇가지 보안 허점이 있는것으로 알려져있습니다.

GitHub - postfinance/kubelet-csr-approver: Kubernetes controller to enable automatic kubelet CSR validation after a series of (configurable) security checks
Kubernetes controller to enable automatic kubelet CSR validation after a series of (configurable) security checks - postfinance/kubelet-csr-approver

kubelet-csr-approver

이것을 위한 대안으로 kubelet-csr-approver 라는 것이 있고 kubespray 또한 kubelet 의 serverTLSBootstrap 옵션을 활성화 하면 이것을 함께 배포하는것으로 개선되었습니다.

그럼 kubespray 에서 저친구 어떻게 쓸 수 있어요?

 

우선 전역변수에 kubelet_rotate_server_certificates: true 를 넣어줘야 하고

이렇게 ansible host.yaml 파일에 전역변수로 넣어주면 된다.
kubespray/roles/kubernetes-apps/kubelet-csr-approver/defaults/main.yml at 5616a4a3eedf3462d25346f02afe57bf36ae77c3 · kubernetes-sigs/kubespray
Deploy a Production Ready Kubernetes Cluster. Contribute to kubernetes-sigs/kubespray development by creating an account on GitHub.

여기의 kubelet-csr-approver 의 변수를 설정하는 defaults/main.yml 내에 kubelet_csr_approver_values 를 자신의 상황에 맞게 적절하게 채워줘야합니다.

현재 테스트하는 환경의 경우 모든 노드는 node1, node2 와 같이 node 라는 이름을 prefix 로 갖게하고, 모든 노드는 192.168.207.0/24 CIDR 블록 안에서 프로비저닝 했었기 때문에 아래와 같이 설정했습니다.

여러분에 상황에 맞게 수정해야하고 .. 자세한 내용은 kubelet-csr-approver 레포 참고

이렇게 하면 우리가 수동으로 approve 하지 않고 kubelet-csr-appover 가 설정에 맞는 요청에 대해서 자동으로 approve 하도록 사용 할 수 있습니다.

Kubespray 안쓰면 어떻게 써요?

GitHub - postfinance/kubelet-csr-approver: Kubernetes controller to enable automatic kubelet CSR validation after a series of (configurable) security checks
Kubernetes controller to enable automatic kubelet CSR validation after a series of (configurable) security checks - postfinance/kubelet-csr-approver

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 해줘야 합니다.