K8s安全总结

1. 预备知识

1.1 kubernetes api相关知识

Kubernetes API是集群系统中的重要组成部分,Kubernetes中各种资源(对象)的数据通过该API接口被提交到后端的持久化存储(etcd)中,Kubernetes集群中的各部件之间通过该API接口实现解耦合,同时Kubernetes集群中一个重要且便捷的管理工具kubectl也是通过访问该API接口实现其强大的管理功能的。Kubernetes API中的资源对象都拥有通用的元数据,资源对象也可能存在嵌套现象,比如在一个Pod里面嵌套多个Container。创建一个API对象是指通过API调用创建一条有意义的记录,该记录一旦被创建,Kubernetes将确保对应的资源对象会被自动创建并托管维护。

在Kubernetes系统中,大多数情况下,API定义和实现都符合标准的HTTP REST格式, 比如通过标准的HTTP动词(POST、PUT、GET、DELETE)来完成对相关资源对象的查询、创建、修改、删除等操作。但同时Kubernetes 也为某些非标准的REST行为实现了附加的API接口,例如Watch某个资源的变化、进入容器执行某个操作等。另外,某些API接口可能违背严格的REST模式,因为接口不是返回单一的JSON对象,而是返回其他类型的数据,比如JSON对象流(Stream)或非结构化的文本日志数据等。

更多详情请见:http://geek.csdn.net/news/detail/57265

1.2 k8s中的用户概念

Kubernetes集群有两类用户:普通用户和由Kubernetes管理的服务帐户。

API请求与普通用户或服务帐户相关联,或被视为匿名请求。这就意味着每个内部的进程或者集群外部的用户在工作站上使用kubectl在节点上操作,以及用户使用控制台,只要请求API,都需要认证,负责被认为是匿名用户。

普通用户与一般的用户名的概念相同,是一个非常传统的概念,可以简单的理解为用户名和密码。是假定被外部或独立服务管理的。管理员分配私钥,用户像Keystone或google账号一样,被存储在包含用户名和密码列表的文件里。在这点上,Kubernetes没有代表正常用户帐户的资源对象。不能通过API调用将普通用户添加到集群中。在Kubernetes中,用户名在一个集群中是全局唯一的,也就是说在同一个集群中,只允许有一个指定名称的用户账号,而与集群中创建了多少个命名空间(Namespace)或者启动了多少个API Server无关。此外,用户账号可以从数据库等第三方系统(如LDAP)同步进来,以实现与其它系统共享用户账号信息。

相比之下,服务帐户是由Kubernetes API管理的用户。它们绑定到特定的命名空间,并由API服务器自动创建或通过API调用手动创建。服务帐户与存储为Secrets的一组证书相关联,这些凭据被挂载到pod中,以便集群进程与Kubernetes API通信。

1.3 账号类型

对kubernetes api的访问方有三种:k8s集群的组件、普通用户、k8s集群中运行的应用。

Kubernetes根据使用账号的进程是否在Pod内部运行这个标准将账号划分为用于提供给外部进程使用的用户账号(User Accounts)和用于提供给内部进程使用的服务账号(Service Accounts),这两种不同的账号类型。
当进程在Pod内部运行时,一般建议该进程使用服务账号来访问API Server,而当进程运行在Pod之外甚至在Kubernetes集群之外时,则建议该进程使用用户账号来访问API Server;当然,这个标准并不是绝对的,例如,当Pod内部运行的进程使用用户账号来访问API Server时,并不会被API Server视为不合法而拒绝访问。

2. API SERVER权限控制

Kubernetes API Server是对外暴露的API访问地址,API Server权限控制分为三种:Authentication(认证)、Authorization(授权)、AdmissionControl(准入控制)。
这里写图片描述
例如:通过输入yaml文件到kubectl(kubectl create -f pod.yaml)创建POD的命令会被发送到有安全保障的api-server端口(https://),然后身份验证流就开始生效了。注意,如果你使用不安全的端口(http://),那么验证就无法应用。理想情况下生产环境设置中应该避免不安全端口(http://)。

Authentication:即身份验证,这个环节它面对的输入是整个http request。它负责对来自client的请求进行身份校验,支持的方法包括:client证书验证(https双向验证)、basic auth、普通token以及jwt token(用于serviceaccount)。APIServer启动时,可以指定一种Authentication方法,也可以指定多种方法。如果指定了多种方法,那么APIServer将会逐个使用这些方法对客户端请求进行验证,只要请求数据通过其中一种方法的验证,APIServer就会认为Authentication成功;

Authorization:授权。这个阶段面对的输入是http request context中的各种属性,包括:user、group、request path(比如:/api/v1、/healthz、/version等)、request verb(比如:get、list、create等)。APIServer会将这些属性值与事先配置好的访问策略(access policy)相比较。APIServer支持多种authorization mode,包括AlwaysAllow、AlwaysDeny、ABAC、RBAC和Webhook。APIServer启动时,可以指定一种authorization mode,也可以指定多种authorization mode,如果是后者,只要Request通过了其中一种mode的授权,那么该环节的最终结果就是授权成功。

Admission Control:从技术的角度看,Admission control就像a chain of interceptors(拦截器链模式),它拦截那些已经顺利通过authentication和authorization的http请求。http请求沿着APIServer启动时配置的admission control chain顺序逐一被拦截和处理,如果某个interceptor拒绝了该http请求,那么request将会被直接reject掉,而不是像authentication或authorization那样有继续尝试其他interceptor的机会。

2.1 认证(着重介绍客户端证书和serviceaccount)

总结:k8s组件通过客户端证书(借助kubeconfig)访问apiserver,应用通过serviceaccount访问apiserver,用户通过用户名、密码或openid connect(我们使用的就是这种,DEX)访问apiserver。一次可以指定多个认证方式,会依顺序认证,直到有一个认证方式通过。

kubernetes1.8版本现在支持以下这些认证方法:

  • X509客户端证书:这种方式也叫作TLS双向认证,也就是服务器客户端互相验证证书的正确性,在都正确的情况下协调通信加密方案
  • 静态Token文件:用token唯一标识请求者,只要apiserver存在该token,则认为认证通过,但是如果需要新增Token,则需要重启kube-apiserver组件,实际使用不可取
  • Bootstrap Tokens:这是1.6版本提供的新的解决方案,还处于alpha版本。对于它还不是很了解,暂时略过不谈
  • 静态Password文件(Basic Authentication):类似静态Token文件,是使用用户名密码方式标识请求者,新增也需要重启kube-apiserver组件,不可取
  • Service Account Tokens:主要用于运行在集群里的pod与apiserver通信时进行认证,基本是由集群自动创建管理的
  • OpenID Connect Token认证:这种认证方式其实与现在的第三方登陆方式并无二致,参考OAuth2.0原理即可
  • Webhook Token:提供调用第三方认证方法的接口,只要该接口满足kubernetes定义好的输入输出,即可用于请求认证,适用于已有认证方案的团队
  • 认证代理
  • Keystone Password:keystone是openstack里用于身份认证的组件,kubernetes现在简单的实现了基于用户名密码的认证方式,可能离实际使用还是有一段距离
    匿名请求:当允许匿名请求的时候,没有被其他认证方法拒绝的请求都会被认为是匿名请求,同时拥有用户名system:anonymous,和组system:unauthenticated
    自定义方法:当你觉得上面所有的方法都不够用,或者说不适用于你自身的业务需求的时候,你可以自定义认证方法

2.1.1 X509客户端证书认证

为了使用这个方案,api-server启动参数加如下3个参数:

  • –client-ca-file=CA_CERTIFICATE_FILE(如:–client-ca-file=/etc/kubernetes/ssl/ca.pem)
  • –tls-cert-file
  • –tls-private-key-file

说明:

  • –client-ca-file这个参数用来开启并指定CA证书路径,CA_CERTIFICATE_FILE包含一个或多个认证中心,可以被用来验证呈现给api-server的客户端证书,api-server启动时,会解析ca文件,遍历其中的cert(客户端的证书,这个CA在认证的时候告诉apiserver这些客户端的证书是我签发的),加入certpool,客户端证书的Common Name(/CN)将作为用户名。
  • 另外两个参数分别是apiserver*服务端数字证书和服务端私钥。如果不指定,会在server启动时自动在/var/run/kubernetes下为该端口绑定的公有IP地址生成自签名证书*文件和私钥文件。
  • 除了–tls-private-key-file外,启动参数中还可以看到指定了一个key文件–service-account-key-file,这个是用来认证serviceaccount的token的,若未指定,默认是tls-private-key-file所指定的
  • 其实一般情况下使用 TLS 的系统都会去 /etc/ssl 查找一个本机信任的 CA 列表,但是 Kubernetes 很傲娇,他不会去找,必须显式的告知签发 CA(ca.crt)。
  • 所有客户端证书都应该由这一CA 签发。

客户端证书认证使用步骤如下:

  1. 产生根证书ca.crt(内置CA公钥,用于验证某.crt文件,是否是该CA机构签发的证书;)
  2. 用根证书ca.crt签发apiserver的服务端证书server.crt。其中的/CN是apiserver所在地址,是其它服务访问apiserver要用的地址。
  3. 用根证书ca.crt签发其他k8s集群组件访问apiserver所用的客户端证书和私钥,其中的/CN是其他k8s集群组件所在地址。
  4. 需要访问apiserver的组价创建kubeconfig文件,里面包含客户端证书、私钥、ca根证书;启动时加参数–kubeconfig=指定kubeconfig文件

实例:

1.生成根证书(根证书ca.crt和私钥ca.key用来签发其他证书)

openssl genrsa -out /etc/kubernetes/ca.key 2048

openssl req -new -x509 -nodes -key /etc/kubernetes/ca.key -subj "/CN=kube-ca" -days 10000 -out /etc/kubernetes/ca.crt 

备注:

  • ca.key这个私钥和ca.crt要保留好,有它俩才能签发其它证书。
  • 从当前启动参数中,我们仅能看到一种机制:–client-ca-file=/etc/kubernetes/ssl/ca.crt,也就是client证书校验机制。apiserver会用/etc/kubernetes/ssl/ca.crt对client端发过来的client.crt进行验证。

可以通过如下命令查看根证书:openssl x509 -noout -text -in /etc/kubernetes/ssl/ca.crt

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 15034780941394504725 (0xd0a6459bc7a1ec15)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kube-ca
        Validity
            Not Before: Nov 11 02:31:02 2017 GMT
            Not After : Mar 29 02:31:02 2045 GMT
        Subject: CN=kube-ca
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                ...

2. 生成kube-apiserver证书和私钥

openssl genrsa -out /etc/kubernetes/server.key 2048

openssl req -new -key /etc/kubernetes/server.key -subj "/CN=kube-apiserver" -out /etc/kubernetes/server.csr         

openssl x509 -req -in /etc/kubernetes/server.csr -CA /etc/kubernetes/ca.crt -CAkey /etc/kubernetes/ca.key -CAcreateserial -out /etc/kubernetes/server.crt -days 10000

备注:

  • server.key:kube-apiserver服务端私钥文件;
  • subj “/CN=kube-apiserver”一定要用域名形式,为了下面controller-manager、scheduler等用go语言写的程序能访问:https://github.com/docker/docker/issues/8943(有待考证)
  • server.crt:kube-apiserver服务端公钥证书;

可以通过如下命令查看apiserver的证书:openssl x509 -noout -text -in /etc/kubernetes/ssl/apiserver.crt

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 15970227368482527382 (0xdda1a52d809f5896)
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: CN=kube-ca
        Validity
            Not Before: Nov 11 02:31:02 2017 GMT
            Not After : Nov  9 02:31:02 2027 GMT
        Subject: CN=kube-apiserver
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                ...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Key Usage:
                Digital Signature, Non Repudiation, Key Encipherment
            X509v3 Subject Alternative Name:
                DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:k8smaster01, DNS:k8smaster02, DNS:k8smaster03, DNS:k8sslave01, DNS:k8sslave02, DNS:k8sslave03, IP Address:10.142.21.21, IP Address:10.142.21.21, IP Address:10.142.21.22, IP Address:10.142.21.22, IP Address:10.142.21.23, IP Address:10.142.21.23, IP Address:10.233.0.1, IP Address:127.0.0.1
    Signature Algorithm: sha1WithRSAEncryption

可以来验证一下server.crt是否是ca.crt签发的:

openssl verify -CAfile ca.crt server.crt
server.crt: OK

3. 用根证书ca.crt和ca.key分别签发其他要访问api_server的k8s集群组件的证书和私钥(以kube-scheduler为例):

    openssl genrsa -out /etc/kubernetes/scheduler.key 2048

    openssl req -new -key /etc/kubernetes/scheduler.key -subj "/CN=10.132.47.61" -out /etc/kubernetes/scheduler.csr

    openssl x509 -req -in /etc/kubernetes/scheduler.csr -CA /etc/kubernetes/ca.crt -CAkey /etc/kubernetes/ca.key -CAcreateserial -out /etc/kubernetes/cs_client.crt -days 10000

备注:

  • scheduler.key:kube-scheduler服务端私钥文件;
  • 这里的/CN用不用域名形式都行,反正没人找它
  • scheduler.crt:kube-scheduler服务端公钥的数字证书;

4.创建kubeconfig文件并使用

当使用 kubeconfig 文件的时候,可以按照如下方式设置使用证书:

kind: Config
users:
- name: green-user
  user:
    client-certificate: path/to/my/client/cert
    client-key: path/to/my/client/key

Kubernetes 做了很多用户证书方面的假设(用户名就是 Common Name,Group 就是 Organization)。如果这些假设不符合需求,那么就应该停用客户端证书认证,改用认证代理。

以controller-manager为例:
创建 kubeconfig文件:vim /etc/kubernetes/cm_kubeconfig

apiVersion: v1
kind: Config
users:
- name: controllermanager
  user:
    client-certificate: /etc/kubernetes/cs_client.crt
    client-key: /etc/kubernetes/cs_client.key
clusters:
- name: local
  cluster:
    certificate-authority: /etc/kubernetes/ca.crt
contexts:
- context:
    cluster: local
    user: controllermanager
  name: my-context
current-context: my-context

启动参数加:–kubeconfig=/etc/kubernetes/cm_kubeconfig

说明:

  • 可以看到controllermanager启动参数中还有如下两个参数:
    –root-ca-file=/etc/kubernetes/ca.crt,这是用来放在serviceaccount的secret里的,其实和controllermanager的客户端认证没关系,不要搞混。
    –service-account-private-key-file=/etc/kubernetes/cs_client.key,这个也是用来签发serviceaccount的secret里的token的,其实也和controllermanager的客户端认证无关。
  • 可以看到controllermanager启动参数中还有如下两个参数:
    –root-ca-file=/etc/kubernetes/ca.crt,这是用来放在serviceaccount的secret里的,其实和controllermanager的客户端认证没关系,不要搞混。
    –service-account-private-key-file=/etc/kubernetes/cs_client.key,这个也是用来签发serviceaccount的secret里的token的,其实也和controllermanager的客户端认证无关。
  • controllermanager的启动参数中–master=https://lk-k8s-ssl-master-47-61:6443,假如通过https访问,这个参数所填的值要和apiserver的/CN一致
  • *

2.1.2 service account tokens

参数知识:

服务帐户是一个自动启用的使用签名的承载令牌来验证请求的认证器。apiserver加如下两个启动参数:

  • –service-account-key-file 一个包含用户签名 bearer tokens的秘钥文件。这个是用来认证serviceaccount的token用的,如果不指定,将使用apiserver的TLS私钥。
  • –service-account-lookup 可选,如果指定,从API中删除的tokens将会撤销。

controllermanager加如下启动参数:

  • –service-account-private-key-file PEM 编码的 X509 RSA 或者 ECDSA Key,用于签署 Service Account Token。

Controller Manager 使用私钥签署 Service Account Token。跟 Kubernetes 中使用的其他私钥不同的是,这个私钥是使用同一 CA 验证的,因此,需要给每个 Controller Manager 指定一致的私钥文件。如果两个不同的 Controller Manager 用了两个不同的 Key,那就杯具了,他们会用各自的 Key 来生成 Token,最终导致无效判定。我觉得这点不太合理,Kubernetes 应该和其他方面一样,使用 CA 进行管理。通过对源码的阅读,我觉得原因可能是 jwt-go 不支持 CA。

这个 Key 也不需要什么 CA 来做签署,生成很容易:

openssl genrsa -out private.key 4096

然后分发给每个 Controller Manager 和 API Server 就可以了。

service account与user account的区别:

Service account是为了方便Pod里面的进程调用Kubernetes API或其他外部服务而设计的。它与User account不同

  • User account是为人设计的,而service account则是为Pod中的进程调用Kubernetes API而设计;
  • User account是跨namespace的,而service account则是仅局限它所在的namespace;
  • 每个namespace都会自动创建一个default service account
  • Token controller检测service account的创建,并为它们创建secret
  • 开启ServiceAccount Admission Controller后
    • 每个Pod在创建后都会自动设置spec.serviceAccount为default(除非指定了其他ServiceAccout)
    • 验证Pod引用的service account已经存在,否则拒绝创建
    • 如果Pod没有指定ImagePullSecrets,则把service account的ImagePullSecrets加到Pod中
    • 每个container启动后都会挂载该service account的token和ca.crt到/var/run/secrets/kubernetes.io/serviceaccount/

服务账号通常由API服务器自动创建,并通过ServiceAccount Admission Controller与集群中的pods关联。通过阅读K8s的官方文档“Accessing the api from a pod”,我们知道K8s cluster为Pod访问API Server做了很多“预备”工作,最重要的一点就是在Pod被创建的时候, serviceaccount (Bearer token)被自动mount到/var/run/secrets/kubernetes.io/serviceaccount路径下。使集群中的进程可以与API服务器通信。账号可以在PodSpec的serviceAccountName字段中被明确。
注意:通常省略serviceAccountName,因为这是自动完成的。

实例:

服务账号的bearer tokens在集群外部使用也是可以的,可以用于创建希望与apiserver长期通信的身份。要手动创建服务帐户,只需使用kubectl创建 serviceaccount即可。这将在当前命名空间中创建一个服务帐户,并创建一个关联的密钥。
例1:

$ kubectl create serviceaccount jenkins
serviceaccount "jenkins" created
$ kubectl get serviceaccounts jenkins -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  # ...
secrets:
- name: jenkins-token-1yvwg

创建的secret保存了apiserver的公共CA和签名的JSON Web Token(JWT)。

$ kubectl get secret jenkins-token-1yvwg -o yaml
apiVersion: v1
data:
  ca.crt: (APISERVER'S CA BASE64 ENCODED)
  token: (BEARER TOKEN BASE64 ENCODED)
kind: Secret
metadata:
  # ...
type: kubernetes.io/service-account-token

注意:值是base64编码的,因为秘钥通常被base64编码。

Name:        my-golang-1147314274-0qms5
Namespace:    default
Node:        10.47.136.60/10.47.136.60
Start Time:    Thu, 24 Nov 2016 14:59:52 +0800
Labels:        pod-template-hash=1147314274
        run=my-golang
Status:        Running
IP:        172.16.99.9
... ...

Containers:
  my-golang:
    ... ...
    Volume Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-40z0x (ro)
    Environment Variables:    <none>
... ...
Volumes:
  default-token-40z0x:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-40z0x
QoS Class:    BestEffort
Tolerations:    <none>

serviceaccount顾名思义,是Pod中程序访问APIServer所要使用的账户信息,我们来看看都有啥:
例2:

[root@k8smaster01 ~]# kubectl get serviceaccount
NAME                            SECRETS   AGE
default                         1         23d
istio-ingress-service-account   1         22d
istio-manager-service-account   1         22d
istio-pilot-service-account     1         17d
[root@k8smaster01 ~]# kubectl describe serviceaccount/default
Name:       default
Namespace:  default
Labels:     <none>
Annotations:    <none>

Image pull secrets: <none>

Mountable secrets:  default-token-kvbrf

Tokens:             default-token-kvbrf

[root@k8smaster01 ~]# kubectl describe secret/default-token-kvbrf
Name:       default-token-kvbrf
Namespace:  default
Labels:     <none>
Annotations:    kubernetes.io/service-account.name=default
        kubernetes.io/service-account.uid=499b56e5-c68b-11e7-a503-00505694b7e8

Type:   kubernetes.io/service-account-token

Data
====
token:      eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4ta3ZicmYiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjQ5OWI1NmU1LWM2OGItMTFlNy1hNTAzLTAwNTA1Njk0YjdlOCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.NUWLNbTSLLXrk3Ca4ab4YTODEGzpmeZJflkHbqAW_bGgMx6bRQNEPNtoXhs-bgX47YXiRw_qjLEqZyWZ4QOE26h0S6_uSIfOootd-5gtD6cvBmM2-7xhx4NBuycGxylAGIKyWChKX5VsqanZ-8FIh7vHEKMjO6YG__TYeDoddRJmALMK6Rtl9l0KmIom6fJAipXBlLNnw1OquKg0j-VxUREAbmIWLJjsYo8tE6A7Wd8EiJXca0pfrESOMWGwD_4I5Nk6eE4ZuaYXN_bb343zQ9dPLPNmFLi92Nqx0PDQwkuFstD8BdCT5lIkceXl0McN1YcPC6JmBUrJ78dAev3dRw
ca.crt:     1090 bytes
namespace:  7 bytes

mount到Pod中/var/run/secrets/kubernetes.io/serviceaccount路径下的default-token-40z0x volume包含三个文件:

  • ca.crt:CA的公钥证书
  • namspace文件:里面的内容为:”default”
  • token:用在Pod访问APIServer时候的身份验证。

理论上,使用这些信息Pod可以成功访问APIServer,我们来测试一下。注意在Pod的世界中,APIServer也是一个Service,通过kubectl get service可以看到:

# kubectl get services
NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes     192.168.3.1     <none>        443/TCP    43d

kubernetes这个Service监听的端口是443,也就是说在Pod的视角中,APIServer暴露的仅仅是insecure-port。并且使用”kubernetes”这个名字,我们可以通过kube-dns获得APIServer的ClusterIP。

启动一个基于golang:latest的pod,pod.yaml如下:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-golang
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: my-golang
    spec:
      containers:
      - name: my-golang
        image: golang:latest
        command: ["tail", "-f", "/var/log/bootstrap.log"]

Pod启动后,docker exec -it container-id /bin/bash切入container,并执行如下命令:

# TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
# curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes:443/version -H "Authorization: Bearer $TOKEN"
Unauthorized

查看API Server的log:

E1125 17:30:22.504059 2743425 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error

似乎是验证token失败。这个问题在kubernetes的github issue中也有被提及,目前尚未解决。

不过仔细想了想,如果每个Pod都默认可以访问APIServer,显然也是不安全的,虽然我们可以通过authority和admission control对默认的token访问做出限制,但总感觉不那么“安全”。

签名的JWT可以作为bearer token来验证给定的service account。请看上边关于token如何包含在request中。通常,这些秘钥被挂载到pods中,用于集群中访问API服务器,但也可以从群集外部使用。

服务账号使用用户名进行验证 system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT),并分配给组 system:serviceaccounts 和 system:serviceaccounts:(NAMESPACE)。

警告:由于service account tokens存储在秘钥中,任何具有对这些秘钥的读取访问权限的用户都可以作为服务帐户进行身份验证。在授予服务帐户权限并阅读秘密功能时,要小心谨慎。

2.1.3 静态密码文件(Basic Authentication)

承接service account的例2,我们来试试basic auth方式(这种方式的弊端是API Server运行中,无法在运行时动态更新auth文件,对于auth文件的修改,必须重启APIServer后生效)。

我们首先在APIServer侧为APIServer创建一个basic auth file:

// /srv/kubernetes/basic_auth_file
admin123,admin,admin

basic_auth_file中每一行的格式:password,username,useruid

修改APIServer的启动参数,将basic_auth_file传入并重启apiserver:

KUBE_APISERVER_OPTS=" --insecure-bind-address=10.47.136.60 --insecure-port=8080 --etcd-servers=http://127.0.0.1:4001 --logtostderr=true --service-cluster-ip-range=192.168.3.0/24 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota --service-node-port-range=30000-32767 --advertise-address=10.47.136.60 --basic-auth-file=/srv/kubernetes/basic_auth_file --client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key"

我们在Pod中使用basic auth访问API Server:

# curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes:443/version -basic -u admin:admin123
{
  "major": "1",
  "minor": "3",
  "gitVersion": "v1.3.7",
  "gitCommit": "a2cba278cba1f6881bb0a7704d9cac6fca6ed435",
  "gitTreeState": "clean",
  "buildDate": "2016-09-12T23:08:43Z",
  "goVersion": "go1.6.2",
  "compiler": "gc",
  "platform": "linux/amd64"
}

Pod to APIServer authentication成功了。

2.1.4 静态Token文件

当在命令行指定- -token-auth-file=SOMEFILE选项时,API服务器从文件中读取 bearer tokens。目前,tokens持续无限期,如果不重启 API server,token列表无法重新加载,不实用。

令牌文件是一个至少包含3列的csv文件: token, user name, user uid,后跟可选的组名。注意,如果您有多个组,则列必须是双引号,例如。

token,user,uid,"group1,group2,group3"

Putting a Bearer Token in a Request
当通过http客户端使用 bearer token 认证时,API服务器需要一个值为Bearer THETOKEN的授权头。bearer token必须是,可以放在HTTP请求头中且值不需要转码和引用的一个字符串。例如:如果bearer token是31ada4fd-adec-460c-809a-9e56ceb75269,它将会在HTTP头中按下面的方式呈现:

Authorization: Bearer 31ada4fd-adec-460c-809a-9e56ceb75269

2.1.5 Bootstrap Tokens(不重要,略过)

2.1.6 OpenID Connect

OpenID Connect 是一些由OAuth2提供商支持的OAuth2,特别是Azure Active Directory,Salesforce和Google。OAuth2的协议的主要扩展是增加一个额外字段,返回了一个叫ID token的access token。这个token是被服务器签名的JSON Web Token (JWT) ,具有众所周知的字段,比如用户的email。
为了识别用户,验证使用来自OAuth2 token响应的id_token (而不是 access_token)作为bearer token。token如何包含在请求中请参阅上面。

2.2 授权

授权

认证之后的请求是授权模块。

请求必须包含请求者的用户名,请求的操作以及受该操作影响的对象,如果策略已经声明用户具有完成请求的权限,则该请求将被授权。

例如:设置如下Bob策略,那么会在namespace projectCaribou 中读取pods

{
    "apiVersion": "abac.authorization.kubernetes.io/v1beta1",
    "kind": "Policy",
    "spec": {
        "user": "bob",
        "namespace": "projectCaribou",
        "resource": "pods",
        "readonly": true
    }
}

如果Bob发出以下请求,被允许读取projectCaribou namespace中的对象,请求被授权

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "spec": {
    "resourceAttributes": {
      "namespace": "projectCaribou",
      "verb": "get",
      "group": "unicorn.example.org",
      "resource": "pods"
    }
  }
}

如果Bob向projectCaribou namespace中对象写入(create或update)请求,则会拒绝其授权。
如果Bob请求在不同的namespace中读取(get)对象,比如projectFish,授权也将被拒绝。
Kubernetes授权要求使用公共常见得REST属性与云提供商的访问控制系统进行交互。为了避免访问控制系统与Kubernetes API与外部API的冲突,所以必须使用REST格式。

Kubernetes支持多种授权模块,如ABAC模式、RBAC模式和Webhook模式。当管理员创建集群时,他们将会配置在API Server中使用的授权模块。如果配置了多个授权模块,Kubernetes会检查每个模块,当通过其中任何模块授权请求,则授权成功,如果所有模块都拒绝了该请求,则授权失败(HTTP 403)。

2.3 准入控制(Admission Control)

准入控制admission controller本质上是一段代码,这个准入代码在apiserver中,而且必须被编译到二进制文件中才能被执行。在对kubernetes api的请求过程中,当请求通过了前面的认证和授权之后,还需要经过准入控制处理通过之后,apiserver 才会处理这个请求。Admission Control 有一个准入控制列表,我们可以通过命令行设置选择执行哪几个准入控制器。在对集群进行请求时,每个准入控制代码都按照一定顺序执行。只有所有的准入控制器都检查通过之后,apiserver 才执行该请求,否则返回拒绝。

不同于授权和认证只关心请求的用户和操作,准入控制还处理请求的内容,并且仅对创建、更新、删除或连接(如代理)等有效,而对读操作无效。

Kubernetes v1.7新增了Initializers和GenericAdmissionWebhook,可以用来方便地扩展准入控制。

2.3.1 为什么需要Admission Control

在kubernetes中,一些高级特性正常运行的前提条件为,将一些准入模块处于enable状态。总结下,对于kubernetes apiserver,如果不适当的配置准入控制模块,他就不能称作是一个完整的server,某些功能也不会正常的生效。

2.3.2 主要的准入控制器介绍

  • AlwaysAdmit
    允许所有请求

  • AlwaysDeny
    拒绝所有请求

  • AlwaysPullImages
    强制设置Pod拉取镜像策略为Always。这样能够保证私有镜像只能被有拉取权限的使用者使用。

  • DenyExecOnPrivileged(deprecated)
    它会拦截所有想在privileged container上执行命令的请求。(如果自己的集群支持privileged container,自己又希望限制用户在这些privileged container上执行命令,那么强烈推荐使用它。)

  • DenyEscalatingExec
    这个插件禁止那些通过主机执行而获得privileges去执行exec和attach Pod的命令。

  • ImagePolicyWebhook
    通过webhook决定image策略,需要同时配置–admission-control-config-file

  • ServiceAccount
    一个serviceAccount为运行在pod内的进程添加了相应的认证信息。当准入模块中开启了此插件(默认开启),如果pod没有serviceAccount属性,将这个pod的serviceAccount属性设为“default”;确保pod使用的serviceAccount始终存在;如果LimitSecretReferences 设置为true,当这个pod引用了Secret对象却没引用ServiceAccount对象,弃置这个pod;如果这个pod没有包含任何ImagePullSecrets,则serviceAccount的ImagePullSecrets被添加给这个pod;如果MountServiceAccountToken为true,则将pod中的container添加一个VolumeMount 。

  • ResourceQuota
    它会观察所有的请求,确保在namespace中ResourceQuota对象处列举的container没有任何异常。如果在kubernetes中使用了ResourceQuota对象,就必须使用这个插件来约束container。(推荐在admission control参数列表中,这个插件排最后一个。)

  • LimitRanger
    实现配额控制。他会观察所有的请求,确保没有违反已经定义好的约束条件,这些条件定义在namespace中LimitRange对象中。如果在kubernetes中使用LimitRange对象,则必须使用这个插件。

  • SecurityContextDeny
    禁止创建设置了 Security Context 的 pod。这个插件将会将使用了 SecurityContext的pod中定义的选项全部失效。关于 SecurityContext的描述:SecurityContext 在container中定义了操作系统级别的安全设定(uid, gid, capabilities, SELinux等等)。

  • NamespaceLifecycle
    确保处于termination状态的namespace不再接收新的对象创建请求,并拒绝请求不存在的namespace。

  • InitialResources
    根据镜像的历史使用记录,为容器设置默认资源请求和限制

  • DefaultStorageClass
    为PVC设置默认StorageClass

  • DefaultTolerationSeconds
    设置Pod的默认forgiveness toleration为5分钟

  • PodSecurityPolicy
    使用Pod Security Policies时必须开启

  • NodeRestriction
    限制kubelet仅可访问node、endpoint、pod、service以及secret、configmap、PV和PVC等相关的资源(v1.7版本以上才支持)

  • EventRateLimit (alpha)
    This plug-in is introduced in v1.9 to mitigate the problem where the API server gets flooded by event requests. The cluster admin can specify event rate limits by:
    Ensuring that eventratelimit.admission.k8s.io/v1alpha1=true is included in the –runtime-config flag for the API server;
    Enabling the EventRateLimit admission controller;
    Including a EventRateLimit configuration in the file provided to the API server’s command line flag –admission-control-config-file.
    There are four types of limits that can be specified in the configuration:
    Server: All event requests received by the API server share a single bucket.
    Namespace: Each namespace has a dedicated bucket.
    User: Each user is allocated a bucket.
    SourceAndObject: A bucket is assigned by each combination of source and involved object of the event.
    Below is a sample snippet for such a configuration:
    EventRateLimit:
    limits:

    • type: Namespace
      qps: 50
      burst: 100
      cacheSize: 2000
    • type: User
      qps: 10
      burst: 50
      See the EventRateLimit proposal for more details.
  • GenericAdmissionWebhook (alpha)
    This plug-in is related to the Dynamic Admission Control introduced in v1.7. The plug-in calls the webhooks configured via ExternalAdmissionHookConfiguration, and only admits the operation if all the webhooks admit it. Currently, the plug-in always fails open. In other words, it ignores the failed calls to a webhook.

  • Initializers (alpha)
    This plug-in is introduced in v1.7. The plug-in determines the initializers of a resource based on the existing InitializerConfigurations. It sets the pending initializers by modifying the metadata of the resource to be created. For more information, please check Dynamic Admission Control.

  • InitialResources (experimental)
    This plug-in observes pod creation requests. If a container omits compute resource requests and limits, then the plug-in auto-populates a compute resource request based on historical usage of containers running the same image. If there is not enough data to make a decision the Request is left unchanged. When the plug-in sets a compute resource request, it does this by annotating the the pod spec rather than mutating the container.resources fields. The annotations added contain the information on what compute resources were auto-populated.
    See the InitialResouces proposal for more details.

  • NamespaceAutoProvision
    This plug-in examines all incoming requests on namespaced resources and checks if the referenced namespace does exist. It creates a namespace if it cannot be found. This plug-in is useful in deployments that do not want to restrict creation of a namespace prior to its usage.

  • LimitPodHardAntiAffinity
    This plug-in denies any pod that defines AntiAffinity topology key other than kubernetes.io/hostname in requiredDuringSchedulingRequiredDuringExecution.

  • NamespaceExists
    This plug-in checks all requests on namespaced resources other than Namespace itself. If the namespace referenced from a request doesn’t exist, the request is rejected.

  • OwnerReferencesPermissionEnforcement
    This plug-in protects the access to the metadata.ownerReferences of an object so that only users with “delete” permission to the object can change it. This plug-in also protects the access to metadata.ownerReferences[x].blockOwnerDeletion of an object, so that only users with “update” permission to the finalizers subresource of the referenced owner can change it.

  • PersistentVolumeLabel
    This plug-in automatically attaches region or zone labels to PersistentVolumes as defined by the cloud provider, e.g. GCE and AWS. It helps ensure the Pods and the PersistentVolumes mounted are in the same region and/or zone. If the plug-in doesn’t support automatic labelling your PersistentVolumes, you may need to add the labels manually to prevent pods from mounting volumes from a different zone.

  • PodNodeSelector
    This plug-in defaults and limits what node selectors may be used within a namespace by reading a namespace annotation and a global configuration.

  • PodPreset
    This plug-in injects a pod with the fields specified in a matching PodPreset. See also PodPreset concept and Inject Information into Pods Using a PodPreset for more information.

  • PodTolerationRestriction
    This plug-in first verifies any conflict between a pod’s tolerations and its namespace’s tolerations, and rejects the pod request if there is a conflict. It then merges the namespace’s tolerations into the pod’s tolerations. The resulting tolerations are checked against the namespace’s whitelist of tolerations. If the check succeeds, the pod request is admitted otherwise rejected.
    If the pod’s namespace does not have any associated default or whitelist of tolerations, then the cluster-level default or whitelist of tolerations are used instead if specified.
    Tolerations to a namespace are assigned via the scheduler.alpha.kubernetes.io/defaultTolerations and scheduler.alpha.kubernetes.io/tolerationsWhitelist annotation keys.

  • Priority
    The priority admission controller uses the priorityClassName field and populates the integer value of the priority. If the priority class is not found, the Pod is rejected.

推荐的设置控制器顺序:

--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds

2.3.3 动态准入控制(Dynamic Admission Control)

传统的准入控制不够灵活,原因如下:

  • 准入控制代码需要被编译到kube-apiserver的二进制文件中,如果你寻找的目标缺失,你需要 fork Kubernetes,并且写一个准入插件,并自己保持 fork。
  • 只有在apiserver启动的时候才可以进行准入控制插件的配置变更

某些托管集群的供应商可能不允许自定义 apiserver的启动参数,因此可能无法启用源代码中可用的所有准入控制器。于是k8s1.7便引入了两个特性:Initializers和External Admission Webhooks来解决以上两个问题。即可以让准入控制代码独立于apiserver,并且可以动态修改配置。

Initializers

Initializers 类似于准入控制器 plug-ins, 你可以在创建资源之前截取它。但它们也与准入控制器 plug-ins 不同, 因为它们不是 Kubernetes 源代码的一部分, 也不是编译而成的。你需要自己写一个控制器。Initializers可以用来给资源执行策略或者配置默认选项,包括Initializers控制器和用户定义的Initializer任务,控制器负责执行用户提交的任务,并完成后将任务从metadata.initializers列表中删除。

Initializers的开启方法为

  • kube-apiserver配置–admission-control=…,Initializers
  • kube-apiserver开启admissionregistration.k8s.io/v1alpha1 API,即配置–runtime-config=admissionregistration.k8s.io/v1alpha1
    以上两项配置后,kubectl 创建deploment和daemonset啥的就超时无响应了,什么鬼???
  • 部署Initializers控制器

能用 Initializers 做什么

以下是对 Initializers 的一些想法,比如在你的集群中实施一个特定的策略:

  • 如果该容器开放 80 端口或具有特定的注释, 则向该 pod 注入一个代理 sidecar 容器。
  • 将带有测试证书的卷自动插入测试命名空间中的所有 pod。
    如果一个隐藏密令少于 20 个字符(有可能是密码),则阻止其创建。
  • 如果不打算修改对象并拦截只读对象, web 钩子可能会是一个更快和更精简的选择来获取有关对象的通知。

备注:上面列出的一些功能,例如注入 sidecar 容器或卷,也可以使用 Pod Presets 以牺牲灵活性来进行实现。

另外,可以使用initializerconfigurations来自定义哪些资源开启Initializer功能

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
  name: example-config
spec:
  initializers:
    # the name needs to be fully qualified, i.e., containing at least two "."
    - name: podimage.example.com
      rules:
        # apiGroups, apiVersion, resources all support wildcard "*".
        # "*" cannot be mixed with non-wildcard.
        - apiGroups:
            - ""
          apiVersions:
            - v1
          resources:
            - pods

注意,如果不需要修改对象的话,建议使用性能更好的GenericAdmissionWebhook。

如何开发Initializers

  • 参考Kubernetes Initializer Tutorial 开发Initializer
  • Initializer必须有一个全局唯一的名字,比如initializer.vaultproject.io
  • 对于Initializer自身的部署,可以使用Deployment,但需要手动设置initializers
    列表为空,以避免无法启动的问题,如
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  initializers:
    pending: []

Initializer有可能收到信息不全的资源(比如还未调度的Pod没有nodeName和status),在实现时需要考虑这种情况

GenericAdmissionWebhook

GenericAdmissionWebhook提供了一种Webhook方式的准入控制机制,它不会改变请求对象,但可以用来验证用户的请求。

GenericAdmissionWebhook的开启方法

注意,webhook准入控制器必须使用TLS,并需要通过externaladmissionhookconfigurations.clientConfig.caBundle向kube-apiserver注册:

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ExternalAdmissionHookConfiguration
metadata:
  name: example-config
externalAdmissionHooks:
- name: pod-image.k8s.io
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
  # fail upon a communication error with the webhook admission controller
  # Other options: Ignore
  failurePolicy: Fail
  clientConfig:
    caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
    service:
      name: <name of the front-end service>
      namespace: <namespace of the front-end service>

3. Kubelet 认证

总算到了 Kubelet 环节了,下面是 API Server 和 Kubelet 相关的内容:

API Server 参数

–kubelet-certificate-authority CA 证书的路径。

–kubelet-client-certificate TLS 证书文件

–kubelet-client-key TLS Key 文件

Kubelet 参数

–client-ca-file

请求中的客户端证书如果是由文件中的 CA 签署的,那么他的 Common Name 就会被用作 ID 进行认证。

–tls-cert-file

用来提供 HTTPS 服务的 x509 证书(其中也可包含中间人证书)。如果不提供–tls-cert-file和–tls-private-key-file,就会为主机地址生成一个自签名的证书和对应的 Key,并保存到–cert-dir目录里。

–tls-private-key-file

–tls-cert-file 对应的 Key

校验 kubelet 的请求是有用的,因为 Kubelet 的职责就是在主机上执行代码。

这里实际上有两个 CA,这里不准备深入描述,情况和 API Server 是一样的,Kubelet 用 TLS 来进行认证,也支持客户证书认证。

另外还要告知 API Server,用什么 CA 检查 Kubelet 的 TLS,另外用什么证书来跟 Kubelet 通信。

再说一次,这两个 CA 是可以不同的。

默认情况下,所有未被配置的其他身份验证方法拒绝的,对 kubelet 的 HTTPS 端点的请求将被视为匿名请求,并被授予 system:anonymous 用户名和 system:unauthenticated 组。

如果要禁用匿名访问并发送 401 Unauthorized 的未经身份验证的请求的响应:

启动 kubelet 时指定 –anonymous-auth=false 标志
如果要对 kubelet 的 HTTPS 端点启用 X509 客户端证书身份验证:

启动 kubelet 时指定 –client-ca-file 标志,提供 CA bundle 以验证客户端证书
启动 apiserver 时指定 –kubelet-client-certificate 和 –kubelet-client-key 标志
参阅 apiserver 认证文档 获取更多详细信息。
启用 API bearer token(包括 service account token)用于向 kubelet 的 HTTPS 端点进行身份验证:

确保在 API server 中开启了 authentication.k8s.io/v1beta1 API 组。
启动 kubelet 时指定 –authentication-token-webhook, –kubeconfig 和 –require-kubeconfig 标志
Kubelet 在配置的 API server 上调用 TokenReview API 以确定来自 bearer token 的用户信息
Kubelet 授权

接着对任何成功验证的请求(包括匿名请求)授权。默认授权模式为 AlwaysAllow,允许所有请求。

细分访问 kubelet API 有很多原因:

启用匿名认证,但匿名用户调用 kubelet API 的能力应受到限制
启动 bearer token 认证,但是 API 用户(如 service account)调用 kubelet API 的能力应受到限制
客户端证书身份验证已启用,但只有那些配置了 CA 签名的客户端证书的用户才可以使用 kubelet API
如果要细分访问 kubelet API,将授权委托给 API server:

确保 API server 中启用了 authorization.k8s.io/v1beta1 API 组
启动 kubelet 时指定 –authorization-mode=Webhook、 –kubeconfig 和 –require-kubeconfig 标志
kubelet 在配置的 API server 上调用 SubjectAccessReview API,以确定每个请求是否被授权
kubelet 使用与 apiserver 相同的 请求属性 方法来授权 API 请求。

Verb(动词)是根据传入的请求的 HTTP 动词确定的:

HTTP 动词 request 动词
POST create
GET, HEAD get
PUT update
PATCH patch
DELETE delete
资源和子资源根据传入请求的路径确定:

Kubelet API 资源 子资源
/stats/* nodes stats
/metrics/* nodes metrics
/logs/* nodes log
/spec/* nodes spec
all others nodes proxy
Namespace 和 API 组属性总是空字符串,资源的名字总是 kubelet 的 Node API 对象的名字。

当以该模式运行时,请确保用户为 apiserver 指定了 –kubelet-client-certificate 和 –kubelet-client-key 标志并授权了如下属性:

verb=*, resource=nodes, subresource=proxy
verb=*, resource=nodes, subresource=stats
verb=*, resource=nodes, subresource=log
verb=*, resource=nodes, subresource=spec
verb=*, resource=nodes, subresource=metrics

参考

http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/
https://www.kubernetes.org.cn/service-account
http://blog.csdn.net/qq_34463875/article/details/78042852
http://dockone.io/article/1474
http://blog.csdn.net/yan234280533/article/details/76320414
https://kubernetes.io/docs/admin/admission-controllers/
https://ahmet.im/blog/initializers/
http://docs.kubernetes.org.cn/27.html
https://zhuanlan.zhihu.com/p/27700061

猜你喜欢

转载自blog.csdn.net/liukuan73/article/details/78710496
k8s