探讨下kubernetes的证书体系

序言

一直以来,对kubernetes的身份验证、授权、准入控制似懂非懂。抄起键盘胡乱打,似懂非懂最可怕。所以,利用起周末时间,好好地学习一下。

pem证书和相关命令

pem是什么

PEM: Privacy Enhanced Mail的缩写,以文本的方式进行存储。

  • 以pem格式存储的证书结构
    1
    2
    3
    -----BEGIN CERTIFICATE-----
    Base64编码过的证书数据
    -----END CERTIFICATE-----

kubernetes使用的是.crt和.key后缀类型的证书和密钥。在生成证书的过程中,可以用cfssl生成.pem证书,然后直接命名为.crt。

证书查看命令

1
2
openssl x509 -in xxx.crt -text -noout
cfssl certinfo -cert xxx.crt

证书标准

kubernetes使用X.509数字证书标准。使用kubeadm部署集群的话,可以自动生成证书,但证书有效期只有一到两年。推荐使用kubeadm生成证书用于参考,然后按照kubernetes的证书标准自制证书。

kubeadm和证书类型

生成证书命令:

1
kubeadm init phase certs all --cert-dir=绝对路径

查看生成的证书:

kubeadm主要是生成了etcd和kubernetes本身的证书。使用kubeadm安装集群的过程中,如果我们按照kubernetes标准自制证书,且跳过kubeadm init phase certs步骤,是不是所有场景都会使用到自制证书了呢?
答案是否定的,至少/etc/kubernetes/admin.conf、kubelet.conf、controller-manager.conf、scheduler.conf里面仍会默认生成认证证书和密钥。这些配置主要与user accounts有关,因此我们还要针对这些场景,自制证书,同时对上述文件进行扩展,这是后话。
完整的证书:

可以看出,主要是根证书、证书、密钥。
下面对根证书和证书的内容进行抽象说明,主要参考SSL 数字证书的标准、编码以及文件扩展名

根证书主要内容

使用openssl x509 -in xxx.crt -text -noout查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 0 (0x0) 说明:CA机构给该证书的唯一序列号,根证书为0
Signature Algorithm: sha256WithRSAEncryption 说明:签名算法为SHA-256
Issuer: CN=xxx 说明:证书颁发者的相关信息
Validity 说明:证书生效日期和失效日期
Not Before: xxx
Not After : xxx
Subject: CN=xxx 说明:证书持有者的相关信息
Subject Public Key Info: 说明:服务端公开的密钥
Public Key Algorithm: rsaEncryption 说明:RSA公钥
Public-Key: (2048 bit)
Modulus:
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
xxx, xxx, xxx
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption 说明:数字签名
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:

证书主要内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 12345678901234567890 (0x1234567890abcdef) 说明:CA机构给该证书的唯一序列号
Signature Algorithm: sha256WithRSAEncryption 说明:签名算法为SHA-256
Issuer: CN=xxx 说明:证书颁发者的相关信息
Validity 说明:证书生效日期和失效日期
Not Before: xxx
Not After : xxx
Subject: CN=xxx 说明:证书持有者的相关信息
Subject Public Key Info: 说明:服务端公开的密钥
Public Key Algorithm: rsaEncryption 说明:RSA公钥
Public-Key: (2048 bit)
Modulus:
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
xxx
X509v3 Extended Key Usage:
xxx
X509v3 Subject Alternative Name:
DNS:xxx, IP Address:xxx 说明:支持的DNS和IP
Signature Algorithm: sha256WithRSAEncryption 说明:数字签名
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:

各个证书的区别

注:证书的O、CN字段用于提供RBAC所需的用户组和用户。
kubernetest证书制作标准参考PKI certificates and requirements
另外,参考Key usage extensions and extended key usage,简单说明一下证书中用到的Key Usage和Extended Key Usage。

Key Usage 说明
Digital Signature 当公钥与数字签名机制一起使用时使用,以支持除不可否认、证书签名或CRL签名之外的其他安全服务。数字签名通常用于具有完整性的实体身份验证和数据源身份验证。
Key Encipherment 当证书与用于加密密钥的协议一起使用时使用。一个例子是S/MIME信封,其中使用证书中的公钥加密快速(对称)密钥。SSL协议还执行密钥加密。
Certificate Sign 当subject公钥用于验证证书上的签名时使用。此扩展只能在CA证书中使用。
Extended Key Usage 说明
TLS Web Client Authentication 数字签名和/或密钥协议
TLS Web Server Authentication 数字签名、密钥加密或密钥协议
  • etcd
    注:kubeadm默认生成的证书包含的信息
证书 颁发者信息 持有者信息
ca.crt CN=etcd-ca CN=etcd-ca
healthcheck-client.crt CN=etcd-ca O=system:masters, CN=kube-etcd-healthcheck-client
peer.crt CN=etcd-ca CN=<hostname>
server.crt CN=etcd-ca CN=<hostname>
证书 Key Usage Basic Constraints Extended Key Usage Subject Alternative Name
ca.crt Digital Signature, Key Encipherment, Certificate Sign CA:TRUE - -
healthcheck-client.crt Digital Signature, Key Encipherment - TLS Web Client Authentication -
peer.crt Digital Signature, Key Encipherment - TLS Web Server Authentication, TLS Web Client Authentication DNS:<hostname>, DNS:localhost, IP Address:<Host_IP>, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
server.crt Digital Signature, Key Encipherment - TLS Web Server Authentication, TLS Web Client Authentication DNS:<hostname>, DNS:localhost, IP Address:<Host_IP>, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
  • kubernetes
    注:kubeadm默认生成的证书包含的信息
证书 颁发者信息 持有者信息
ca.crt CN=kubernetes CN=kubernetes
apiserver.crt CN=kubernetes CN=kube-apiserver
apiserver-kubelet-client.crt CN=kubernetes O=system:masters, CN=kube-apiserver-kubelet-client
apiserver-etcd-client.crt CN=etcd-ca O=system:masters, CN=kube-apiserver-etcd-client
front-proxy-ca.crt CN=front-proxy-ca CN=front-proxy-ca
front-proxy-client.crt CN=front-proxy-ca CN=front-proxy-client
sa.pub - -
证书 Key Usage Basic Constraints Extended Key Usage Subject Alternative Name
ca.crt Digital Signature, Key Encipherment, Certificate Sign CA:TRUE - -
apiserver.crt Digital Signature, Key Encipherment - TLS Web Server Authentication DNS:<hostname>, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:集群serviveIP, IP Address:<Host_IP>
apiserver-kubelet-client.crt Digital Signature, Key Encipherment - TLS Web Client Authentication -
apiserver-etcd-client.crt Digital Signature, Key Encipherment - TLS Web Client Authentication -
front-proxy-ca.crt Digital Signature, Key Encipherment, Certificate Sign CA:TRUE - -
front-proxy-client.crt Digital Signature, Key Encipherment - TLS Web Client Authentication -
sa.pub - - - -
  • user accounts
    注:自制证书包含的信息
证书 颁发者信息 持有者信息
admin.crt CN=kubernetes O=system:masters, CN=kubernetes-admin
kubelet.crt CN=kubernetes O=system:nodes, CN=system:node:<nodeName>
scheduler.crt CN=kubernetes CN=system:kube-scheduler
controller-manager.crt CN=kubernetes CN=system:kube-controller-manager
证书 Key Usage Basic Constraints Extended Key Usage Subject Alternative Name Subject Key Identifier Authority Key Identifier
admin.crt Digital Signature, Key Encipherment CA:FALSE TLS Web Client Authentication - xx:xx xx:xx
kubelet.crt Digital Signature, Key Encipherment CA:FALSE TLS Web Client Authentication, TLS Web Server Authentication DNS:localhost, IP Address:127.0.0.1, IP Address:<Host_IP> xx:xx xx:xx
scheduler.crt Digital Signature, Key Encipherment CA:FALSE TLS Web Client Authentication - xx:xx xx:xx
controller-manager.crt Digital Signature, Key Encipherment CA:FALSE TLS Web Client Authentication - xx:xx xx:xx

注意:kubelet的Key Usage需要同时包含TLS Web Client Authentication和TLS Web Server Authentication。具体原因后面介绍。

自制证书

参考PKI certificates and requirementsCFSSL as an external CA for non-ha kubeadm intialized clusters

ectd

  • ca-config.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    {
    "signing": {
    "profiles": {
    "server": {
    "expiry": "876000h",
    "usages": [
    "digital signature",
    "key encipherment",
    "server auth",
    "client auth"
    ]
    },
    "client": {
    "expiry": "876000h",
    "usages": [
    "digital signature",
    "key encipherment",
    "client auth"
    ]
    }
    }
    }
    }

ca

  • ca-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "CN": "etcd-ca",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -initca ca-csr.json | cfssljson -bare ca

healthcheck-client

  • healthcheck-client-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "CN": "kube-etcd-healthcheck-client",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client healthcheck-client-csr.json | cfssljson -bare healthcheck-client

peer

  • peer-csr.json.sed

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "CN": "kube-etcd-peer",
    "hosts": ["localhost","127.0.0.1",<ETCD_NODE_IPS>],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server peer-csr.json | cfssljson -bare peer

server

  • server-csr.json.sed

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "CN": "kube-etcd",
    "hosts": ["localhost","127.0.0.1",<ETCD_NODE_IPS>],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server-csr.json | cfssljson -bare server

kubernetes

  • ca-config.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    {
    "signing": {
    "default": {
    "expiry": "876000h"
    },
    "profiles": {
    "server": {
    "usages": [
    "digital signature",
    "key encipherment",
    "server auth",
    "client auth"
    ],
    "expiry": "876000h"
    },
    "client": {
    "usages": [
    "digital signature",
    "key encipherment",
    "client auth"
    ],
    "expiry": "876000h"
    }
    }
    }
    }

ca

  • ca-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "CN": "kubernetes-ca",
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "ca": {
    "expiry": "876000h"
    }
    }
  • 命令

    1
    cfssl gencert -initca ca-csr.json | cfssljson -bare ca

apiserver

  • apiserver-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "CN": "kube-apiserver",
    "hosts": [
    "localhost",
    "127.0.0.1",
    <MASTER_HOST_IPS>,
    <MASTER_HOST_NAMES>,
    "<CLUSTER_SVC_IP>",
    "<VIP>",
    "kubernetes",
    "kubernetes.default",
    "kubernetes.default.svc",
    "kubernetes.default.svc.cluster",
    "kubernetes.default.svc.cluster.local"
    ],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server apiserver-csr.json | cfssljson -bare apiserver

apiserver指定了-profile=server,但其应该不需要client auth,因为由apiserver主动向etcd、kubelet等发送请求时,会使用额外的证书(apiserver作为客户端),如apiserver-etcd-client.crt、apiserver-kubelet-client.crt。profile server包含client auth,是为了提供给kubelet用的。

apiserver-kubelet-client

  • apiserver-kubelet-client-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "CN": "kube-apiserver-kubelet-client",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [
    {
    "O": "system:masters"
    }
    ]
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client apiserver-kubelet-client-csr.json | cfssljson -bare apiserver-kubelet-client

apiserver-etcd-client

  • apiserver-etcd-client-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "CN": "kube-apiserver-etcd-client",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [
    {
    "O": "system:masters"
    }
    ]
    }
  • 命令

    1
    cfssl gencert -ca=etcd/ca.pem -ca-key=etcd/ca-key.pem -config=etcd/ca-config.json -profile=client apiserver-etcd-client-csr.json | cfssljson -bare apiserver-etcd-client

front-proxy

  • front-proxy-ca-config.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "signing": {
    "default": {
    "expiry": "876000h"
    },
    "profiles": {
    "client": {
    "expiry": "876000h",
    "usages": [
    "digital signature",
    "key encipherment",
    "client auth"
    ]
    }
    }
    }
    }

front-proxy-ca

  • front-proxy-ca-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "CN": "kubernetes-front-proxy-ca",
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "ca": {
    "expiry": "876000h"
    }
    }
  • 命令

    1
    cfssl gencert -initca front-proxy-ca-csr.json | cfssljson -bare front-proxy-ca

front-proxy-client

  • front-proxy-client-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "CN": "front-proxy-client",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -ca=front-proxy-ca.pem -ca-key=front-proxy-ca-key.pem -config=front-proxy-ca-config.json -profile=client front-proxy-client-csr.json | cfssljson -bare front-proxy-client

service account

  • 命令
    1
    2
    openssl genrsa -out sa.key 2048
    openssl rsa -in sa.key -pubout -out sa.pub

user accounts

admin

  • admin-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "CN": "kubernetes-admin",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [
    {
    "O": "system:masters"
    }
    ]
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client admin-csr.json | cfssljson -bare admin

kubelet

  • kubelet-csr.json.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "CN": "system:node:<nodeName>",
    "hosts": [
    "127.0.0.1",
    "localhost",
    "HOST_IP"
    ],
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [
    {
    "O": "system:nodes"
    }
    ]
    }

<nodeName>必须精确匹配由kubelet向apiserver注册时提供的节点名的值。
hosts字段不能为空,否则apiserver向kubelet发请求时kubelet无法验证ip的有效性。

  • 命令
    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server kubelet-csr.json | cfssljson -bare kubelet

注意:kubelet指定的profile需要包含client auth和server auth。具体原因后面介绍。

scheduler

  • scheduler-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "CN": "system:kube-scheduler",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client scheduler-csr.json | cfssljson -bare scheduler

controller-manager

  • controller-manager-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "CN": "system:kube-controller-manager",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    }
    }
  • 命令

    1
    cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client controller-manager-csr.json | cfssljson -bare controller-manager

格式转换

.pem和-key.pem直接转为.crt和.key文件。

1
2
3
4
5
for f in *-key.pem; do mv -v "$f" "${f%-key.pem}.key"; done
for f in *.pem; do mv -v "$f" "${f%.pem}.crt"; done
cd etcd
for f in *-key.pem; do mv -v "$f" "${f%-key.pem}.key"; done
for f in *.pem; do mv -v "$f" "${f%.pem}.crt"; done

配置user accounts证书

为user accounts配置上述自定义证书,参考Configure certificates for user accounts

1
2
3
4
KUBECONFIG=<filename> kubectl config set-cluster default-cluster --server=https://<host ip>:6443 --certificate-authority <path-to-kubernetes-ca> --embed-certs
KUBECONFIG=<filename> kubectl config set-credentials <credential-name> --client-key <path-to-key>.pem --client-certificate <path-to-cert>.pem --embed-certs
KUBECONFIG=<filename> kubectl config set-context default-system --cluster default-cluster --user <credential-name>
KUBECONFIG=<filename> kubectl config use-context default-system

<filename>分别为

  • /etc/kubernetes/admin.conf
  • /etc/kubernetes/kubelet.conf
  • /etc/kubernetes/controller-manager.conf
  • /etc/kubernetes/scheduler.conf

kubernetes认证场景

通过上述证书内容和csr.json,可知kubernetes内部常用的加解密算法为非对称加密算法RSA。接下来具体分析一些场景下的认证过程。首先介绍一下https双向认证过程。

https双向认证

参考图解SSL/TLS认证流程

kubectl

kubectl与apiserver通信,以控制和获取集群资源。这是一个双向认证过程。kubectl要验证apiserver的证书有效性,apiserver也要验证kubectl证书有效性。

默认证书

kubectl使用~/.kube/config配置文件,该文件拷贝自/etc/kubernetes/admin.conf。
需要关注的字段主要是:

  • certificate-authority-data
    用于验证apiserver发过来的证书。
  • client-certificate-data
    kubectl发送的证书,apiserver会对其进行验证。
  • client-key-data
    kubectl使用的密钥。

将内容进行base64 -dopenssl x509 -in xxx.crt -text -noout即可查看证书内容。可以发现,默认情况下的证书和密钥是kubeadm生成的,有效期只有一年。参考配置user accounts证书对其进行自定义。

通信过程

抓包研究下kubectl和apiserver的通信握手过程。apiserver监听端口为6443。((tcp[12] & 0xf0) >> 2)表示TCP头的大小。TLS包的第一个字节定义内容类型,值22(十六进制中的0x16)被定义为“握手”内容。

1
tcpdump -ni any "tcp port 6443 and (tcp[((tcp[12] & 0xf0) >> 2)] = 0x16)" -w test.sap

执行kubectl get svc,从而捕抓到kubectl和apiserver之间的握手包。
用wireshark查看test.sap。

可以发现,kubectl首先向apiserver发送了client hello报文,apiserver响应hello报文之后,发送自己的证书,并发送客户端证书请求。kubectl收到证书请求后,发送自己的证书,并验证apiserver证书的有效性。
具体的证书内容可以通过wireshark查看。

apiserver使用了--client-ca-file字段指定了用于认证客户端证书的根证书。kubectl则是通过--certificate-authority-data验证apiserver的证书。简单修改下certificate-authority-data的内容,测试一下。

1
2
# kubectl get pods
Unable to connect to the server: x509: certificate signed by unknown authority

查看抓包情况:

可以发现,apiserver的证书并没有通过kubectl的验证。

tls+rbac

参考使用kubectl访问Kubernetes集群时的身份验证和授权,apiserver负责集群访问权限控制,访问权限控制由身份验证(authentication)、授权(authorization)和准入控制(admission control)三步骤按序进行。身份验证支持的方法包括:client证书验证(https双向验证)、basic auth、普通token以及jwt token(用于serviceaccount)。授权支持的方式包括Node、RBAC、Webhook。

身份信息

kubectl的身份信息在client-certificate的数据中。从~/.kube/config中获取证书的持有者信息:

1
Subject: O=system:masters, CN=kubernetes-admin

证书的O、CN字段用于提供RBAC所需的用户组和用户。起作用的主要是group = system:masters
参考User-facing Roles,可知kubernetes默认创建了cluster-admin的ClusterRole,并且绑定了group = system:masters

1
2
kubectl get clusterrolebinding/cluster-admin -o yaml
kubectl get clusterrole/cluster-admin -o yaml

可以发现,apiGroups、resources、verbs等都被授予了全部权限。

注:经过试验,修改了该场景下的CN后(即重新自定义证书),kubectl可以正常工作,因此在该场景下,CN应该没有发挥什么作用。

kubelet

kubelet.conf

kubelet.conf用于指明如何与apiserver进行通信。
需要关注的字段主要是:

  • certificate-authority-data
    用于验证apiserver发过来的证书。
  • client-certificate-data
    kubelet发送的证书,apiserver会对其进行验证。
  • client-key-data
    kubelet使用的密钥。

默认情况下,通过kubeadm init phase kubeconfig kubelet --config=kubeadm.yaml生成的kubelet.conf将会包含有效期为一年的证书,默认使用了/etc/kubernetes/pki/ca.crt。在配置user accounts证书一节,通过参数指定可以修改证书。
如果kubelet.conf的certificate-authority-data根证书内容有误,kubelet将会启动失败,出现类似以下错误日志:

1
Unable to register node "xxx.xxx.xxx.xxx" with API server: Post https://xxx.xxx.xxx.xxx:6443/api/v1/nodes: x509: certificate signed by unknown authority

apiserver则将出现类似以下错误日志:

1
http: TLS handshake error from xxx.xxx.xxx.xxx:48718: remote error: tls: bad certificate

如果kubelet.conf的client-certificate-data证书或client-key-data密钥内容有误,kubelet将会启动失败,出现类似以下错误日志:

1
2
Unable to load TLS configuration from existing bootstrap client config: tls: failed to find any PEM data in certificate input
Unable to load TLS configuration from existing bootstrap client config: tls: failed to find any PEM data in key input

这种场景下是无法使用kubectl log命令的:

1
Error from server: Get https://xxx.xxx.xxx.xxx:10250/containerLogs/kube-system/kube-apiserver-xxx.xxx.xxx.xxx/kube-apiserver?follow=true: dial tcp xxx.xxx.xxx.xxx:10250: connect: connection refused

参考apiserver-kubelet,可知通过kubectl查看log需要由apiserver连接到kubelet。

client-certificate-data和client-key-data也可以直接指向文件路径:

1
2
3
4
5
users:
- name: default-auth
user:
client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem
client-key: /var/lib/kubelet/pki/kubelet-client-current.pem

参考Kubernetes TLS bootstrapping那点事,可知kubelet-client-current.pem是一个软连接文件,当kubelet配置了--feature-gates=RotateKubeletClientCertificate=true选项后,会在证书总有效期的70%~90%的时间内发起续期请求,请求被批准后会生成一个/var/lib/kubelet/pki/kubelet-client-时间戳.pem。kubelet-client-current.pem文件则始终软连接到最新的真实证书文件,除首次启动外,kubelet一直会使用这个证书同apiserver通讯。参考kubelet,可知RotateKubeletClientCertificate参数默认为true。
查看kubelet-client-current.pem文件:

1
2
3
4
5
6
-----BEGIN CERTIFICATE-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----
-----BEGIN EC PRIVATE KEY-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END EC PRIVATE KEY-----

使用openssl x509 -in ./cert -text -noout查看证书内容,可以发现公钥加密算法为id-ecPublicKey,而不是rsaEncryption。

kubelet.conf vs kubelet-client-current.pem

既然kubelet.conf和kubelet-client-current.pem都用作kubelet和apiserver通信的证书,那两者之间的关系是什么?
基于kubelet启动的过程中需要向apiserver进行节点注册,以该场景为例进行抓握手包:

1
tcpdump -ni any "tcp port 6443 and (tcp[((tcp[12] & 0xf0) >> 2)] = 0x16)" -w test.sap

然后重启kubelet:

1
systemctl restart kubelet

查看握手包的Certificate的serialNumber,对比kubelet.conf和kubelet-client-current.pem证书中的serialNumber,发现默认情况下,kubelet是以kubelet-client-current.pem的内容与apiserver通信的。
由此,猜测kubelet.conf类似于备胎的作用,当不进行client证书轮转更新时,使用kubelet使用kubelet.conf与apiserver进行通信。以下进行实验证实:

  • 修改/var/lib/kubelet/kubeadm-flags.env,添加--feature-gates=RotateKubeletClientCertificate=false --rotate-certificates=false
  • systemctl restart kubelet

再次抓包,可以发现握手包的Certificate的serialNumber等于kubelet.conf证书中的serialNumber,猜测成立。

证书问题

实验过程中,实际上是出现了一些奇怪的问题。如果开启RotateKubeletClientCertificate的话,每次重启kubelet,若kubelet-client-current.pem不存在,则会自动创建新的证书文件(通过certificatesigningrequests请求),并将kubelet-client-current.pem指向它。

kubelet-client-current.pem内容错误

实验过程:

  • systemctl stop kubelet
  • rm /var/lib/kubelet/pki/kubelet-client-current.pem
  • systemctl restart kubelet
  • journalctl -xeu kubelet

发现kubelet无法注册节点,apiserver出现以下错误日志:

1
Unable to authenticate the request due to an error: [x509: certificate signed by unknown authority, x509: certificate specifies an incompatible key usage]

查看kubelet-client-current.pem的证书内容,发现kubelet-client-current.pem的证书内容为kubelet.conf的证书内容。比对之前的证书内容,发现两者的差异是:

  • kubelet-client-current.pem:X509v3 Extended Key Usage为TLS Web Server Authentication
  • 之前的有效证书:X509v3 Extended Key Usage为TLS Web Client Authentication

猜测kubernetes在生成kubelet-client-current.pem的时候,若kubelet.conf的证书Key Usage包含Client Authentication,则轮转生成全新的证书内容(采用公钥加密算法为id-ecPublicKey);否则kubelet-client-current.pem直接引用kubelet.conf的证书内容。该猜测经过实验已证实。
在这个场景中,我们生成kubelet.crt的时候,指定了-profile=server,且该profile不包含client auth的usages;由该证书所生成的kubelet.conf、由该kubelet.conf所生成的kubelet-client-current.pem,并不满足认证要求。

查看log时apiserver和kubelet的验证

借助kubelet-client-current.pem证书出错(即只包含server auth)这个场景,验证下kubectl log的执行过程:

1
kubectl log kube-apiserver-xxx.xxx.xxx.xxx -n kube-system -f

输出以下日志:

1
Error from server (InternalError): Internal error occurred: Authorization error (user=kube-apiserver-kubelet-client, verb=get, resource=nodes, subresource=proxy)

抓取kubectl<->apiserver和apiserver<->kubelet的握手包:

1
2
tcpdump -ni any "tcp port 6443 and (tcp[((tcp[12] & 0xf0) >> 2)] = 0x16)" -w 6443.sap
tcpdump -ni any "tcp port 10250 and (tcp[((tcp[12] & 0xf0) >> 2)] = 0x16)" -w 10250.sap

继续执行kubectl log,查看握手包,发现:

  • kubectl使用~/.kube/config(O=system:masters, CN=kubernetes-admin)的证书与apiserver通信,apiserver使用/etc/kubernetes/pki/apiserver.crt与kubectl通信(CN=kube-apiserver)。
  • apiserver使用/etc/kubernetes/pki/apiserver-kubelet-client.crt与kubelet通信,kubelet使用/etc/kubernetes/pki/kubelet.crt与apiserver通信。

考虑到引用kubelet.crt的地方,是/var/lib/kubelet/config.yaml中的tlsCertFile和tlsPrivateKeyFile,这两个字段是由kubeadm.yaml指定的:

1
2
3
4
5
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
address: "HOST_IP"
tlsCertFile: KUBELET_CERT
tlsPrivateKeyFile: KUBELET_KEY

若不指定,将生成自签名证书/var/lib/kubelet/pki/kubelet.crt和私钥/var/lib/kubelet/pki/kubelet.key。此处我们指定为自制证书路径/etc/kubernetes/pki/kubelet.crt和/etc/kubernetes/pki/kubelet.key。将其修改为临时生成的证书路径,然后执行kubectl log,抓取10250端口的握手包,可以发现kubelet使用临时证书与apiserver通信。

在上一节中,kubelet向apiserver发请求注册节点,kubelet作为客户端,因此其证书需要包含client auth。在本节中,apiserver向kubelet发请求查看日志,kubelet作为服务端,其证书不但需要包含server auth,还需要包含host字段,用于验证apiserver所访问的ip。
若kubelet.crt不包含hosts字段,则apiserver可能报错:

1
Error from server: Get https://xxx.xxx.xxx.xxx:10250/containerLogs/kube-system/kube-apiserver-xxx.xxx.xxx.xxx/kube-apiserver?follow=true: x509: cannot validate certificate for xxx.xxx.xxx.xxx because it doesn't contain any IP SANs

若kubelet.crt不包含server auth,则apiserver可能报错:

1
Error from server: Get https://xxx.xxx.xxx.xxx:10250/containerLogs/kube-system/kube-apiserver-xxx.xxx.xxx.xxx/kube-apiserver?follow=true: x509: certificate specifies an incompatible key usage

因此,需要更新kubelet.crt,保证其同时包含client auth(确保正确生成kubelet-client-current.pem)和server auth(用于tlsCertFile),同时证书hosts字段包含节点ip。

身份信息

假设kubelet.crt同时包含client auth和server auth。获取kubelet证书的持有者信息:

1
Subject: O=system:nodes, CN=system:node:<nodeName>

修改CN=test,生成证书,并修改kubelet.conf,可以发现以下现象:
kubelet发出的证书请求被拒绝,无法向apiserver查阅资源:

1
2
3
Failed while requesting a signed certificate from the master: cannot create certificate signing request: certificatesigningrequests.certificates.k8s.io is forbidden: User "test" cannot create resource "certificatesigningrequests" in API group "certificates.k8s.io" at the cluster scope

Failed to list *v1.Node: nodes "xxx.xxx.xxx.xxx" is forbidden: User "test" cannot list resource "nodes" in API group "" at the cluster scope

kubectl log出现认证错误:

1
Error from server (InternalError): Internal error occurred: Authorization error (user=kube-apiserver-kubelet-client, verb=get, resource=nodes, subresource=proxy)

此时,kubelet-client-current.pem没有自动轮转,而是采用了kubelet.conf的内容。

修改O=test,生成证书,并修改kubelet.conf,可以发现以下现象:
kubelet发出的证书请求被拒绝,无法向apiserver查阅资源:

1
2
3
Failed while requesting a signed certificate from the master: cannot create certificate signing request: certificatesigningrequests.certificates.k8s.io is forbidden: User "system:nodes:xxx.xxx.xxx.xxx" cannot create resource "certificatesigningrequests" in API group "certificates.k8s.io" at the cluster scope

Failed to list *v1.Node: nodes "xxx.xxx.xxx.xxx" is forbidden: User "system:nodes:xxx.xxx.xxx.xxx" cannot list resource "nodes" in API group "" at the cluster scope

kubectl log出现认证错误:

1
Error from server (InternalError): Internal error occurred: Authorization error (user=kube-apiserver-kubelet-client, verb=get, resource=nodes, subresource=proxy)

此时,kubelet-client-current.pem没有自动轮转,而是采用了kubelet.conf的内容。

由此可以发现,kubelet的证书需要同时满足CN和O的定义形式。具体可以参考Using Node AuthorizationUsing RBAC Authorization的说明。

kube-controller-manager

相关的启动参数:

1
2
3
4
5
6
7
8
9
10
--authentication-kubeconfig=/etc/kubernetes/controller-manager.conf,
--authorization-kubeconfig=/etc/kubernetes/controller-manager.conf,
--client-ca-file=/etc/kubernetes/pki/ca.crt,
--cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt,
--cluster-signing-key-file=/etc/kubernetes/pki/ca.key,
--kubeconfig=/etc/kubernetes/controller-manager.conf,
--requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt,
--root-ca-file=/etc/kubernetes/pki/ca.crt,
--service-account-private-key-file=/etc/kubernetes/pki/sa.key,
--use-service-account-credentials=true

controller-manager.conf用于与apiserver通信,其中根证书部分用于验证apiserver证书,证书密钥部分用于发送给apiserver。可以修改controller-manager.conf并通过重启docker进行验证。
若根证书无效,则kube-controller-manager日志会出现以下日志:

1
error retrieving resource lock kube-system/kube-controller-manager: Get https://xxx.xxx.xxx.xxx:6443/api/v1/namespaces/kube-system/endpoints/kube-controller-manager?timeout=10s: x509: certificate signed by unknown authority

至于controller-manager与apiserver的通信,可以通过抓包查看,并重启docker:

1
tcpdump -ni any "src host xxx.xxx.xxx.xxx and dst host xxx.xxx.xxx.xxx and tcp port 6443 and (tcp[((tcp[12] & 0xf0) >> 2)] = 0x16)" -w 6443.sap

关于service-account,参考管理Service Accounts,通过--service-account-private-key-file参数项传入一个服务账户私钥文件至Token管理器,私钥用于为生成的服务账户token签名。同样地,通过--service-account-key-file参数将对应的公钥传入kube-apiserver,公钥用于认证过程中的token校验。
我们可以通过创建serviceaccount的方式,并查看对应生成的secret,然后在jwt.io上查看token内容,以及进行签名验证。

access-token vs cert

实验场景,获取controller-manager metrics:

1
2
curl https://xxx.xxx.xxx.xxx:10257/metrics --cert /etc/kubernetes/pki/controller-manager.crt --key /etc/kubernetes/pki/controller-manager.key -k
curl https://xxx.xxx.xxx.xxx:10257/metrics --header "Authorization: Bearer $TOKEN" -k

得出以下结论:

  • 服务端下发的是自签名的根证书和证书
  • 客户端证书需要包含client auth
  • 以controller-manager.crt为证书,User system:kube-controller-manager需要与已存在的system:auth-delegator ClusterRole进行绑定,不然会报以下错误:Internal Server Error: "/metrics": subjectaccessreviews.authorization.k8s.io is forbidden: User "system:kube-controller-manager" cannot create resource "subjectaccessreviews" in API group "authorization.k8s.io" at the cluster scope
  • 以controller-manager.crt为证书,还需要为User system:kube-controller-manager赋予metrics的访问权限
  • 以apiserver-kubelet-client.crt为证书,不需要绑定system:auth-delegator ClusterRole,因为证书指定了O=system:masters,具备admin权限
  • 以apiserver-etcd-client.crt为证书,Unauthorized,因为签发的根证书是etcd根证书
  • 以secret admin-user为token的话,User system:kube-controller-manager需要与已存在的system:auth-delegator ClusterRole进行绑定
  • 以secret default为token的话,User system:kube-controller-manager需要与已存在的system:auth-delegator ClusterRole进行绑定,同时需要为ServiceAccount default赋予metrics的访问权限

以上实验参考Securing Controller Manager and Scheduler Metrics,主要的配置内容如下:
User system:kube-controller-manager与已存在的system:auth-delegator ClusterRole进行绑定

1
2
3
4
5
6
7
8
9
10
11
12
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: system:kube-controller-manager:auth-delegate
subjects:
- kind: User
name: system:kube-controller-manager
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: system:auth-delegator
apiGroup: rbac.authorization.k8s.io

secret default:default(<namespace>:<service-account>)授权/metrics nonResourceURLs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secure-metrics-scrape
rules:
- apiGroups:
- ""
resources:
- nodes/metrics
verbs:
- get
- nonResourceURLs:
- /metrics
verbs:
- get

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: metrics-endpoint
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: secure-metrics-scrape
subjects:
- kind: ServiceAccount
name: default
namespace: default

获取default:default token

1
TOKEN=$(kubectl describe secret $(kubectl get secrets -n default | grep ^default | cut -f1 -d ' ') | grep -E '^token' | cut -f2 -d':' | tr -d " ")

为User system:kube-controller-manager赋予metrics的访问权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secure-metrics-scrape
rules:
- apiGroups:
- ""
resources:
- nodes/metrics
verbs:
- get
- nonResourceURLs:
- /metrics
verbs:
- get

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: metrics-endpoint
subjects:
- kind: User
name: system:kube-controller-manager
apiGroup: rbac.authorization.k8s.io
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: secure-metrics-scrape

front-proxy-client

front-proxy-client的相关配置主要是用于aggregated apiserver的。
apiserver关于front-proxy-client的相关参数:

1
2
3
4
5
6
7
--proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
--proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
--requestheader-allowed-names=front-proxy-client
--requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
--requestheader-extra-headers-prefix=X-Remote-Extra-
--requestheader-group-headers=X-Remote-Group
--requestheader-username-headers=X-Remote-User

aggregated apiserver

aggregated apiserver即聚合apiserver,用于解耦apiserver,集成用户自定义apiserver。这些apiserver可以跟core apiserver无缝衔接,使用kubectl也可以管理它们。可参考Kubernetes API Aggregation Setup

使用kubectl访问kubernetes API

参考APIServiceSpec,apiservice的service字段表明:service是对该api server的服务引用,必须在端口443上通信。如果服务为空,这意味着api groupversion的处理将在此服务器上本地处理。调用将简单地委托给要执行的常规处理程序链。
查询所有api:

1
kubectl get apiservice

查询指定api:

1
kubectl get apiservice v1beta1.events.k8s.io -o yaml

访问api:

1
kubectl get --raw=/apis/events.k8s.io/v1beta1

apiservice的注册参考Register APIService objects

client-ca-file vs requestheader-client-ca-file

参考CA Reusage and Conflicts

  • client-ca-file
    当请求到达apiserver时,如果开启client-ca-file选项,apiserver将检查请求的证书。如果证书是由client-ca-file所指定的ca证书签发的,请求将被视为合法的。user将取自CN=字段,group将取自O=字段。
  • requestheader-client-ca-file
    当请求到达apiserver时,如果开启requestheader-client-ca-file选项,apiserver将检查请求的证书。如果证书是由requestheader-client-ca-file所指定的ca证书签发的,请求将被视为潜在的合法请求。apiserver将检查证书的CN=是否在requestheader-allowed-names列表中,如果在,允许该请求,否则拒绝。

如果同时指定两个选项,apiserver先检查requestheader-client-ca-file根证书,再检查client-ca-file根证书。常规的客户端请求匹配client-ca-file,聚合请求匹配requestheader-client-ca-file。若两者指定了相同的根证书,则可能会引发问题:通常情况下,client request证书能通过client-ca-file验证,在client-ca-file和requestheader-client-ca-file指定了相同的根证书的情况下,client request证书将匹配requestheader-client-ca-file,但CN=通常不匹配requestheader-allowed-names,导致无法通过apiserver进行身份验证。(常规的客户端请求证书由于不匹配requestheader-client-ca-file,所以会尝试匹配client-ca-file)

认证流程

参考Authentication Flow,大致流程如下:

  • Kubernetes apiserver:对请求用户进行身份验证,并授权他们对请求的API路径的权限。
  • Kubernetes apiserver:将请求代理到扩展apiserver
  • 扩展apiserver:验证来自Kubernetes apiserver的请求
    当我们配置了apiserver关于front-proxy-client的相关参数,kubernetes将会在kube-system命名空间下创建extension-apiserver-authenticationconfigmap,扩展apiservers可以通过该configmap来验证请求。
    这需要为扩展apiservers绑定适当的角色,默认的角色是kube-system命名空间下的extension-apiserver-authentication-reader
  • 扩展apiserver:授权来自原始用户的请求
    扩展apiserver通过向Kubernetes apiserver发送一个标准的SubjectAccessReview请求来验证用户/组是否被授权执行给定的请求。
    这需要为扩展apiservers绑定适当的角色,Kubernetes包含一个默认的ClusterRolesystem:authdelegate-delegate,它可以被授予扩展apiserver的服务帐户。
  • 扩展apiserver:执行

apiserver认证源码实现

在以实际例子验证上述认证流程之前,这里讲解一下apiserver,已解答实际验证过程中的一些问题。

指定http处理链

创建apiserver配置信息,配置信息中指定默认的http处理链。
代码片段:

1
2
3
4
5
6
7
8
9
10
11
//func buildGenericConfig(s *options.ServerRunOptions, proxyTransport *http.Transport)
genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)

//func NewConfig(codecs serializer.CodecFactory)
return &Config{
BuildHandlerChainFunc: DefaultBuildHandlerChain,
...
}

//func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config)
handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)

参考:genericapiserver.NewConfigDefaultBuildHandlerChaingenericapifilters.WithAuthentication

构建身份验证器

上述请求处理链绑定genericapifilters.WithAuthentication,然后传递c.Authentication.Authenticator身份验证器。这个身份验证器是一个union authenticator,包含了不同的认证场景下的不同的处理方法。
代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//func buildGenericConfig(s *options.ServerRunOptions, proxyTransport *http.Transport)
genericConfig.Authentication.Authenticator, genericConfig.OpenAPIConfig.SecurityDefinitions, err = BuildAuthenticator(s, clientgoExternalClient, versionedInformers)

//func BuildAuthenticator(s *options.ServerRunOptions, extclient clientgoclientset.Interface, versionedInformer clientgoinformers.SharedInformerFactory)
authenticatorConfig := s.Authentication.ToAuthenticationConfig()
...
return authenticatorConfig.New()

//func (config Config) New()
if config.RequestHeaderConfig != nil {
requestHeaderAuthenticator, err := headerrequest.NewSecure(
config.RequestHeaderConfig.ClientCA,
config.RequestHeaderConfig.AllowedClientNames,
config.RequestHeaderConfig.UsernameHeaders,
config.RequestHeaderConfig.GroupHeaders,
config.RequestHeaderConfig.ExtraHeaderPrefixes,
)
if err != nil {
return nil, nil, err
}
authenticators = append(authenticators, authenticator.WrapAudienceAgnosticRequest(config.APIAudiences, requestHeaderAuthenticator))
}
...
if len(config.ClientCAFile) > 0 {
certAuth, err := newAuthenticatorFromClientCAFile(config.ClientCAFile)
if err != nil {
return nil, nil, err
}
authenticators = append(authenticators, certAuth)
}
...
authenticator := union.New(authenticators...)

参考:genericConfig.Authentication.AuthenticatorBuildAuthenticatorauthenticatorConfig.Newheaderrequest.NewSecureNewSecureunion.New

验证过程

请求处理链绑定genericapifilters.WithAuthentication,然后传递c.Authentication.Authenticator身份验证器。实际调用Authenticator的AuthenticateRequest方法,而Authenticator是我们的union。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler
handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)

//func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences) http.Handler
resp, ok, err := auth.AuthenticateRequest(req)
if err != nil || !ok {
if err != nil {
klog.Errorf("Unable to authenticate the request due to an error: %v", err)
}
failed.ServeHTTP(w, req)
return
}

//func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error)
for _, currAuthRequestHandler := range authHandler.Handlers {
resp, ok, err := currAuthRequestHandler.AuthenticateRequest(req)
if err != nil {
if authHandler.FailOnError {
return resp, ok, err
}
errlist = append(errlist, err)
continue
}

if ok {
return resp, ok, err
}
}

return nil, false, utilerrors.NewAggregate(errlist)

每接收一个请求时,则遍历union authenticator的处理方法,一旦通过验证就跳出循环。union.AuthenticateRequest调用每个handler的AuthenticateRequest,按照func (config Config) New()的指定顺序,分别是Front proxy、Basic auth、Cert、Token auth、Service account token、Bootstrap token、OIDC、Webhook…
这里我们关注一下Front proxy的处理流程,首先验证证书的合法性,然后判断CN是否在allowedCommonNames里面,最后返回到上层验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//func (a *Verifier) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error)
if _, err := req.TLS.PeerCertificates[0].Verify(optsCopy); err != nil {
return nil, false, err
}
if err := a.verifySubject(req.TLS.PeerCertificates[0].Subject); err != nil {
return nil, false, err
}
return a.auth.AuthenticateRequest(req)

//func (a *Verifier) verifySubject(subject pkix.Name) error
if len(a.allowedCommonNames) == 0 {
return nil
}
if a.allowedCommonNames.Has(subject.CommonName) {
return nil
}
return fmt.Errorf("x509: subject with cn=%s is not in the allowed list", subject.CommonName)

上层验证实际上是尝试从头部信息中获取name、group、extra信息:

1
2
3
4
5
6
7
//func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error)
name := headerValue(req.Header, a.nameHeaders)
if len(name) == 0 {
return nil, false, nil
}
groups := allHeaderValues(req.Header, a.groupHeaders)
extra := newExtra(req.Header, a.extraHeaderPrefixes)

可以发现,如果CN不在allowedCommonNames里,则会报错not in the allowed list,返回nil, false, err,由于err不为空,union.AuthenticateRequest将继续使用下一个handler进行验证,并将错误信息登记输出。
如果通过了front-proxy的证书认证,但是头部信息中没有包含name的话,验证也是失败的,返回nil, false, nil,由于err为空,union.AuthenticateRequest将继续使用下一个handler进行验证,但没有将错误信息登记输出。
参考:genericapifilters.WithAuthenticationWithAuthenticationunion.AuthenticateRequestVerifier.AuthenticateRequestrequestHeaderAuthRequestHandler.AuthenticateRequest

验证认证流程

案例

这里采取的案例是prometheus-operator,它将会创建一个apiservice。

1
2
3
4
kubectl get apiservice

===output
v1beta1.metrics.k8s.io monitoring/prometheus-adapter True 18d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kubectl get apiservice v1beta1.metrics.k8s.io -o yaml

===output
apiVersion: apiregistration.k8s.io/v1
kind: APIService
spec:
group: metrics.k8s.io
groupPriorityMinimum: 100
insecureSkipTLSVerify: true
service:
name: prometheus-adapter
namespace: monitoring
version: v1beta1
versionPriority: 100

开启aggregator路由请求

尝试以下操作,默认出错:

1
2
3
4
5
6
7
8
9
kubectl api-resources
error: unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: Unauthorized

kubectl get --raw "/apis/metrics.k8s.io/v1beta1"
error: You must be logged in to the server (Unauthorized)

kubectl get <API_RESOURCE_NAME>.<API_VERSION>.<API_GROUP>
kubectl get pods.v1beta1.metrics.k8s.io
error: the server doesn't have a resource type "pods

参考CA Reusage and Conflicts,这是因为apiserver默认没有开启--enable-aggregator-routing=true,该参数的作用是打开到endpoints IP的aggregator路由请求,替换cluster IP。将其添加到/etc/kubernetes/manifests/kube-apiserver.yaml(kubeadm安装的集群),重启kubelet,apiserver可能就地重启,所以可以通过docker stop/rm apiserver容器强制创建新服务。

通过curl方式验证

上述kubectl get pods.v1beta1.metrics.k8s.io方式实际上是发送了以下请求:

1
2
//kubectl get pods.v1beta1.metrics.k8s.io --v=9
curl -k -v -XGET -H "Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io, application/json" -H "User-Agent: kubectl/v1.13.0 (linux/amd64) kubernetes/ddf47ac" 'https://xxx.xxx.xxx.xxx:6443/apis/metrics.k8s.io/v1beta1/namespaces/default/pods?limit=500'

这个请求实际上是使用了~/.kube/config(admin.conf)的证书,过的是client-ca-file验证,而不是requestheader-client-ca-file。
以下验证requestheader-client-ca-file的方式:
问题1:证书签发错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl -k -XGET 'https://xxx.xxx.xxx.xxx:6443/apis/metrics.k8s.io/v1beta1/namespaces/default/pods?limit=500' --cert /etc/kubernetes/pki/front-proxy-client.crt --key /etc/kubernetes/pki/front-proxy-client.key

===output
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {

},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}
===apiserver log
Unable to authenticate the request due to an error: [x509: subject with cn=kube-front-client is not in the allowed list, x509: certificate signed by unknown authority]

/etc/kubernetes/pki/front-proxy-client.crt签发时使用了错误的CN。需要重新签发证书或将kube-front-client设置到--requestheader-allowed-names

问题2:没有指定username
重新签发证书后继续上述步骤,依然是Unauthorized,但是apiserver日志只输出:

1
Unable to authenticate the request due to an error: x509: certificate signed by unknown authority

根据上一节的内容,我们知道这是由于请求头部没有带上username,front proxy认证失败,继续检查认证时由于front-proxy-client.crt不是由apiserver指定的client-ca-file所签发,因此报错。
执行以下命令:

1
curl -k -XGET -H "X-Remote-User:front-proxy-client" 'https://xxx.xxx.xxx.xxx:6443/apis/metrics.k8s.io/v1beta1/namespaces/default/pods?limit=500' --cert /etc/kubernetes/pki/front-proxy-client.crt --key /etc/kubernetes/pki/front-proxy-client.key

问题3:没有权限
上述步骤将产生权限不足的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {

},
"status": "Failure",
"message": "pods.metrics.k8s.io is forbidden: User \"front-proxy-client\" cannot list resource \"pods\" in API group \"metrics.k8s.io\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"group": "metrics.k8s.io",
"kind": "pods"
},
"code": 403
}

使用以下文件进行授权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secure-metrics-scrape
rules:
- apiGroups:
- metrics.k8s.io
resources:
- pods
verbs:
- "*"

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: metrics-endpoint
subjects:
- kind: User
name: front-proxy-client
apiGroup: rbac.authorization.k8s.io
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: secure-metrics-scrape

再次执行上述命令,将成功获得输出。

抓包验证:
进入prometheus-adapter容器,查看设备,设备号为33:

1
cat /sys/class/net/eth0/iflink

进入容器所在宿主机,查看设备对,假设为calixxxxx:

1
2
3
ip link |grep ^33

for i in /sys/class/net/*/ifindex; do grep -l 33 $i; done

docker stop/rm强制删除apiserver,然后重新curl(避免apiserver使用已有连接,从而捕捉不到握手包),抓握手包:

1
tcpdump -i calixxxxx "(tcp[((tcp[12] & 0xf0) >> 2)] = 0x16)" -w 6443.sap

由于启用了calico和ipvs,查看握手包,可以发现实际是tunnel向prometheus-adapter pod通信。通信双方client是发送了front-proxy-client证书,而server端则是发送临时签发的证书。

开启caBundle:
若apiservice开启caBundle,添加caBundle之后发送curl指令:

1
2
3
4
curl:Error: 'x509: certificate is valid for localhost, localhost, not prometheus-adapter.monitoring.svc'

prometheus-adapter的pod日志,其中xxx.xxx.xxx.xxx是IPIP Tunnel的ip:
http: TLS handshake error from xxx.xxx.xxx.xxx:40812: remote error: tls: bad certificate

证书或配置及其作用

证书或配置 目前已知作用
*.conf相关
~/.kube/config(admin.conf) 与apiserver通信时kubectl使用的证书(apiserver使用apiserver.crt)
kubelet.conf key usage不包含client auth、O或CN不符合规定时,会导致kubelet-client-current.pem直接采用其证书内容(在kubelet-client-current.pem不存在的情况下)
controller-manager.conf 与apiserver通信时kube-controller-manager使用的证书(apiserver使用apiserver.crt)
scheduler.conf -
kubelet相关
/var/lib/kubelet/pki/kubelet-client-current.pem kubelet向apiserver注册节点、获取资源信息等操作时kubelet使用的证书(apiserver使用apiserver.crt)
kubelet.crt 由kubectl log触发的apiserver与kubelet通信时,kubelet使用的证书(tlsCertFile字段指定)
apiserver-kubelet-client.crt 由kubectl log触发的apiserver与kubelet通信时,apiserver使用的证书
apiserver相关
apiserver.crt kubectl与apiserver通信时apiserver使用的证书;kubelet向apiserver注册节点、获取资源信息等操作时apiserver使用的证书;kube-controller-manager与apiserver通信时apiserver使用的证书
apiserver-etcd-client.crt -
front-proxy相关
front-proxy-client.crt 用于扩展apiserver的认证
service account相关
sa.pub apiserver用于认证过程中的token校验
etcd相关
healthcheck-client.crt -
peer.crt -
server.crt -

access token

通过上文可知,kube-controller-manager通过--service-account-private-key-file参数项传入一个服务账户私钥文件至Token管理器,私钥用于为生成的服务账户token签名。同样地,通过--service-account-key-file参数将对应的公钥传入kube-apiserver,公钥用于认证过程中的token校验。
参考Accessing the API from a Pod,可知每个pod里面都放置了token文件夹和ca证书,用于在需要时与apiserver安全通信。
从Pod中定位和验证apiserver的推荐方式是通过kubernetes.default.svc这个DNS名称,该名称将会解析为服务IP,然后服务 IP将会路由到apiserver。
向apiserver进行身份验证的推荐方法是使用ServiceAccount凭证。ServiceAccount凭证放置在pod中每个容器的文件系统中,位于/var/run/secrets/kubernetes.io/serviceaccount/token,该凭证即为对应命名空间下默认创建的default-token。/var/run/secrets/kubernetes.io/serviceaccount/ca.crt,用于验证apiserver的服务证书。默认命名空间将被放置在/var/run/secrets/kubernetes.io/serviceaccount/namespace文件中。

显示 Gitment 评论