Kubernetes TLS Setup Howto (ko)

Kubernetes 클러스터의 보안 통신

Kubernetes api서버는 비보안의 HTTP와 보안 HTTPS/TLS 프로토콜 모두를 지원합니다.

8080 포트를 사용하는 비보안 HTTP는 기본으로 설정되지만, 이름에서 알수 있듯 안전하지 않습니다. 따라서 KUBIC 클러스터에서 안전한 HTTPS 통신을 설정해야 합니다. 그 방법을 기술 합니다.

SSL 대 TLS

HTTPS를 설정하기 전에, 먼저 HTTPS가 사용하는 기술인 SSL과 TLS에 대해 알아봅시다.

  • SSL (Secure Sockets Layer protocol)
  • Netscape 개발
  • v2.0(1995), v3.0(1996)
  • 오래된 프로토콜
  • POODLE/BEAST 공격
  • 권장하지 않음

POODLE 공격으로 SSL 3.0은 더이상 안전하지 않습니다.

  • TLS (Transport Layer Security protocol)
  • v1.0 (1999, RFC 2246)
  • v1.1 (2006, RFC 4346)
  • 최신 v1.2 (2008, RFC 5246)
  • v1.3 곧 출시 예정
  • SSL 보다 안전 (v1.0은 여전히 BEAST 공격에 취약)
  • 가능하면 TLS 사용 권장

보안을 위해 SSL v2/v3, TLS v1.0, 특히 SSL v2/v3를 사용하지 원길 권장합니다. 그래서 이제부턴 TLS만 언급할 것입니다.

openssl은 표준 TLS 구현에 사용됩니다.

서버와 클라이언트 간의 TLS 핸드쉐이크(handshake)

서버와 클라이언트는 어떻게 핸드쉐이크를 할까? 서버와 클라이언트간 데이터 전송을 하기전에, 9개의 핸드쉐이크 단계가 있습니다.

  1. TLS 클라이언트가 TLS 서버에 “client hello” 메시지 전달
  • TLS version: 지원되는 TLS 버전
  • CipherSuites: 지원되는 암호화 방법
  • 이후 계산에 사용할 랜덤 바이트 문자열
  1. TLS 서버는 “server hello” 메시지로 응답
  • CipherSuite: TLS 클라이언트가 보낸 CipherSuites 중 선택
  • 세션 ID
  • 다른 랜덤 바이트 문자열
  • 서버 인증서
  • (선택사항) 클라이언트 인증서 요청
    • 지원되는 인증서 타입 목록
    • 허용 가능한 인증기관(CAs:Certification Authorities)의 고유이름(DN:Distinguished Name)
  1. TLS 클라이언트는 서버 인증서 확인
  • CA.crt + server.crt
  1. TLS 클라이언트 전송
  • 비밀키(secret key)를 계산하기 위한 랜덤 바이트 문자열
  • 비밀키(secret key)는 서버와 클라이언트 모두 공유 됩니다.
  • 이 랜덤 바이트 문자열은 서버 인증서로 자체 암호화됩니다.
  1. (선택사항) 클라이언트 전송
  • 클라언트의 개인키(private key)로 암호화된 램덤 바이트 문자열
  • 클라이언트 인증서 혹은 인증서 없음 경고인 “no certificate alert”
  1. (선택사항) TLS 서버가 클라이언트의 인증서 확인
  • CA.crt + client.crt
  1. TLS 클라이언트 전송
  • 비밀키(secret key) 암호화된 “finished” 메시지
  1. TLS 서버 전송
  • 동일한 비밀키(secret key)로 암호화된 “finished” 메시지
  1. TLS 세션 기간동안 서버와 클라이언트 교환
  • 공유 비밀키(shared secret key)로 대칭된 암호화 메시지
 ----------                              -----------
|TLS Client|                            | TLS Server|
|          |1.client hello              |           |
|          |--------------------------->|           |
|          |CipherSuites                |           |
|          |                            |           |
|          |2.server hello              |           |
|          |<-------------------------->|           |
|          |ciphersuite                 |           |
|          |server cert.                |           |
|          |client cert req.(opt.)      |           |
|          |                            |           |
|3. verify |                            |           |
|   server |                            |           |
|   cert.  |                            |           |
|          |4.client key exchange       |           |
|          |--------------------------->|           |
|          |random string(secret key)   |           |
|          |(encrypted with svr cert.)  |           |
|          |5.client certificate(opt.)  |           |
|          |                            |6.verify   |
|          |                            |  client   |
|          |                            |  cert.    |
|          |7.client "finish"           |           |
|          |--------------------------->|           |
|          |8.server "finish"           |           |
|          |<---------------------------|           |
|          |                            |           |
|          |9. exchange messages        |           |
|          |<-------------------------->|           |
|          |(encrypted with secret key) |           |
 ----------                              -----------

아래는 클러스터에서 TLS 환경을 설정하는 절차입니다.

Kubernetes master

인증서를 저장할 디렉토리를 생성합니다.

$ sudo mkdir /srv/kubernetes

Certificate Authority(CA) 키와 인증서를 생성합니다.

$ sudo openssl genrsa -out /srv/kubernetes/ca.key 4096
$ sudo openssl req -x509 -new -nodes \
  -key /srv/kubernetes/ca.key \
  -subj "/CN=kubemaster" -days 10000 \
  -out /srv/kubernetes/ca.crt

For the server certificate, I’ll add 서버 인증서의 경우 아래를 추가합니다.

  • Subject: CN=kubernetes.default.svc (prometheus와 같은 다른 애드온을 위해)
  • subjectAltName = IP:10.24.0.1 (kubedns를 위해)

http://wiki.cacert.org/FAQ/subjectAltName

서버키를 생성합니다.

$ sudo openssl genrsa -out /srv/kubernetes/server.key 2048

서버 인증서에 대한 Certificate Signing Request(CSR)를 만듭니다.

$ cat /etc/ssl/openssl.cnf \
  <(printf "[SAN]\nsubjectAltName='DNS.1:kubernetes.default.svc,DNS.2:kubemaster,IP.1:10.24.0.1'") \
  > /tmp/openssl.cnf
$ sudo openssl req \
  -new -key /srv/kubernetes/server.key \
  -subj "/CN=kubernetes.default.svc" \
  -reqexts SAN \
  -config /tmp/openssl.cnf \
  -out /srv/kubernetes/server.csr

CSR에서 서버 인증서를 생성하고 CA key과 인증서로 서명합니다.

$ sudo openssl x509 -req -in /srv/kubernetes/server.csr \
-CA /srv/kubernetes/ca.crt -CAkey /srv/kubernetes/ca.key \
-CAcreateserial \
-extensions SAN -extfile /tmp/openssl.cnf \
-days 10000 -out /srv/kubernetes/server.crt

인증서를 살펴봅니다.(선택사항)

$ sudo openssl x509 -noout -text -in /srv/kubernetes/server.crt

인증서 파일을 안전하게 만들어야합니다. Kubernetes master의 3총사(apiserver, controller-manager, scheduler)는 ‘kube’ 사용자에 의해 실행되므로 ‘kube’ 사용자만 인증서를 읽게끔 합니다.

$ sudo chown -R kube:nogroup /srv/kubernetes
$ sudo chmod 600 /srv/kubernetes/*

kubelet에 대한 전달토클(bearer token)를 이용하여 랜덤 토큰을 생성합니다. 클라이언트 인증만 사용한다면 건너 뛸수 있습니다.

$ TOKEN=$(dd if=/dev/urandom bs=128 count=1 2>/dev/null \
  | base64 |tr -d '=+/' |dd bs=32 count=1 2>/dev/null)

api 서버에 대한 토큰 파일을 생성합니다. 이 파일은 /etc/kubernetes/apiserver(–token-auth-file 옵션)에서 사용됩니다.

$ echo "${TOKEN},kubelet,kubelet" | \
  sudo tee /srv/kubernetes/tokens.csv

/etc/kubernetes/apiserver를 수정합니다.

KUBE_API_ARGS="--kubelet-https=true
--client-ca-file=/srv/kubernetes/ca.crt
--tls-cert-file=/srv/kubernetes/server.crt
--tls-private-key-file=/srv/kubernetes/server.key
--token-auth-file=/srv/kubernetes/tokens.csv"

/etc/kubernetes/controller-manager파일을 수정합니다.

KUBE_CONTROLLER_MANAGER_ARGS="--root-ca-file=/srv/kubernetes/ca.crt
--service-account-private-key-file=/srv/kubernetes/server.key
--node-monitor-grace-period=20s --pod-eviction-timeout=20s"

kube-apiserver와 kube-controller-manager 서비스를 재시작합니다.

$ sudo systemctl restart kube-{apiserver,controller-manager}.service

kube-apiserver가 TCP 포트 6443을 열었는지 확인합니다.

$ sudo netstat -ntpl |grep 6443
tcp6  0    0    :::6443      :::*    LISTEN 7609/kube-apiserver

각 minion에 대한 인증서를 생성합니다.

$ MINIONS=$(echo kubeminion{1,2,3,4})
$ for MINION in $MINIONS; do \
    sudo openssl req -newkey rsa:2048 -nodes -keyout \
      /srv/kubernetes/${MINION}.key -subj "/CN=${MINION}" -out \
      /srv/kubernetes/${MINION}.csr; \
    sudo openssl x509 -req -days 10000 \
      -in /srv/kubernetes/${MINION}.csr \
      -CA /srv/kubernetes/ca.crt -CAkey /srv/kubernetes/ca.key \
      -CAcreateserial -out /srv/kubernetes/${MINION}.crt; \
  done

다시 인증서 파일을 안전하게 합니다. ‘kube’사용자만 인증서 파일을 읽게끔 합니다.

$ sudo chown -R kube:nogroup /srv/kubernetes
$ sudo chmod 600 /srv/kubernetes/*

CA와 클라이언트 인증서를 minions에게 복사합니다.

$ for MINION in $MINIONS; do \
  ssh ${MINION} sudo rm -fr /srv/kubernetes; \
  ssh ${MINION} sudo mkdir /srv/kubernetes; \
  sudo scp /srv/kubernetes/ca.crt \
    /srv/kubernetes/${MINION}.{crt,key} \
    jijisa@${MINION}:~/; \
  ssh ${MINION} sudo mv ~jijisa/${MINION}.{crt,key} ca.crt \
    /srv/kubernetes/; \
  ssh ${MINION} sudo chown -R root:root /srv/kubernetes/; \
  ssh ${MINION} sudo chmod 600 /srv/kubernetes/*; \
done

각 minion에서 다이나믹 듀요(kubelet과 kube-proxy)는 root로 싱행되므로 키 디렉토리의 소유권은 root만 소유해야합니다.

Kubernetes Minions

각 minion에서 kubelete 서비스가 apiserver와 안전하게 통신하도록 설정합니다. 각 minion에서 kubeconfig파일을 /var/lib/kublelete에 생성할 것입니다.

$ KUBECONFIG=/var/lib/kubelet/kubeconfig
$ MINION=$(hostname)
$ TOKEN=<the_token_master_created>
$ CLUSTER=<cluster_name>
$ MASTER=<master_hostname>
$ sudo rm -f ${KUBECONFIG}
$ sudo kubectl config --kubeconfig=${KUBECONFIG} \
  set-cluster ${CLUSTER} \
  --server=https://${MASTER}:6443 \
  --certificate-authority=/srv/kubernetes/ca.crt --embed-certs=true
$ sudo kubectl config --kubeconfig=${KUBECONFIG} \
  set-credentials kubelet \
  --client-certificate=/srv/kubernetes/${MINION}.crt \
  --client-key=/srv/kubernetes/${MINION}.key --embed-certs=true \
  --token=${TOKEN}
$ sudo kubectl config --kubeconfig=${KUBECONFIG} \
  set-context kubelet-context --cluster=${CLUSTER} --user=kubelet
$ sudo kubectl config --kubeconfig=${KUBECONFIG} \
  use-context kubelet-context

내 환경에서는 cluster_name은 debian-cluster이고 master는 kubernetes.default.svc입니다.

/etc/kubernetes/config 파일을 수정합니다.

KUBE_MASTER="--master=https://kubemaster:6443"

/etc/kubernetes/kubelet를 변경합니다.

KUBELET_API_SERVER="--api-servers=https://kubemaster:6443"
KUBELET_ARGS="--cluster-dns=10.24.0.10 --cluster-domain=kubic.local
--enable_server=true --register-node=true
--kubeconfig=/var/lib/kubelet/kubeconfig
--node-status-update-frequency=5s"

/etc/kubernetes/proxy를 변경합니다.

KUBE_PROXY_ARGS="--kubeconfig=/var/lib/kubelet/kubeconfig"

이제 kubenetes minion의 다이나믹 듀오(kubelete과 kube-proxy) 서비스를 재시작합니다.

$ sudo systemctl restart kube{let,-proxy}.service

테스트

  • 클라인트 인증서 이용 클라이언트 인증서를 이용해 kubernetes api에 안전하게 접근할수 있는지 확인합니다.

    $ sudo curl -k --key /srv/kubernetes/${MINION}.key --cert \
    /srv/kubernetes/${MINION}.crt --cacert /srv/kubernetes/ca.crt \
    https://kubemaster:6443/version
    {
      "major": "1",
      "minor": "4",
      "gitVersion": "v1.4.2",
      "gitCommit": "c072cc2ee4dfc21b60196e5994c56c40e1c29b93",
      "gitTreeState": "clean",
      "buildDate": "2016-10-15T00:07:23Z",
      "goVersion": "go1.6.3",
      "compiler": "gc",
      "platform": "linux/amd64"
    }
    

    인증서가 자체 서명(self-signed)되었기때문에 -k를 사용해야 합니다.

    -k, --insecure: (SSL) CA certificate self-signed.
    
  • 토큰 이용

    $ curl -k -H "Authorization: Bearer ${TOKEN}" \
    https://kubemaster:6443/version
    

References