Kubernetes过一系列机制来实现集群的安全机制,包括API Server的认证授权、准入控制机制及保护敏感信息的Secret机制等。集群的安全性必须考虑以下的几个目标:
- 保证容器与其所在宿主机的隔离;
- 限制容器给基础设施及其他容器带来消极影响的能力;
- 最小权限原则,合理限制所有组件权限,确保组件只执行它被授权的行为,通过限制单个组件的能力来限制他所能达到的权限范围;
- 明确组件间边界的划分;
- 划分普通用户和管理员角色;
- 在必要的时候允许将管理员权限赋给普通用户;
- 允许拥有Secret数据(Keys、Certs、Passwords)的应用在集群中运行;
下面分别从Authentication、Authorization、Admission Control、Secret和Service Account等方面来说明集群的安全机制。
API Server认证
Kubernetes集群中所有资源的访问和变更都是通过Kubernetes API Server的REST API来实现的,所以集群安全的关键点在于识别认证客户端身份(Authentication)以及访问权限的授权(Authorization)。
Kubernetes提供管理三种级别的客户端身份认证方式:
- 最严格的HTTPS证书认证:基于CA根证书签名的双向数字证书认证方式;
- HTTP Token认证:通过一个Token来识别合法用户;
- HTTP Base认证:通过用户名+密码的方式认证;
SSL双向认证步骤:
- HTTPS通信双方的务器端向CA机构申请证书,CA机构是可信的第三方机构,它可以是一个公认的权威的企业,也可以是企业自身。企业内部系统一般都使用企业自身的认证系统。CA机构下发根证书、服务端证书及私钥给申请者;
- HTTPS通信双方的客户端向CA机构申请证书,CA机构下发根证书、客户端证书及私钥个申请者;
- 客户端向服务器端发起请求,服务端下发服务端证书给客户端。客户端接收到证书后,通过私钥解密证书,并利用服务器端证书中的公钥认证证书信息比较证书里的消息,例如域名和公钥与服务器刚刚发送的相关消息是否一致,如果一致,则客户端认为这个服务器的合法身份;
- 客户端发送客户端证书给服务器端,服务端接收到证书后,通过私钥解密证书,获得客户端的证书公钥,并用该公钥认证证书信息,确认客户端是否合法;
- 客户端通过随机秘钥加密信息,并发送加密后的信息给服务端。服务器端和客户端协商好加密方案后,客户端会产生一个随机的秘钥,客户端通过协商好的加密方案,加密该随机秘钥,并发送该随机秘钥到服务器端。服务器端接收这个秘钥后,双方通信的所有内容都都通过该随机秘钥加密;
CA认证流程图:
上述是双向SSL协议的具体通信过程,这种情况要求服务器和用户双方都有证书。单向认证SSL协议不需要客户拥有CA证书,对应上面的步骤,只需将服务器端验证客户端证书的过程去掉,以及在协商对称密码方案和对称通话秘钥时,服务器端发送给客户端的是没有加过密的(这并不影响SSL过程的安全性)密码方案。
HTTP Token原理:HTTP Token的认证是用一个很长的特殊编码方式的并且难以被模仿的字符串——Token来表明客户身份的一种方式。在通常情况下,Token是一个复杂的字符串,比如我们用私钥签名一个字符串的数据就可以作为一个Token,此外每个Token对应一个用户名,存储在API Server能访问的一个文件中。当客户端发起API调用请求时,需要在HTTP Header里放入Token,这样一来API Server就能够识别合法用户和非法用户了。
HTTP Base:常见的客户端账号登录程序,这种认证方式是把“用户名+冒号+密码”用BASE64算法进行编码后的字符串放在HTTP REQUEST中的Header Authorization域里发送给服务端,服务端收到后进行解码,获取用户名及密码,然后进行用户身份的鉴权过程。
API Server授权
对合法用户进行授权(Authorization)并且随后在用户访问时进行鉴权,是权限与安全系统的重要一环。授权就是授予不同用户不同访问权限,API Server目前支持一下集中授权策略:
- AlwaysDeny:拒绝所有请求,该配置一般用于测试;
- AlwaysAllow:接收所有请求,如果集群不需要授权流程,可以采用该策略,此为Kubernetes默认的策略;
- ABAC:(Attribute-Base Access Control)为基于属性的访问控制,表示使用用户配置的授权规则去匹配用户的请求;
为了简化授权的复杂度,对于ABAC模式的授权策略,Kubernetes仅有下面四个基本属性:
- 用户名(代表一个已经被认证的用户的字符型用户名)
- 是否是只读请求(REST的GET操作是只读的)
- 被访问的是哪一类资源,例如Pod资源/api/v1/namespaces/default/pods
- 被访问对象所属的Namespace
当API Server启用ABAC模式时,需要指定授权文件的路径和名字(--authorization_policy_file=SOME_FILENAME),授权策略文件里的每一行都是一个Map类型的JOSN对象,被称为访问策略对象,我们可以通过设置“访问策略对象”中的如下属性来确定具体的授权行为:
- user:字符串类型,来源于Token文件或基本认证文件中的用户名字段的值;
- readonly:true时表示该策略允许GET请求通过;
- resource:来自于URL的资源,例如“Pod”;
- namespace:表明该策略允许访问某个namespace的资源;
eg:
- {"user":"alice"}
- {"user":"kubelet","resource":"Pods","readonly":true}
- {"user":"kubelet","resource":"events"}
- {"user":"bob","resource":"Pods","readonly":true,"ns":"myNamespace"}
Admission Control准入控制
通过认证和鉴权之后,客户端并不能得到API Server的真正响应,这个请求还需通过Admission Control所控制的一个“准入控制链”的层层考验,Admission Control配备有一个“准入控制器”的列表,发送给API Server的任何请求都需要通过列表中每个准入控制器的检查,检查不通过API Server拒绝此调用请求。此外,准入控制器还能够修改请求参数以完成一些自动化的任务。比如Service Account这个控制器,当前可配置的准入控制如下:
- AlwaysAdmit:允许所有请求;
- AlwaysPullmages:在启动容器之前总去下载镜像,相当于在每个容器的配置项imagePullPolicy=Always
- AlwaysDeny:禁止所有请求,一般用于测试;
- DenyExecOnPrivileged:它会拦截所有想在Privileged Container上执行命令的请求,如果你的集群支持Privileged Container,你又希望限制用户在这些Privileged Container上执行命令,强烈推荐你使用它;
- Service Account:这个plug-in将ServiceAccount实现了自动化,默认启用,如果你想使用ServiceAccount对象,那么强烈你推荐使用它;
- SecurityContextDeny:这个插件将使用SecurityContext的Pod中的定义全部失效。SecurityContext在Container中定义了操作系统级别的安全设定(uid,gid,capabilityes,SELinux等)
- ResourceQuota:用于配额管理目的,作用于namespace上,它会观察所有请求,确保在namespace上的配额不会超标。推荐在Admission Control参数列表中这个插件排最后一个;
- LimitRanger:用于配额管理,作用于Pod与Container,确保Pod与Container上的配额不会超标;
- NamespaceExists(已过时):对所有请求校验namespace是否已存在,如果不存在则拒绝请求,已合并至NamespaceLifecycle。
- NamespaceAutoProvision(已过时):对所有请求校验namespace,如果不存在则自动创建该namespace,推荐使用NamespaceLifecycle。
- NamespaceLifecycle:如果尝试在一个不存在的namespace中创建资源对象,则该创建请求将被拒绝。当删除一个namespace时,系统将会删除该namespace中所有对象,保存Pod,Service等。
在API Server上设置--admission-control参数,即可定制我们需要的准入控制链,如果启用多种准入控制选项,则建议的设置如下:
- --admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
下面着重介绍三个准入控制器:
SecurityContextDeny
Security Context时运用于容器的操作系统安全设置(uid、gid、capabilities、SELinux role等),Admission Control的SecurityContextDeny插件的作用是,禁止创建设置了Security Context的Pod,例如包含以下配置项的Pod:
- spec.containers.securityContext.seLinuxOptions
- spec.containers.securityContext.runAsUser
ResourceQuota
ResourceQuota不仅能够限制某个Namespace中创建资源的数量,而且能够限制某个namespace中被Pod所请求的资源总量。该准入控制器和资源对象ResourceQuota一起实现了资源的配额管理;
LimitRanger
准入控制器LimitRanger的作用类似于上面的ResourceQuota控制器,这对Namespace资源的每个个体的资源配额。该插件和资源对象LimitRange一起实现资源限制管理。
Service Account
Servuce Account是一种账号,但他并不是给Kubernetes的集群的用户(系统管理员、运维人员、租户用户等),而是给运行在Pod里的进程用的,它为Pod里的进程提供必要的身份证明。
Pod中访问Kubernetes API Server服务的时候,是以Service方式访问服务名为kubernetes这个服务的,而kubernetes服务又只在HTTPS安全端口443上提供服务,那么如何进行身份认证呢?在Kubernetes的官方文档并没有清除的说明这个问题。
通过查看源码获知这是在用一种类似HTTP Token的新的认证方式--ServiceAccount Auht,Pod中的客户端调用Kubernetes API的时候,在HTTP Header中传递了一个Token字符串,这类似于之前提到的HTTP Token认证方式,存在以下几个不同点:
- 此处的Token的内容来源于Pod里指定路径下的一个文件(/run/secrets/kubernetes.io/serviceaccount/token),这种token是动态生成的,确切的说,是由KubernetesController进程用API Server的私钥(--service-account-private-key-file指定的私钥)签名生成的一个JWT Secret。
- 官方提供的客户端REST框架代码里,通过HTTPS方式与API Server建立链接后,会用Pod里指定路径下的一个CA证书(/run/secrets/kubernetes.io/serviceaccount/ca.crt)验证API Server发来的证书,验证是否是被CA证书签名的合法证书。
- API Server收到这个Token以后,采用自己的私钥(实际是使用参数service-account-key-file)指定的私钥,如果此参数没有设置,则默认采用tls-private-key-file指定的参数,即自己的私钥,对token进行合法性验证。
明白原理之后。接下来分析认证过程中涉及的Pod中的三个文件:
- /run/secrets/kubernetes.io/serviceaccount/token
- /run/secrets/kubernetes.io/serviceaccount/ca.crt
- /run/secrets/kubernetes.io/serviceaccount/namespace(客户端采用这里指定的namespace作为参数调用Kubernetes API)
这三个文件由于参与到Pod进程与API Server认证的过程中,起到了类似Secret(私密凭据)的作用,所以他们被称为Kubernetes Secret对象。Secret从属于ServiceAccount资源对象,属于Service Account的一部分,一个ServiceAccount对象里面可以包括多个不同的Secret对象,分别用于不同目的的认证活动。
下面通过命令来直观的加深对ServiceAccount的认识:
查看系统中ServiceAccount对象,可以看到一个名为default的Service Account对象,包含一个名为default-token-xxx的Secret,这个Secret同时是“Mountable secrets”,表明他是需要被Mount到Pod上的。
- kubectl describe serviceaccounts
- kubectl describe secrets default-token-xxx
default-token-xxx包括三个数据项:
- token
- ca.crt
- namespace
联想到“Mountable secrets”的标记,以及之前看到的Pod中的三个文件的文件名:每个namespace下有一个名为default的默认的ServiceAccount对象,这个ServiceAccount里有一个名为Tokens的可以作为Volume一样被Mount到Pod里的Secret,当Pod启动时这个Secret会被自动Mount到Pod的指定目录下,用来协助完成Pod中的进程访问API Server时的身份鉴权过程。
一个ServiceAccount可以包括多个Secrets对象:
- 名为Tokens的Secret用于访问API Server的Secret,也被称为ServiceAccountSecret;
- 名为Image Pull secrets的Secret用于下载容器镜像时的认证过程,通常镜像库运行在Insecure模式下,所以这个Secret为空;
- 用户自定义的其他Secret,用于用户的进程;
如果一个Pod在定义时没有指定spec.service.AccountName属性,则系统会自动为其赋值为“Default”,即使用同一namespace下默认的ServiceAccount,如果某个Pod需要使用非default的ServiceAccount,需要在定义时指定:
apiVersion:v1
kind:Pod
metadata:
name:mypod
spec:
containers:
- name:mycontainer
image:
serviceAccountName:myserviceaccount
Kubernetes之所以要创建两套独一的账号系统,原因如下:
- User账号是给人用的,ServiceAccount是给Pod里的进程使用的,面向对象不同;
- User账号是全局性的,ServiceAccount则属于某个具体的Namespace;
- 通常来说,User账号是与后端的用户数据库同步的,创建一个新用户通常要走一套复杂的业务流程才能实现,ServiceAccount的创建则需要极轻量级实现方式,集群管理员可以很容易为某些特定任务组创建一个ServiceAccount。
- 对于这两种不同的账户,其审计要求通常不同;
- 对于一个复杂的系统来说,多个组件通常拥有各种账号的配置信息,ServiceAccount是Namespace隔离的,可以针对组件进行一对一的定义,同时具备很好的“便携性”。
下面分析Service Account与Secret相关的一些运行机制:
Controller manager创建了ServiceAccountController与Token Controllerl两个安全相关的控制器。其中ServiceAccountController一直监听Service Account和Namespace的事件,如果一个Namespace中没有default Service Account,那么Service Account Controller就会为该Namespace创建一个默认的(default)的Service Account,这就是我们之前看到的每个namespace下都有一个名为default的ServiceAccount的原因。
如果Controller manager进程在启动时指定了API Server私钥(service-account-private-key-file)参数,那么Controller manager会创建Token Controller。Token Controller也监听Service Account的事件,如果发现新建的Service Account里没有对应的Service Account Secret,则会用API Server私钥创建一个Token(JWT Token),并用该Token、CA证书Namespace名称等三个信息产生一个新的Secret对象,然后放入刚才的Service Account中;如果监听到的事件是删除Service Account事件,则自动删除与该Service Account相关的所有Secret。此外,Token Controller对象同时监听Secret的创建、修改和删除事件,并根据事件的不同做不同的处理。
当我们在API Server的鉴权过程中启用了Service Account类型的准入控制器,即在kube-apiserver的启动参数中包括下面的内容时:
- --admission_control=ServiceAccount
则针对Pod新增或修改的请求,Service Account准入控制器会验证Pod里Service Account是否合法。
- 如果spec.serviceAccount域没有被设置,则Kubernetes默认为其制定名字为default的Serviceaccount;
- 如果Pod的spec.serviceAccount域指定了default以外的ServiceAccount,而该ServiceAccount没有事先被创建,则该Pod操作失败;
- 如果在Pod中没有指定“ImagePullSecrets”,那么该sec.serviceAccount域指定的ServiceAccount的“ImagePullSecrets”会被加入该Pod;
- 给Pod添加一个新的Volume,在该Volume中包含ServiceAccountSecret中的Token,并将Volume挂载到Pod中所有容器的指定目录下(/var/run/secrets/kubernetes.io/serviceaccount);
综上所述,ServiceAccount正常运行需要以下几个控制器:
- Admission Controller
- Token Controller
- Service Account Controller
Secret私密凭据
Secret主要作用是保管私密数据,比如密码、OAuth Tokens、SSH Keys等信息。将这些私密信息放在Secret对象中比直接放在Pod或Docker Image中要更安全,也便于使用和分发。
- 创建Secret
secret.yaml
apiVersion:v1
kind:Secret
metadata:
name:mysecret
type: Opaque
data:
password:dmfsdWUtMg0k
username:dmfsdWUtMg0k
kubectl create -f secret.yaml
在上面的data域中的各子域的值必须为BASE64编码值,其中password域和username域BASE64编码前的值分别为value-1和value-2。一旦secret被创建,可以通过以下三个方式使用它:
- 在创建Pod时,通过为Pod指定ServiceAccount来自动使用该Seret;
- 通过挂载该Secret到Pod来使用它;
- Docker镜像下载时使用,通过指定Pod的spec.ImagePullSecrets来引用它;
- 第一种方式主要用在API Server鉴权方面;
- 第二种方式如下:
apiVersion:v1
kind:Pod
metadata:
name:mypod
namespace:myns
spec:
containers:
- name:mycontainer
image:redis
volumeMounts:
- name:foo
mountPath:“/etc/foo”
readOnly:true
volumes:
- name:foo
secret:
secretName:mysecret
- 第三种方式如下:
- 执行login登录私有registry
- docker login localhost:5000(输入账户及密码,则会创建新用户,并把相关信息写入~/。dockercfg文件中)
- 用BASE64编码dockercfg的内容;
- cat ~/.dockercfg|grep base64
- 将上一步命令的输出结果作为secret的“data.dockercfg”域的内容,由此来创建一个Secret:
image-pull-secret.yaml:
apiVersion:v1
kind:Secret
metadata:
name:myregistrykey
data:
.dockercfg:xxx
type:kubernetes.io/dockercfg
- kubectl create -f image-pull-secret.yaml
- 在创建Pod的时候引用该Secret:
pods.yaml
apiVersion:v1
kind:Pod
metadata:
name:mypod2
spec:
containers:
- name:foo
image:xxxxxx:v1
imagePullSecrets:
- name:myregistrykey
- kubectl create -f pods.yaml
每个单独的Secret大小不能超过1M,Kubernetes不鼓励创建大尺寸的Secret,因为如果使用大尺寸的Secret,则将大量占用API Server和kubelet的内存。当然创建许多小的Secret也能耗尽API Server和kubelet的内存。
在使用Mount方式挂载Secret时,Container中Secret的“data”域的各个域的key值作为目录中的文件,Value值被BASE64编码后存储在相应的文件中。前面的例子中创建的Secret,被挂载到一个叫做mycontainer的container中,在该container中可以通过命令查看所生产的文件和文件中的内容:
- ls /etc/foo
username
password
- cat /etc/foo/username
value-1
- cat /etc/foo/password
value-2
我们可以通过Secret保管其他系统的敏感信息(比如数据库用户名和密码),并以Mount的方式将Secret挂载到Container中,然后通过访问目录中的文件的方式获取该敏感信息。当Pod被API Server创建时,API Server不会校验该Pod引用的Secret是否存在。一旦这个Pod被调度,则Kubelet将试着获取Secret的值。如果Secret不存在或暂时无法连接到API Server,则kubelet将按一定的时间间隔定期重试获取该Secret,并发送一个Event来解释Pod没有启动的原因。一旦Secret被Pod获取,则Kubelet将创建并Mount包含Secret的Volume。只有所有的Volume被Mount后,Pod中的Container才会被启动。在kubelet启动Pod中container后,Container中和Secret相关的Volume将不会被改变,即使Secret本身被修改了。为了使用更新后的Secret,必须删除旧的Pod,并重新创建一个新的Pod,因此更新Secret的流程和部署一个新的Image是一样的。