那些你需要了解的Kubernetes证书知识

证书是Kubernetes不可或缺的一部分,其作用主要有两个作用,一是保证数据的完整性和安全性,二是使服务端和客户端可以通过证书来校验对方的身份。Kubernetes是一个分布式的系统,组件之间通过网络通信,使用证书,也就是HTTPS来实现安全传输是十分必要的。HTTPS可以让客户端验证服务端的身份,例如确保kubectlkubelet访问的是真实的API Server。同时服务端也可以开启TLS双向校验来验证客户端的身份,例如当我们使用kubectl访问API Server时,API Server会校验我们的客户端证书(保存在kubeconfig中),判断用户的合法性,进一步结合权限管理,判断用户是否具备对应的权限。当我们使用kubeadm创建集群时,kubeadm会为我们创建好各种证书。本文将基于kubeadm创建的集群,总结一下在部署和运维一个Kubernetes集群过程中,我们需要了解的各种与证书有关的知识。

Kubernetes的根CA

互联网上的证书需要由权威CA机构签发,同时还要缴纳一笔不菲的费用,但由于Kubernetes目的是要在集群内建立信任体系,因此使用自签CA就足够了。Kubernetes需要一个根CA,集群中的证书(除了kubelet的服务端证书)都需要由该根CA或者基于该CA的中间CA签发。使用kubeadm init创建控制平面时,kubeadm会为我们生成根CA,以及相关证书。kubeadm创建的集群的默认证书存放路径为/etc/kubernetes/pki

[root@master-1 ~]# ls -lh /etc/kubernetes/pki/
总用量 56K
-rw-r--r--. 1 root root 1.3K 10月  5 20:36 apiserver.crt  # apiserver的服务端证书
-rw-r--r--. 1 root root 1.1K 10月  5 20:36 apiserver-etcd-client.crt # apiserver访问etcd的客户端证书
-rw-------. 1 root root 1.7K 10月  5 20:36 apiserver-etcd-client.key # 证书私钥
-rw-------. 1 root root 1.7K 10月  5 20:36 apiserver.key # 证书私钥
-rw-r--r--. 1 root root 1.1K 10月  5 20:36 apiserver-kubelet-client.crt # apiserver访问kubelet的客户端证书
-rw-------. 1 root root 1.7K 10月  5 20:36 apiserver-kubelet-client.key # 证书私钥
-rw-r--r--. 1 root root 1.1K 9月   8 22:58 ca.crt # 集群根CA证书
-rw-------. 1 root root 1.7K 9月   8 22:58 ca.key # 集群根CA私钥
drwxr-xr-x. 2 root root  162 9月   8 22:58 etcd
-rw-r--r--. 1 root root 1.1K 9月   8 22:58 front-proxy-ca.crt
-rw-------. 1 root root 1.7K 9月   8 22:58 front-proxy-ca.key
-rw-r--r--. 1 root root 1.1K 10月  5 20:36 front-proxy-client.crt
-rw-------. 1 root root 1.7K 10月  5 20:36 front-proxy-client.key
-rw-------. 1 root root 1.7K 9月   8 22:58 sa.key # kube-controller-manager 用于给service account token签名的私钥
-rw-------. 1 root root  451 9月   8 22:58 sa.pub # kube-apiserver 解开service account token签名所需的公钥
复制代码

根CA需要在集群中所有的节点上都保存一份,因为集群的各组件都需要使用该CA证书来验证证书签名。

服务端证书

所有对外暴露HTTPS服务的组件都需要一个服务端证书。在Kubernetes中,kube-apiserverkubele对外提供HTTPS服务。以kube-apiserver为例,在kube-apiserver的启动命令中需要设置--tls-cert-filetls-private-key-file命令行参数来分别指定kube-apiserver的服务端证书以及对应的私钥:

 [root@master-1 ~]# cat /etc/kubernetes/manifests/kube-apiserver.yaml
 ...
 - command:
    - kube-apiserver
    - --client-ca-file=/etc/kubernetes/pki/ca.crt # 指定CA证书
    ...
    - --tls-cert-file=/etc/kubernetes/pki/apiserver.crt # 指定服务端证书
    - --tls-private-key-file=/etc/kubernetes/pki/apiserver.key # 指定私钥
...
   
复制代码

这里值得注意的是,当我们的kube-apiserver是通过弹性IP,NAT网关,或者负载均衡暴露时,要在服务端证书的SANs(Subject Alternative Name)字段中添加对应的DNS名称或IP地址,否则客户端会因访问地址与证书不匹配而报错。kubeadm会帮我们设置一些默认的SANs。可使用openssl查看证书内容:

[root@master-1 ~]# openssl x509 -noout -text -in /etc/kubernetes/pki/apiserver.crt
Certificate:
    Data:
        ...
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes
        Validity
            Not Before: Sep  8 14:58:50 2021 GMT
            Not After : Sep  8 14:58:51 2022 GMT
        Subject: CN=kube-apiserver
        Subject Public Key Info:
            ....
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Subject Alternative Name: # 看这里
                DNS:master-1, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:192.168.33.220
    Signature Algorithm: sha256WithRSAEncryption
         ...
复制代码

可以看到在Subject Alternative Name字段中,已经包含了节点hostname(master-1),service DNS(kubernetes.default等),service IP(10.96.0.1)以及节点的Node IP(192.168.33.220)。

Tips: 当我们使用kubeadm创建集群时候,可以在init时使用 --apiserver-cert-extra-sans 参数指定SANs,kubeadm会在生成证书时设置对应的SANs字段。

客户端证书

Kubernetes的组件会通过TLS双向校验来验证客户端的身份。所有由集群根CA签发的客户端证书都认为是通过认证的(Authentication),可以正常建立HTTPS连接,进一步的,Kubernetes会从客户端证书中读取用户名,用户组信息,结合RBAC等鉴权策略来判断客户端是否有对应的操作权限(Authorization)。在客户端证书中,Common Name表示用户名,Organization表示用户组。

kubelet客户端证书

kubelet访问kube-apiserver时需要带上客户端证书(TLS双向校验),证书的默认保存路径为/var/lib/kubelet/pki

[root@master-1 ~]# ls -lh /var/lib/kubelet/pki
总用量 12K
-rw-------. 1 root root 2.7K 9月   8 22:59 kubelet-client-2021-09-08-22-59-01.pem
lrwxrwxrwx. 1 root root   59 9月   8 22:59 kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2021-09-08-22-59-01.pem
-rw-r--r--. 1 root root 2.2K 9月   8 22:58 kubelet.crt
-rw-------. 1 root root 1.7K 9月   8 22:58 kubelet.key
复制代码

其中kubelet-client-2021-09-08-22-59-01.pem表示的是客户端证书,kubelet-client-current.pem是证书的一个软链接。证书之所以带日期是因为kubelet的证书快过期时会自动更新,因此带上时间方便区分新旧证书。查看一下证书信息:

[root@master-1 ~]# openssl x509 -noout -text -in /var/lib/kubelet/pki/kubelet-client-current.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 7265397798501666783 (0x64d3e0cdd398cfdf)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes
        Validity
            Not Before: Sep  8 14:58:50 2021 GMT
            Not After : Sep  8 14:58:55 2022 GMT
        Subject: O=system:nodes, CN=system:node:master-1 # O表示Organization, CN表示Common Name
        Subject Public Key Info:
        ....
复制代码

可以看到kubelet的用户组是system:nodes, 用户名为system:node:master-1。有了这些信息,kube-apiserver就可以基于Node Authorizer来限制kubelet只能读取和修改本节点上的资源。

扫描二维码关注公众号,回复: 13155155 查看本文章

Kubernetes使用一种名为Node Authorizer的授权方式专管理kubelet发出的API请求。为了获得Node Authorizer的授权,kubelet必须使用一个凭证,该凭证将它们标识为system:nodes用户组,用户名为system: Node:<nodeName>
Node Authorizer参考:kubernetes.io/docs/refere…

kube-apiserver客户端证书

类似的,kube-apiserver调用kubelet接口时(执行exec/logs命令),kubelet也会要求校验kube-apiserver的客户端证书,该证书保存路径为/etc/kubernetes/pki/apiserver-kubelet-client.crt。查看一下证书信息:

[root@master-1 ~]# openssl x509 -noout -text -in /etc/kubernetes/pki/apiserver-kubelet-client.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 5903840273946896753 (0x51eea773033fe971)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes
        Validity
            Not Before: Sep  8 14:58:50 2021 GMT
            Not After : Oct  5 12:36:44 2022 GMT
        Subject: O=system:masters, CN=kube-apiserver-kubelet-client
        ....
复制代码

该证书的用户组是system:masters,这是Kubernetes内置的用户组,Kubernetes会自动为该用户组绑定集群管理员权限。

# ClusterRole/cluster-admin 包含了所有操作权限,当kube-apiserver启动时,会自动将该权限绑定给system:masters用户组。
[root@master-1 ~]# kubectl get clusterRoleBinding cluster-admin -owide
NAME            ROLE                        AGE   USERS   GROUPS           SERVICEACCOUNTS
cluster-admin   ClusterRole/cluster-admin   27d           system:masters
复制代码

kubectl客户端证书

当我们使用kubectl访问kube-apiserver时,也要提供客户端证书。kubectl使用的客户端证书存放在kubeconfig中:

[root@master-1 ~]# cat ~/.kube/config
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: <表示CA证书,很长的一段base64编码> ①
    server: https://192.168.33.220:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
  user:
    client-certificate-data: <表示客户端证书,很长的一段base64编码> ②
    client-key-data: <表示客户端证书私钥,很长的一段base64编码>
复制代码

① 由于是自签CA,所以kubeconfig里也要保存根CA的证书(certificate-authority-data),用于校验kube-apiserver的服务端证书。

② 客户端证书使用base64编码后保存在client-certificate-data字段中。查看一下客户端证书部分:

[root@master-1 ~]# grep "client-certificate-data" ~/.kube/config \
| sed 's/\s*client-certificate-data:\s*//' \
| base64 -d | openssl x509 -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 368716738867724927 (0x51df1eba2f3827f)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes
        Validity
            Not Before: Sep  8 14:58:50 2021 GMT
            Not After : Sep  8 14:58:54 2022 GMT
        Subject: O=system:masters, CN=kubernetes-admin # O表示Organization, CN表示Common Name
        ....
复制代码

可以看到,该kubeconfig的用户名为kubernetes-admin,用户组为system:masters,即拥有所有权限。示例中的kubeconfigkubeadm默认创建的集群管理员凭证,集群管理员可以通过签发任意的用户组和用户名的证书,制作成kubeconfig,结合RBAC权限管理来控制权限。

kube-controller-manager和kube-scheduler

kube-controller-managerkube-scheduler访问kube-apiserver时也需要带上客户端证书,它们的客户端证书都保存在各自的kubeconfig凭证中。

[root@master-1 ~]# ls -ls /etc/kubernetes/controller-manager.conf /etc/kubernetes/scheduler.conf
8 -rw-------. 1 root root 5486 10月  5 20:36 /etc/kubernetes/controller-manager.conf
8 -rw-------. 1 root root 5438 10月  5 20:36 /etc/kubernetes/scheduler.conf
复制代码

查看证书内容,会发现kube-controller-manager的用户名为system:kube-controller-managerkube-scheduler的用户名为system:scheduler。即使是内部组件,基于最小权限原则,Kubernetes依然会为这两个用户只绑定必要的权限。

system: 前缀是Kubernetes保留的关键字,用于描述内置的系统用户和系统用户组,在kube-apiserver启动时,会默认为这些用户绑定好对应的权限

证书轮换

证书有过期时间,kubeadm搭建的集群默认的过期时间为1年。我们需要及时的更新证书,以免证书过期,导致集群不能工作。

手工轮换

kubelet的客户端/服务端证书可以实现自动轮换外,其他证书需要手动更新。kubeadm提供了两种更新方法:

  1. 使用kubeadm upgrade升级集群,升级成功后也会更新证书,延长1年有效期。
  2. 使用kubeadm cert renew命令手动更新证书,延长1年有效期,需要重启控制平面。

由于安全原因,kubeadm并没有提供可以修改证书过期时间的配置,并且推荐我们至少每年都对集群做一次升级(upgrade)。但生产集群很难做到1年1次升级那么频繁,如果对安全性要求没有那么高,修改一下kubeadm源码,把证书默认时间设置长一点也是一个不错的选择。

详细文档:kubernetes.io/docs/tasks/…

自动轮换

kubelet的客户端证书一般无需我们关心,可自动实现更新。服务端证书虽然也可以配置自动更新,但情况比较复杂,我们在后续的kubelet的服务端证书小节中做单独说明。当kubelet发现自己的客户端证书快要过期时,会创建一个CSR(CertificateSigningRequest)资源来申请一张新证书。kube-controller-manager会基于kubelet当前的权限来判断是否批准该CSR,一旦批准,kube-controller-manager会为kubelet签发新的证书,并保存到CSR中。kubelet随后下载CSR中的证书,保存到本地磁盘,实现证书轮换。

详细文档:kubernetes.io/docs/refere…

根CA轮换

kubeadm创建的根CA证书的默认有效期为10年,相对宽松一些。目前Kubernetes还没有提供可以自动更新根CA的方法,可以参考文档教程进行手动更新。

CSR(CertificateSigningRequest)

Kubernetes提供了一套证书相关的API(Certificate API),用于实现证书的申请与自动签发。证书的申请者,如kubelet,可以通过创建一个CSR资源来向指定的证书签名者(由CSR的singerName字段指定)申请证书签名,CSR请求可能被批准,也可能被拒绝。当CSR请求被批准后,对应的证书签名者才会对证书签名,并将签名后的证书保存在CSR的status.Certificat字段中,至此整个签发流程就完成了。证书的申请者可以从status.Certificate中获取已经签名的证书。kube-controller-manager内置了一些签名者,分别处理对应singerName的CSR请求:

  • kubernetes.io/kube-apiserver-client,申请用于访问API Server的证书,不会自动批准。
  • kubernetes.io/kube-apiserver-client-kubeletkubelet申请用于访问API Server的客户端证书,可能会被自动批准。
  • kubernetes.io/kubelet-servingkubelet的服务端证书,不会自动批准。
  • kubernetes.io/legacy-unknown,第三方应用的证书申请,不会自动批准。

kubelet进行客户端证书轮换时,创建的CSR中的singerName就是kubernetes.io/kube-apiserver-client-kubelet,正常情况下,会被kube-controller-manager自动批准,然后签发证书。

CSR 批准/拒绝

当CSR提交后,需要由审批者(可以是人,也可以是程序)批准后才能进行后续的证书签发操作。kube-controller-manager内置了一个审批控制器,可以自动批准某些CSR请求。但为了防止与其他的审批者发生冲突,kube-controller-manager不会显式的拒绝任何CSR。对于不会被kube-controller-manager处理的CSR,我们可以使用API的方式处理,如使用kubectl命令行:

# 批准一个CSR
$ kubectl certificate approve <certificate-signing-request-name>
# 拒绝一个CSR
$ kubectl certificate deny <certificate-signing-request-name>
复制代码

或者实现一个专门的控制器来来自动批准或拒绝。

kubelet客户端证书自动批准

对于kubernetes.io/kube-apiserver-client-kubelet类型的CSR,kube-controller-manager根据申请者是否具备对应的RBAC权限,来决定是否批准该CSR。kubelet在两种情况下都会创建CSR请求:

  1. 在首次加入集群时,还没有生成客户端证书,kubelet需要创建CSR资源来申请,这个阶段也就是TLS引导阶段(TLS bootstrapping)。
  2. 当客户端证书快过期时,kubelet会发起证书轮换,创建CSR请求申请新的证书。

对于这两种场景,Kubernetes提供了两个默认权限(ClusterRole):

  • nodeclient:当节点首次创建证书时,kubelet还没有正式的客户端证书,处于TLS引导阶段。此时kubelet会使用bootstrap token认证方式来请求API Server。kubeadm init创建的bootstrap token所属用户组为system:bootstrappers:kubeadm:default-node-tokenkubeadm会负责将nodeclient权限赋予该用户组。
  • selfnodeclient:当节点请求证书轮换时,kubelet已经有一个正式的客户端证书。kubelet的证书属于system:nodes用户组,kubeadm会负责将selfnodeclient权限赋予该用户组。
# 两个默认权限
[root@master-1 ~]# kubectl get clusterrole -l kubernetes.io/bootstrapping=rbac-defaults  | grep nodeclient
system:certificates.k8s.io:certificatesigningrequests:nodeclient       2021-09-08T14:59:17Z
system:certificates.k8s.io:certificatesigningrequests:selfnodeclient   2021-09-08T14:59:17Z
# kubeadm会负责将这两个权限绑定到对应的用户组
[root@master-1 ~]# kubectl get clusterrolebinding kubeadm:node-autoapprove-bootstrap kubeadm:node-autoapprove-certificate-rotation  -owide
NAME                                            ROLE                                                                               AGE   USERS   GROUPS                                            SERVICEACCOUNTS
kubeadm:node-autoapprove-bootstrap              ClusterRole/system:certificates.k8s.io:certificatesigningrequests:nodeclient       27d           system:bootstrappers:kubeadm:default-node-token
kubeadm:node-autoapprove-certificate-rotation   ClusterRole/system:certificates.k8s.io:certificatesigningrequests:selfnodeclient   27d           system:nodes
复制代码

kube-controller-manager检查权限无误后,会自动批准该CSR请求。

参考文档:kubernetes.io/docs/refere…

CSR 证书签发

当CSR请求被批准后,签发者才可以签发证书。kube-controller-manager同样也内置了签发控制器。通过为kube-controller-manager设置--cluster-signing-cert-file--cluster-signing-key-file启动参数以开启内置的签发控制器,这两个参数分别表示用于签名的证书和私钥,也就是集群的CA证书:

[root@master-1 pki]# cat /etc/kubernetes/manifests/kube-controller-manager.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    component: kube-controller-manager
    tier: control-plane
  name: kube-controller-manager
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
    ...
    - --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
    - --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
    ...
复制代码

在1.18之前,kube-controller-manager会为所有已经批准的CSR签发证书。1.18之后,kube-controller-manager限制了CSR的singerName,只会为上述的四种指定singerName的CSR请求签发证书。类似的,对于不会自动签发证书的CSR请求,我们同样可以通过kubectl来手动签发,亦或者通过实现一个专门的控制器来自动签发。

文档参考:kubernetes.io/docs/refere…

kubelet的服务端证书

kubelet同样对外暴露了HTTPS服务,其客户端主要是kube-apiserver和一些监控组件,如metric-serverkube-apiserver需要访问kubelet来获取容器的日志和执行命令(kubectl logs/exec), 监控组件需要访问kubelet暴露的cadvisor接口来获取监控信息。理想情况下,我们需要将kubelet的CA证书配置到kube-apiservermetric-server中,以便于校验kubelet的服务端证书,保证安全性。但使用默认的集群设置方法是无法做到这点的,需要做一些额外的工作。

前文提到,Kubernetes中除了kubelet的服务端证书以外,其他证书都要由集群根CA(或是基于根CA的中间CA)签发。kubelet的证书则没有这个要求。实际上,在kubelet在启动时,如果没有指定服务端证书路径,会创建一个自签的CA证书,并使用该CA为自己签发服务端证书。

kubelet 也可以使用服务端(Serving)证书。kubelet自身向外提供一个HTTPS末端,包含若干功能特性。要保证这些末端的安全性,kubelet可以执行以下操作之一:

  • 使用通过 --tls-private-key-file 和 --tls-cert-file 所设置的密钥和证书
  • 如果没有提供密钥和证书,则创建自签名的密钥和证书
  • 通过 CSR API 从集群服务器请求服务证书

文档参考:kubernetes.io/docs/refere…

这意味着每个kubelet的根CA都不一样,这就导致客户端组件,如metric-server,甚至是kube-apiserver都没办法校验kubelet的服务端证书。为了应对这种情况,metric-server需要添加--kubelet-insecure-tls来跳过服务端证书的校验,而kube-apiserver默认不校验kubelet服务端证书。

那可不可以像申请客户端证书一样,使用CSR来申请服务端证书呢?CSR签发者统一用集群的根CA为各kubelet签发服务端证书,kube-apiserver和其他组件就可以通过配置集群根CA来实现HTTPS的服务端证书校验了。答案是可以的,我们可以在kubelet配置文件配置serverTLSBootstrap为true就可以启用这项特性,即使用CSR来申请服务端证书,而不是使用自签CA来签发证书。这项配置同样也会开启服务端证书的自动轮换功能。不过这个过程并不是全自动的,在证书轮换章节中提到,kubelet的服务端证书CSR请求,即singerNamekubernetes.io/kubelet-serving的CSR请求,不会被kube-controller-manager自动批准,也就是说我们需要手动批准这些CSR,或者使用第三方控制器。

为什么Kubernetes不自动批准kubelet的服务端证书呢?这样不是很方便吗?原因是出于安全考量——Kubernetes没有足够的能力来辨别该CSR是否应该被批准。

HTTPS服务端证书的重要作用就是向客户端证明“我是我”,防止有人冒充“我”跟客户端通信,也就是防止中间人攻击。在向权威CA机构申请证书时,我们要提供一系列证明材料,证明这个站点是我的,包括要证明我是该站点域名的所有者,CA审核通过后才会签发证书。但K8S集群本身是没有足够的能力来辨别kubelet身份的,因为节点IP,DNS名称可能发生变化,K8S自身没有足够的能力判断哪些IP,哪些DNS是合法的,这属于基础设施管理者的职责范围。如果你的集群是云厂商提供,那么你的云厂商可以提供对应的控制器来判断CSR请求的合法性,批准合法的CSR请求。如果是自建集群,那么只有集群管理员才能判断CSR请求中包含的节点IP,DNS名称是不是真实有效的。如果kube-controller-manager自动签发这些证书,则会产生中间人攻击的风险。

这个PR描述了一种中间人攻击的具体场景。假设节点A上的服务bar使用HTTPS暴露服务,并且服务端证书是通过CSR请求申请的,由集群根CA签发。假设有入侵者获取了节点A的权限,那他可以很方便的利用kubelet的客户端证书的权限,创建一个CSR请求来申请一份IP为barservice IP,DNS名称为barservice DNS的服务端证书。如果kube-controller-manager自动通过并签发这个证书,那入侵者就可以使用这个证书,配合节点上的kube-proxy,劫持所有经过bar服务的流量。

如果你对这个话题感兴趣,可以看下这两个issue的讨论: github.com/kubernetes/…
github.com/kubernetes/…

总结

Kubernetes作为一个分布式系统,组件之间使用HTTPS加密通信是基本要求。证书也作为客户端的一种访问凭证,服务端使用TLS双向校验来认证客户端身份,并结合RBAC,Node Authorizer鉴权方案管理用户权限。为了应对证书管理的挑战,Kubernetes提供了证书自轮换、kubelet的TLS自动引导、申请/签发证书API的能力来降低管理复杂度,不过尽管如此,我们还是要特别关注一下集群的证书的有效期,及时更新避免过期。kubelet的服务端证书比较特殊,如果希望遵循最佳的HTTPS安全实践,即校验HTTPS服务端证书,我们可以开启kubelet的服务端证书自动轮替,并使用一种安全的方式来批准kubelet的服务端证书CSR申请。

猜你喜欢

转载自juejin.im/post/7016472622246395934