taxin's notes

読書、勉強メモ etc.

Kubernetes the hard way (GCP版) #4 (CAのプロビジョニングとTLS証明書の生成)

Kubernetes the hard way (GCP版)の続きです。

前回の記事はこちら。 taxintt.hatenablog.com

Chap4. Provisioning a CA and Generating TLS Certificates (CAのプロビジョニングとTLS証明書の生成)

このチャプターでは、Kubernetesクラスター内で利用するTLS証明書の作成を行います。
認証局(CA)の構成ファイル・CA証明書を作成し、それらを利用してTLS証明書を作成するという流れとなっています。

先に、ハンズオンの手順を一通り記載した後に解説を記載していきます。

CAのプロビジョニング

最初にTLS証明書の作成に必要な認証局(CA)の構成ファイルを作成します。
構成ファイルの中では、証明書の利用用途と有効期限をセットした証明書発行に利用するプロファイル(Kubernetes)が定義されています。

github.com

TLS証明書の作成時には、作成した構成ファイルとプロファイル(Kubernetes)を指定します。

// 認証局(CA)の構成ファイルを作成する
~/w/k/h/04 ❯❯❯ {

cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF


次に、CA証明書とCAの秘密鍵の作成を行います。 証明書の作成に必要なCSR (証明書署名要求) を作成した上で、CSRを基に証明書と秘密鍵を作成します。

// CSRを作成する
cat > ca-csr.json <<EOF
{
  "CN": "Kubernetes", 
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "CA",
      "ST": "Oregon"
    }
  ]
}
EOF

// CSRを基にCA証明書とCAの秘密鍵を生成する
cfssl gencert -initca ca-csr.json | cfssljson -bare ca

}
2020/02/29 11:00:07 [INFO] generating a new CA key and certificate from CSR
2020/02/29 11:00:07 [INFO] generate received request
2020/02/29 11:00:07 [INFO] received CSR
2020/02/29 11:00:07 [INFO] generating key: rsa-2048
2020/02/29 11:00:07 [INFO] encoded CSR
2020/02/29 11:00:07 [INFO] signed certificate with serial number 254290516728701407670608002579038214846567871824

// CA証明書(ca.pem)とCAの秘密鍵(ca-key.pem)が生成されている
~/w/k/h/04 ❯❯❯ ls
ca-config.json ca-csr.json    ca-key.pem     ca.csr         ca.pem

TLS証明書の作成

認証局(CA)の構成ファイル、CA証明書、CAの秘密鍵を基に各種証明書を作成します。
作成する証明書は下記の通りです。

  • adminユーザー用クライアント証明書
  • Kubelet用クライアント証明書
  • Controller Manager用クライアント証明書
  • Kube Proxy用クライアント証明書
  • Scheduler用クライアント証明書
  • KubernetesAPI Server用証明書
  • サービスアカウントのキーペア

CSR (証明書署名要求) を作成し、それを基にTLS証明書と秘密鍵を作成する部分は先程の手順と同じです。
これ以降は、認証局(CA)の構成ファイル、CA証明書と秘密鍵を指定して証明書の作成を行います。

最初に、adminユーザー用のクライアント証明書を作成します。
特記事項は下記の通りです。

  • CSRの識別名(DN)の一つであるOrganizationでsystem:mastersを指定する
~/w/k/h/04 ❯❯❯ {

cat > admin-csr.json <<EOF
{
  "CN": "admin",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:masters",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

// クライアント証明書の作成時にCAの証明書・秘密鍵・構成ファイルを引数として渡している
cfssl gencert \
  -ca=ca.pem \ 
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  admin-csr.json | cfssljson -bare admin

}
2020/02/29 11:13:46 [INFO] generate received request
2020/02/29 11:13:46 [INFO] received CSR
2020/02/29 11:13:46 [INFO] generating key: rsa-2048
2020/02/29 11:13:47 [INFO] encoded CSR
2020/02/29 11:13:47 [INFO] signed certificate with serial number 679971876887522464623843539440976113444619121269
2020/02/29 11:13:47 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
websites. For more information see the Baseline Requirements for the Issuance and Management
of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
specifically, section 10.2.3 ("Information Requirements").

//admin-key.pem, admin.pemが作成されている
~/w/k/h/04 ❯❯❯ ls
admin-csr.json admin-key.pem  admin.csr      admin.pem      ca-config.json ca-csr.json    ca-key.pem     ca.csr         ca.pem


次に、Kubelet用のクライアント証明書を作成します。 特記事項は下記の通りです。

  • 識別名(DN)のOrganizationとしてsystem:nodesを指定する
  • API Server側でNode Authorizerを利用したリクエストの許可設定を行うために、証明書のCommon Name(CN)としてsystem:node:${instance}を指定する
  • hostnameとしてノードのホスト名(${instance})とIP(${INTERNAL_IP})、(Chap3で作成した)外部公開用のIPアドレス(${EXTERNAL_IP})を渡している


qiita.com

Kubeletは(API Serverに対しての)クライアントでもありますが、サーバとしての役割も果たすため-hostnameで複数のホスト名を渡します。
これにより、証明書にはCNの別名であるSANs(Subject Alternative Names)が指定されます。

また、-profile=kubernetesKubernetesのプロファイルからusageを参照することで、作成した証明書がクライアント兼サーバ証明書として利用できるようになってると考えられます。

~/w/k/h/04 ❯❯❯ for instance in worker-0 worker-1 worker-2; do
cat > ${instance}-csr.json <<EOF
{
  "CN": "system:node:${instance}",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:nodes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

EXTERNAL_IP=$(gcloud compute instances describe ${instance} \
  --format 'value(networkInterfaces[0].accessConfigs[0].natIP)')

INTERNAL_IP=$(gcloud compute instances describe ${instance} \
  --format 'value(networkInterfaces[0].networkIP)')

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${instance},${EXTERNAL_IP},${INTERNAL_IP} \
  -profile=kubernetes \
  ${instance}-csr.json | cfssljson -bare ${instance}
done
2020/02/29 11:16:58 [INFO] generate received request
2020/02/29 11:16:58 [INFO] received CSR
2020/02/29 11:16:58 [INFO] generating key: rsa-2048
2020/02/29 11:16:58 [INFO] encoded CSR
2020/02/29 11:16:58 [INFO] signed certificate with serial number 359886775191352144581584441997016046901027647498
2020/02/29 11:17:01 [INFO] generate received request
2020/02/29 11:17:01 [INFO] received CSR
2020/02/29 11:17:01 [INFO] generating key: rsa-2048
2020/02/29 11:17:01 [INFO] encoded CSR
2020/02/29 11:17:01 [INFO] signed certificate with serial number 496107270789966248075618951568412965762056311780
2020/02/29 11:17:04 [INFO] generate received request
2020/02/29 11:17:04 [INFO] received CSR
2020/02/29 11:17:04 [INFO] generating key: rsa-2048
2020/02/29 11:17:04 [INFO] encoded CSR
2020/02/29 11:17:04 [INFO] signed certificate with serial number 356879757777462442721200413749147882778190289592

// 各ノード用の証明書(worker-X.pem)と秘密鍵(worker-X-key.pem)が作成されている
~/w/k/h/04 ❯❯❯ ls
admin-csr.json    admin.pem         ca-key.pem        worker-0-csr.json worker-0.pem      worker-1.csr      worker-2-key.pem
admin-key.pem     ca-config.json    ca.csr            worker-0-key.pem  worker-1-csr.json worker-1.pem      worker-2.csr
admin.csr         ca-csr.json       ca.pem            worker-0.csr      worker-1-key.pem  worker-2-csr.json worker-2.pem


Controller Manager〜Scheduler用のクライアント証明書の作成部分は省略します。
(adminユーザー用のクライアント証明書の作成と同じことをしているため)

次に、KubernetesAPI Server用の証明書を作成します。
Kubelet用のクライアント証明書同様、-hostnameでSANsを指定してTLS証明書を作成しています。

特記事項は下記の通りです。

Kubernetesクラスタを作成するとデフォルトでServiceが作成されていますが、その正体はAPI Servier用のService(Cluster IP)です。
内部DNSでは、kubernetes.defaultのホスト名でAレコードが登録されるため、それらのホスト名も指定する必要があります。

kubernetes.io

また、クラスタ内で利用するホスト名(${KUBERNETES_HOSTNAMES})については、別で作成予定のKubernetesのネットワークに関する記事の中で調べた内容をまとめる予定です。

~/w/k/h/04 ❯❯❯ {

KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \
  --region $(gcloud config get-value compute/region) \
  --format 'value(address)')

KUBERNETES_HOSTNAMES=kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.svc.cluster.local

cat > kubernetes-csr.json <<EOF
{
  "CN": "kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=10.32.0.1,10.240.0.10,10.240.0.11,10.240.0.12,${KUBERNETES_PUBLIC_ADDRESS},127.0.0.1,${KUBERNETES_HOSTNAMES} \
  -profile=kubernetes \
  kubernetes-csr.json | cfssljson -bare kubernetes

}
2020/02/29 11:25:41 [INFO] generate received request
2020/02/29 11:25:41 [INFO] received CSR
2020/02/29 11:25:41 [INFO] generating key: rsa-2048
2020/02/29 11:25:41 [INFO] encoded CSR
2020/02/29 11:25:41 [INFO] signed certificate with serial number 603669054680267544191086535903497288645234008053


最後に、サービスアカウントに必要なキーペアを作成します。
Controller Manager(正確にはToken Controller)では、サービスアカウントのAPIリクエストに必要なトークンの生成を行います。
その際に作成したここで作成するキーペアを使用して、サービスアカウント用のトークンを生成して署名します。

qiita.com

~/w/k/h/04 ❯❯❯ {

cat > service-account-csr.json <<EOF
{
  "CN": "service-accounts",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  service-account-csr.json | cfssljson -bare service-account

}
2020/02/29 11:26:20 [INFO] generate received request
2020/02/29 11:26:20 [INFO] received CSR
2020/02/29 11:26:20 [INFO] generating key: rsa-2048
2020/02/29 11:26:20 [INFO] encoded CSR
2020/02/29 11:26:20 [INFO] signed certificate with serial number 372200916003664381145141033359158655751518600952
2020/02/29 11:26:20 [WARNING] This certificate lacks a "hosts" field. This makes it unsuitable for
websites. For more information see the Baseline Requirements for the Issuance and Management
of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org);
specifically, section 10.2.3 ("Information Requirements").


作成が完了した証明書は各ノードに配布します。
(CA証明書を各ノードに配布している理由については後述します。)

~/w/k/h/04 ❯❯❯ for instance in worker-0 worker-1 worker-2; do
  gcloud compute scp ca.pem ${instance}-key.pem ${instance}.pem ${instance}:~/
done
Warning: Permanently added 'compute.7526976075834096736' (ECDSA) to the list of known hosts.
Enter passphrase for key '/Users/.../.ssh/google_compute_engine':
ca.pem                                                                                                                                                 100% 1318    13.7KB/s   00:00
worker-0-key.pem                                                                                                                                       100% 1675    16.2KB/s   00:00
worker-0.pem                                                                                                                                           100% 1493    14.5KB/s   00:00
Warning: Permanently added 'compute.6311120260820139134' (ECDSA) to the list of known hosts.
Enter passphrase for key '/Users/.../.ssh/google_compute_engine':
ca.pem                                                                                                                                                 100% 1318    12.1KB/s   00:00
worker-1-key.pem                                                                                                                                       100% 1675    15.8KB/s   00:00
worker-1.pem                                                                                                                                           100% 1493    11.5KB/s   00:00
Warning: Permanently added 'compute.3606445772456098939' (ECDSA) to the list of known hosts.
Enter passphrase for key '/Users/.../.ssh/google_compute_engine':
ca.pem                                                                                                                                                 100% 1318    12.2KB/s   00:00
worker-2-key.pem                                                                                                                                       100% 1675    16.4KB/s   00:00
worker-2.pem          

~/w/k/h/04 ❯❯❯ for instance in controller-0 controller-1 controller-2; do
  gcloud compute scp ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem \
    service-account-key.pem service-account.pem ${instance}:~/
done
Enter passphrase for key '/Users/.../.ssh/google_compute_engine':
ca.pem                                                                                                                                                 100% 1318    12.9KB/s   00:00
ca-key.pem                                                                                                                                             100% 1675    16.6KB/s   00:00
kubernetes-key.pem                                                                                                                                     100% 1679    16.9KB/s   00:00
kubernetes.pem                                                                                                                                         100% 1663    16.5KB/s   00:00
service-account-key.pem                                                                                                                                100% 1679    15.7KB/s   00:00
service-account.pem                                                                                                                                    100% 1440    13.7KB/s   00:00
Warning: Permanently added 'compute.3758247453048208529' (ECDSA) to the list of known hosts.
Enter passphrase for key '/Users/.../.ssh/google_compute_engine':
ca.pem                                                                                                                                                 100% 1318    13.6KB/s   00:00
ca-key.pem                                                                                                                                             100% 1675    17.3KB/s   00:00
kubernetes-key.pem                                                                                                                                     100% 1679    17.5KB/s   00:00
kubernetes.pem                                                                                                                                         100% 1663    15.9KB/s   00:00
service-account-key.pem                                                                                                                                100% 1679    17.3KB/s   00:00
service-account.pem                                                                                                                                    100% 1440    14.6KB/s   00:00
Warning: Permanently added 'compute.3703518325472787566' (ECDSA) to the list of known hosts.
Enter passphrase for key '/Users/.../.ssh/google_compute_engine':
ca.pem                                                                                                                                                 100% 1318    11.8KB/s   00:00
ca-key.pem                                                                                                                                             100% 1675    16.3KB/s   00:00
kubernetes-key.pem                                                                                                                                     100% 1679    16.7KB/s   00:00
kubernetes.pem                                                                                                                                         100% 1663    15.1KB/s   00:00
service-account-key.pem                                                                                                                                100% 1679    16.6KB/s   00:00
service-account.pem       


Control Planeに関連する証明書 f:id:taxintt:20200315100124p:plain

Worker Nodeに関連する証明書
(図の横幅の都合でWorker-2の記載は省いています) f:id:taxintt:20200315100128p:plain

ここまでがChap4のハンズオンの説明となります。

(+α) 証明書の役割とTLSの概要

そもそも、KubernetesにおいてTLS証明書がどのような役割を果たすかを整理しておきます。
結論から記載すると、KubernetesにおいてTLS証明書は通信の暗号化と各コンポーネントの認証のために利用されています。

kubernetes.io

Kubernetesの公式ドキュメント内のHow certificates are used by your clusterに証明書を利用しているコンポーネントと目的が記載されています。

  • Client certificates for the kubelet to authenticate to the API server
  • Server certificate for the API server endpoint
  • Client certificates for administrators of the cluster to authenticate to the API server
  • Client certificates for the API server to talk to the kubelets
  • Client certificate for the API server to talk to etcd
  • Client certificate/kubeconfig for the controller manager to talk to the API server
  • Client certificate/kubeconfig for the scheduler to talk to the API server

下の方にこのような記載もありましたが、Hard wayの内容に関係ない部分なので割愛します。 (front-proxyとetcdのクラスタ内で利用される証明書に関する説明だと思われます。)

  • Client and server certificates for the front-proxy
  • etcd also implements mutual TLS to authenticate clients and peers


上記から、API server (もしくはAPI Serverと通信するコンポーネント) がTLS証明書を利用していることがわかります。
また、Client certificateServer certificateの2種類のTLS証明書が発行されていることがわかると思います。


TLSの仕様と照らし合わせながら、KubernetesでのTLS証明書の利用用途を説明します。
この記事では、コンポーネントの認証に関連するTLSにおける認証に関する話をメインでまとめます。

TLSの仕様では通信相手 (=サーバー側) の認証が必須となっています。
具体的には、サーバー証明書の正当性検証とサーバーからの署名情報の検証によってサーバの認証(真正性の確認)を行います。
上記の目的のために、サーバー側であるAPI ServerはServer certificateを利用します。

また、TLSではクライアント証明書を利用したクライアント側の認証が可能となっています。
milestone-of-se.nesuke.com

こちらの記事にも、TLSクライアント認証に関する説明が記載されています。 jovi0608.hatenablog.com

このオプションを利用してAPI Serverに対して通信するクライアントの認証を行うため、クライアント側 (=通信元) である各コンポーネントClient certificateを利用します。

TLSの一連のシーケンスについて図示すると下記のようになります。

f:id:taxintt:20200315110117p:plain

証明書の正当性検証と署名情報の検証部分を詳細に図示するとこのようになります。

f:id:taxintt:20200315135645p:plain

サーバー証明書の正当性検証のために、CA証明書(正確には証明書にバンドルされている公開鍵)を利用していることがわかります。
上記のフローを実現するために、手順の中でWorker Nodeに認証局(CA)の証明書 (=ca.pem)を配布する必要があります。

また、署名情報の検証の部分では鍵交換で使うデータを秘密鍵で署名してクライアントに送付し、クライアントはサーバー証明書にバンドルされた公開鍵を用いて署名の検証を行っています。

> for instance in worker-0 worker-1 worker-2; do
     gcloud compute scp ca.pem ${instance}-key.pem ${instance}.pem ${instance}:~/
done

Warning: Permanently added 'compute.7526976075834096736' (ECDSA) to the list of known hosts.
Enter passphrase for key '/Users/.../.ssh/google_compute_engine':

// Worker Node(クライアント側となるNode)に認証局(CA)の証明書 (=ca.pem)を配布している
ca.pem                      
worker-0-key.pem           
worker-0.pem


また、クライアント証明書の検証について図示するとこのようになります。
Client Certificate以前のハンドシェイクのデータを秘密鍵で署名してサーバ側に送付し、サーバ側はクライアント証明書にバンドルされた公開鍵を用いて署名の検証を行っています。

f:id:taxintt:20200315135924p:plain


上記のようなフローにより、クライアント・サーバ側の両者の認証が行われます。
余談ですが、クライアント認証も行うTLSのことをmTLS (Mutual TLS authentication)と言います。(Istioに詳しい方は頻繁に聞く単語かもしれません。)

今回は、Chap4の内容をまとめました。
TLS周りの話は正確に理解する必要があるので、図や参考にした資料のリンクを全て載せておきました。

他に参考にした資料のリンクはこちらです。

TLS, HTTP/2演習

今なぜHTTPS化なのか?インターネットの信頼性のために、技術者が知っておきたいTLSの歴史と技術背景 - エンジニアHub|若手Webエンジニアのキャリアを考える!

パンドラの箱?TLS鍵交換の落とし穴、KCI攻撃とは何か - ぼちぼち日記