在基于k8s做应用开发的时候,都是使用admin来使用k8s,基本不用去关注授权的问题。但是,当我们将k8s作为PaaS平台的容器编排引擎,并引入多租户时,就涉及到权限管理相关的问题了,paas平台的安全都是基于k8s的安全机制来实现的。

Kubernetes中的隔离主要包括这几种:

  • 网络隔离:需要使用网络插件,比如flannel, calico。
  • 资源隔离:kubernetes原生支持资源隔离,pod就是资源隔离和调度的最小单位,同时使用namespace限制用户空间和资源限额。
  • 身份隔离:使用RBAC-基于角色的访问控制,多租户的身份认证和权限控制。

那么k8s的安全机制有哪些?首先我们先来了解一些概念。

基础概念

用户

Kubernetes 中有两种用户:

一种是内置“用户”:ServiceAccount,用于集群内的资源的一种身份,便于认证授权操作,我们叫做系统服务用户。
一种就是我们这种操作集群的人,实际操作 "kubectl" 命令的人,我们直接叫用户。

Service Account

Service Account概念的引入是基于这样的使用场景:运行在pod里的进程需要调用Kubernetes API以及非Kubernetes API的其它服务。这时候pod就需要一个身份做认证授权。所以Service Account它并不是给kubernetes集群的用户使用的,而是给pod里面的进程使用的,它为pod提供必要的身份认证。

kubectl get sa --all-namespaces

NAMESPACE     NAME          SECRETS   AGE
default       build-robot   1         1d
default       default       1         32d
default       kube-dns      1         31d
kube-public   default       1         32d
kube-system   dashboard     1         31d
kube-system   default       1         32d
kube-system   heapster      1         30d
kube-system   kube-dns      1         31d

如果kubernetes开启了ServiceAccount(–admission_control=…,ServiceAccount,… )那么会在每个namespace下面都会创建一个默认的default的sa,可见sa是namespace级别的。其中最重要的就是secrets,它是每个sa下面都会拥有的一个加密的token。

kubectl get sa  default  -o yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2017-05-02T06:39:12Z
  name: default
  namespace: default
  resourceVersion: "175"
  selfLink: /api/v1/namespaces/default/serviceaccounts/default
  uid: 0de23575-2f02-11e7-98d0-5254c4628ad9
secrets:
- name: default-token-rsf8r

当用户再该namespace下创建pod的时候都会默认使用这个sa,kubernetes会把默认的sa挂载到容器内。

看一下这个secret

kubectl get secret default-token-rsf8r -o yaml
apiVersion: v1
data:
  ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR2akNDQXFhZ0F3SUJBZ0lVZlpvZDJtSzNsa3JiMzR3NDhhUmtOc0pVVDJjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pURUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbAphVXBwYm1jeEREQUtCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByCmRXSmxjbTVsZEdWek1CNFhEVEUzTURVd01qQTNNekF3TUZvWERUSXlNRFV3TVRBM016QXdNRm93WlRFTE1Ba0cKQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFVcHBibWN4RURBT0JnTlZCQWNUQjBKbGFVcHBibWN4RERBSwpCZ05WQkFvVEEyczRjekVQTUEwR0ExVUVDeE1HVTNsemRHVnRNUk13RVFZRFZRUURFd3ByZFdKbGNtNWxkR1Z6Ck1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2E5Zk1HVGd2MGl0YnlZcHoycXkKOThKWktXdWdFL0VPbXRYS2ExT0Y3ekUxSFh1cDFOVG8rNkhvUEFuR3hhVzg4Q0s0TENrbWhNSGFLdUxnT3IvVApOMGphdnc5YWlPeVdYR1hXUUxVN3U0aVhoaDV6a2N4bmZxRW9JOW9JV2dMTzVEL3hBL0tnZzRQZDRMeFdqMkFQCk4rcVdxQ2crU3BrdkpIQUZWL3IyTk1BbEIzNHBrK0t5djVQMDJSQmd6Y2xTeSs5OUxDWnlIQ1VocGl0TFFabHoKdUNmeGtBeUNoWFcxMWNKdVFtaDM4aFVKa0dhUW9OVDVSNmtoRTArenJDVjVkWnNVMVZuR0FydWxaWXpJY3kregpkeUZpYWYyaitITyt5blg4RUNySzR1TUF3Nk4zN1pnNjRHZVRtbk5EWmVDTTlPelk5czBOVzc1dHU5bHJPZTVqCnZRSURBUUFCbzJZd1pEQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFqQWQKQmdOVkhRNEVGZ1FVK2RqMThRUkZyMWhKMVhGb1VyYUVVRnpEeVRBd0h3WURWUjBqQkJnd0ZvQVUrZGoxOFFSRgpyMWhKMVhGb1VyYUVVRnpEeVRBd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFBazQ4ODZBa0Fpa3VBVWRiOWU1CitldkVXVVFFaTIyTmc4REhmVTVSbXppU2ZhVllFQ1FuTlBUREprMmYvTm1Kb3RUVWxRZS9Ec3BkNEk1TFova1IKMGI2b1VoZkdmTkVOOXVObkkvZEgzOFBjUTNDaWtVeHhaeFRYTytaaldxcGNHZTRLNzZtaWd2ZWhQR2Z1VUNzQwp0UmZkZDM2YkhnRjN4MzRCWnc5MStDQ2VKQzBSWmNjVENqcHFHUEZFQlM3akJUVUlRVjNodnZycWJMV0hNeTJuCnFIck94UFI1eFkrRU5SQ0xzVWNSdk9icUhBK1g0c1BTdzBwMWpROXNtK1lWNG1ybW9Gd1RyS09kK2FqTVhzVXkKL3ZRYkRzNld4RWkxZ2ZvR3BxZFN6U1k0MS9IWHovMjZWNlFWazJBajdQd0FYZmszYk1wWHdDamRXRG4xODhNbQpXSHM9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
  namespace: ZGVmYXVsdA==
  token: ZXlKaGJHY2lPaUpTVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUprWldaaGRXeDBJaXdpYTNWaVpYSnVaWFJsY3k1cGJ5OXpaWEoyYVdObFlXTmpiM1Z1ZEM5elpXTnlaWFF1Ym1GdFpTSTZJbVJsWm1GMWJIUXRkRzlyWlc0dGNuTm1PSElpTENKcmRXSmxjbTVsZEdWekxtbHZMM05sY25acFkyVmhZMk52ZFc1MEwzTmxjblpwWTJVdFlXTmpiM1Z1ZEM1dVlXMWxJam9pWkdWbVlYVnNkQ0lzSW10MVltVnlibVYwWlhNdWFXOHZjMlZ5ZG1salpXRmpZMjkxYm5RdmMyVnlkbWxqWlMxaFkyTnZkVzUwTG5WcFpDSTZJakJrWlRJek5UYzFMVEptTURJdE1URmxOeTA1T0dRd0xUVXlOVFJqTkRZeU9HRmtPU0lzSW5OMVlpSTZJbk41YzNSbGJUcHpaWEoyYVdObFlXTmpiM1Z1ZERwa1pXWmhkV3gwT21SbFptRjFiSFFpZlEuSmxuamM0Y0xNYkZrRlJVQjIyWGtFN2t4bTJ1dS1aQm9sUTh4VEdDNmdLOTdSZTVOMzBuY2V0SWJsanVOVWFlaDhtMDk2R19nMHE3cmRvWm5XMTV2OFBVXzNyM1hWWlBNc1lxbGRpZlNJbWtReXFqeEphVlBka3Izam5GWVBkVWNaTmk3MFF3cWtEdm5sMXB4SFRNZTZkTVNPTlExbUVoMHZSbHBhRTdjVWtTVlg5blRzaFVJVTVXWE9wRUxwdTVjVjBHV3ZGeDRDSzR6Umt3clNMdlV5X2d5UGZwLWdYVFZQWU80NkJKSWZtaVhlZGhVaW9nempPN285eGxDbUxQVkhyNkFIZGViNExiTVA1dkJ2MlBSZ2RrMW9keTR0VEdxLVRGU3M2VkNoMTZ4dk5IdTRtRVN5TjZmcXVObzJwYUFOelY4b251aTJuaU4yNTU1TzN4SFdR
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
    kubernetes.io/service-account.uid: 0de23575-2f02-11e7-98d0-5254c4628ad9
  creationTimestamp: 2017-05-02T06:42:07Z
  name: default-token-rsf8r
  namespace: default
  resourceVersion: "12551"
  selfLink: /api/v1/namespaces/default/secrets/default-token-rsf8r
  uid: 75c0a236-2f02-11e7-98d0-5254c4628ad9
type: kubernetes.io/service-account-token

上面的内容是经过base64加密过后的,我们直接进入容器内

~ # ls -l  /var/run/secrets/kubernetes.io/serviceaccount/
total 0
lrwxrwxrwx    1 root     root            13 May  4 23:57 ca.crt -> ..data/ca.crt
lrwxrwxrwx    1 root     root            16 May  4 23:57 namespace -> ..data/namespace
lrwxrwxrwx    1 root     root            12 May  4 23:57 token -> ..data/token

可以看到已将ca.crt 、namespace和token放到容器内了,那么这个容器就可以通过https的请求访问apiserver了,这就是所有的pod都能走apiserver的原因。

service account是namespace作用域,而User是全cluster唯一。service account会对应一个虚拟User,User名为system:serviceaccount:${namespace}:${sa_name},比如在default namespace的test service account,则对应的虚拟User为system:serviceaccount:default:test。

用户

可以理解成实际操作 “kubectl” 命令的人,区别于 pod 等资源概念的实际操作的k8s集群的运维人员或者用户,也就是我们的平常的用户。

Kubernetes没有用户以及用户组

Kubernetes并没有提供用户管理和身份认证功能,除了Service Account外,所有的用户信息都依赖外部的用户管理系统来存储,因此通过api-serever根本无法列出User和Group。这符合UNIX设计哲学的,即Do One Thing and Do It Well。Kubernetes只专注于做应用编排,其他的功能则提供接口集成,我们使用的CNI,CSI,CRI都是这个思想。

这样做的好处也显而易见,用户账户信息与Kubernetes集群松耦合,便于集成企业已有的身份认证系统,如AD、LADP、Keycloak等。

我们在写rolebingding的时候,Subjects需要填User或者Group,我们一般使用sa,也可以使用外部身份认证系统。

多租户

1、租户

  • 租户主要是”租”,讲究的是是你租用的一个资源,就属于你私有的,和用户是两个概念。

比如你使用阿里云的存储服务,阿里是“租”给你它的存储设备,你是它的租户,别人不会看见你存储的数据,就像你租的房子别人不能翻查一样。当然同时你也是阿里云网站的用户。

2、多租户的目的?

  • 隔离。

3、k8s多租户的实现

  • 一般可以将租户及其 Kubernetes 资源分隔到单独的命名空间中。然后,您可以使用策略来强制执行租户隔离。通常按命名空间划分,可用于限制 API 访问、资源使用以及允许容器执行的操作。

4、多租户使用场景:企业内部共享集群的多租户

我们可以通过命名空间对不同部门或团队进行资源的逻辑隔离,同时定义以下几种角色的业务人员:

  • 集群管理员(cluster admin)
    • 具有集群的管理能力(扩缩容、添加节点等操作)
    • 负责为租户管理员创建和分配命名空间
    • 负责各类策略(RAM/RBAC/networkpolicy/quota…)的 CRUD
  • 租户管理员(namespace admin)
    • 至少具有集群的 RAM 只读权限
    • 管理租户内相关人员的 RBAC 配置
  • 租户内用户(namespace user)
    • 在租户对应命名空间内使用权限范围内的 k8s 资源

安全机制

APIServer提供了3A机制:认证机制(Authentication)、授权机制(Authorization)以及准入控制机制(Admission Controllers)。

认证机制(Authentication)

k8s 中的认证机制,是在用户访问 APIServer 的第一步。这一步往往只检测请求头或客户端证书,通俗的讲这一步就是验证用户名密码的。所以也就是我们正常使用的认证方式,比如token,basic等。

认证支持好几种方式:

  • 客户端证书
  • Bearer Tokens
    • Service Account Token
    • BootStrap Token
    • Static Token
  • HTTP Basic Auth
  • Authenticating Proxy

1、客户端证书

x509认证是默认开启的认证方式,api-server启动时会指定ca证书以及ca私钥,只要是通过ca签发的客户端x509证书,则可认为是可信的客户端。所以带着签发的客户端证书是可以通过https的安全验证的。

签发客户端证书有两种方式,一种是基于CA根证书签发证书,另一个种是发起CSR(Certificate Signing Requests)请求。

使用CA根证书签发客户端证书

使用CA根证书需要CA的私钥,假设要创建一个int32bit用户,所属的组为int32bit,使用openssl签发证书:

openssl genrsa -out int32bit.key 2048 # 生成私钥
openssl req -new -key int32bit.key -out int32bit.csr -subj "/CN=int32bit/O=int32bit" # 发起CSR请求
openssl x509 -req -in int32bit.csr -CA $CA_LOCATION/ca.crt -CAkey $CA_LOCATION/ca.key -CAcreateserial -out int32bit.crt -days 365 # 基于CSR文件签发x509证书

其中CA_LOCATION为api server的CA证书路径,使用kubeadm部署一般为/etc/kubernetes/pki/。

最后生成config文件:

kubectl config set-credentials int32bit \
  --client-certificate=int32bit.crt \
  --client-key=int32bit.key \
  --embed-certs=true \
  --kubeconfig="int32bit@int32bit-kubernetes.config.config"

注意使用–embed-certs参数,这样才会把证书内容填充到kubeconfig文件,否则仅填充证书路径。

通过CSR签发证书

前面通过CA签发证书需要有CA的私钥,其实Kubernetes可以直接发起CSR请求。

首先创建一个CSR请求,CN为test-csr,O为int32bit,即User为test-csr,Group为int32bbit。

openssl req -new -newkey rsa:4096 \
  -nodes -keyout test-csr.key \
  -out test-csr.csr -subj "/CN=test-csr/O=int32bit"

声明一个CSR Resource:

apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: test-csr
spec:
  groups:
  - int32bit
  request: ... # 这里填test-csr.csr内容并转化为base64
  usages:
  - client auth

创建该资源:

# kubectl apply -f test-csr.yaml
certificatesigningrequest.certificates.k8s.io/test-csr created
# kubectl get csr
NAME       AGE   REQUESTOR          CONDITION
test-csr   4s    kubernetes-admin   Pending

此时CSR的状态为Pending,通过kubectl certificate approve命令签发证书:

# kubectl certificate approve test-csr
certificatesigningrequest.certificates.k8s.io/test-csr approved
# kubectl get csr
NAME       AGE   REQUESTOR          CONDITION
test-csr   2m    kubernetes-admin   Approved,Issued

此时CSR显示已经完成签发,可以读取证书内容:

# kubectl get csr test-csr -o jsonpath='{.status.certificate}' | base64 -d
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIUB9dVsj34xnQ8m5KUQwpdblWapNcwDQYJKoZIhvcNAQEL
...
yvfz8hcwrhQc6APpmZcBnil7iyzia3tnztQjoyaZ0cjC
-----END CERTIFICATE-----

查看证书部分摘要信息:

# kubectl get csr test-csr -o jsonpath='{.status.certificate}' \
  | base64 -d \
  | openssl x509  -noout  \
  -subject  -issuer
subject=O = int32bit, CN = test-csr
issuer=CN = kubernetes

配置kubeconfig使用证书认证的方式和前面的一样。

总结

可见,客户端向k8s提交带有用户名、用户组等信息的CSR,然后管理员在k8s上为该客户签发证书。当然也可以基于ca私钥的基础上直接使用openssl签发证书,用户名和密码都是隐藏在证书中的,以后,用户使用该证书去访问k8s,api-server就能够辨识出用户。结合RBAC的配置,如果该用户或者用户组通过rolebinding绑定了role,那么该用户就拥有该role对对应资源的操作权限。

我们搭建k8s过程中签发的CA证书就是一个客户端证书,是一个拥有集群管理员权限的证书。

使用x509证书相对静态密码来说显然会更安全,只要证书不泄露,可以认为是无懈可击的。但是虽然颁发证书容易,目前却没有很好的方案注销证书。

所以使用x509证书认证适用于Kubernetes内部组件之间认证,普通用户认证并不推荐通过证书的形式进行认证。

身份识别

上面说了,用户信息是隐藏在证书中的,我们来看一下,正常我们可以重kubectl的kubeconfig中获取用户的证书内容,一般是一个base64加密的内容

// admin-client-certificate.txt
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJZjJkVlJqbThFTFF3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T0RBMU1UUXdPREUzTVROYUZ3MHhPVEExTVRRd09ERTNNVGRhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXhCbjNqZHc4MGIxR2ZiNnMKdzJOcnFwTG90TVQ0bnlBZjJIaHFNclhqbk8rd25hSzFBSVRPdy8yMm1EajByd0l1SndkUUlqNS9CYUY2M3BQRQoxcFUwdmhJUFZLNG42Skk0ZG1Nem8vbFIzalpwR2VaVzF6ZFhhQ292dzljN2NsYmlIby9tRkc0eHF5dFZMZlg0Ci9TOG1GcDJBOVFjaWVKR0lvNVMwQlIzRlpsVTFQTTdEUmJMRFZWcTFQZHlOWTJHZnNiR3JIbEdnWHZXQUtDZC8KSDc5Z0FxVm9UWGpTSVdDVll1WWNvTHZkdlZYUVNJaVlscFhGUDFqQlFMdmNVN3ZycXRiMTJSbXJ4bnBrVzRwbApkR0VPWDJzTG1mWVo1VGlGcGtSd3oyR3hzbVd5UmJ0Nk91SVNKRkk2UlowcitSbjR5TURLUHJZbEVuZ0RWYzVLClBaNXptd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFWk5UdlR6Mk9nekNVZHZNRmJyaFBzcCttRDJ2UGpNUkN4aQozQmtBMTB2SUNPU2ZkeW1NbjhhdzBJYktZejJnUWJYcVVmcXpRbVFmYTNpZitRWUJrQis3N3pmc3Y5YW00RVAvCmU2VGc1MnRxVjJQN3MyZUY3dE5BZTIwR3lWNnlGbFExUVVXNS9NNE0rSk1sVitCVWJsOXlFeVFsRU51Y0tmK3UKVFB5S0tUVXR6dlVZcjVFM0VKa3Q4NEVRSU52dzJuUjJqTnZlWjFYV09saVVyS2ZqSEh0ZnZPL241NlVTdUk0dwp1MkxUbElDUmNqNGcrWldsSWplTUZrR3lQYkp5SkFRNjVQMnNHclptMWtsR0dIM216d081Q1AxeVpXdm9VampQCmp6U2pNQ0lhSy9mUjhlUkFKNnExdFQ2YkcyNkwrbmprS0NRRFdLcGpBV09hcHVST2Niaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=

然后针对该文件数据做base64解码,得到client certificate文件:

cat admin-client-certificate.txt | base64 -d > admin-client.crt

# cat admin-client.crt

-----BEGIN CERTIFICATE-----
MIIC8jCCAdqgAwIBAgIIf2dVRjm8ELQwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKa3ViZXJuZXRlczAeFw0xODA1MTQwODE3MTNaFw0xOTA1MTQwODE3MTdaMDQx
FzAVBgNVBAoTDnN5c3RlbTptYXN0ZXJzMRkwFwYDVQQDExBrdWJlcm5ldGVzLWFk
bWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxBn3jdw80b1Gfb6s
w2NrqpLotMT4nyAf2HhqMrXjnO+wnaK1AITOw/22mDj0rwIuJwdQIj5/BaF63pPE
1pU0vhIPVK4n6JI4dmMzo/lR3jZpGeZW1zdXaCovw9c7clbiHo/mFG4xqytVLfX4
/S8mFp2A9QcieJGIo5S0BR3FZlU1PM7DRbLDVVq1PdyNY2GfsbGrHlGgXvWAKCd/
H79gAqVoTXjSIWCVYuYcoLvdvVXQSIiYlpXFP1jBQLvcU7vrqtb12RmrxnpkW4pl
dGEOX2sLmfYZ5TiFpkRwz2GxsmWyRbt6OuISJFI6RZ0r+Rn4yMDKPrYlEngDVc5K
PZ5zmwIDAQABoycwJTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUH
AwIwDQYJKoZIhvcNAQELBQADggEBAEZNTvTz2OgzCUdvMFbrhPsp+mD2vPjMRCxi
3BkA10vICOSfdymMn8aw0IbKYz2gQbXqUfqzQmQfa3if+QYBkB+77zfsv9am4EP/
e6Tg52tqV2P7s2eF7tNAe20GyV6yFlQ1QUW5/M4M+JMlV+BUbl9yEyQlENucKf+u
TPyKKTUtzvUYr5E3EJkt84EQINvw2nR2jNveZ1XWOliUrKfjHHtfvO/n56USuI4w
u2LTlICRcj4g+ZWlIjeMFkGyPbJyJAQ65P2sGrZm1klGGH3mzwO5CP1yZWvoUjjP
jzSjMCIaK/fR8eRAJ6q1tT6bG26L+njkKCQDWKpjAWOapuROcbk=
-----END CERTIFICATE-----

查看证书内容:

# openssl x509 -in ./admin-client.crt -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 9180400125522743476 (0x7f67554639bc10b4)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes
        Validity
            Not Before: May 14 08:17:13 2018 GMT
            Not After : May 14 08:17:17 2019 GMT
        Subject: O=system:masters, CN=kubernetes-admin
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)

   ... ...

从证书输出的信息中,我们看到了下面这行:

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

k8s apiserver对kubectl的请求进行client certificate验证,验证通过后kube-apiserver会得到:group = system:masters的http上下文信息,并传给后续的authorizers。

2、bearer token

Service Account Token

service account是使用Bearer Token认证的,不过和前面的Token不一样的是service account是基于JWT(JSON Web Token)认证机制,JWT原理和x509证书认证其实有点类似,都是通过CA根证书进行签名和校验,只是格式不一样而已,JWT由三个部分组成,每个部分由.分割,三个部分依次如下:

  • Header(头部): Token的元数据,如alg表示签名算法,typ表示令牌类型,一般为JWT,kid表示Token ID等。
  • Payload(负载): 实际存放的用户凭证数据,如iss表示签发人,sub签发对象,exp过期时间等。
  • Signature(签名):基于alg指定的算法生成的数字签名,为了避免被篡改和伪造。

为了便于HTTP传输,JWT Token在传递过程中会转成Base64URL编码,其中Base64URL相对我们常用的Base64编码不同的是=被省略、+替换成-,/替换成_,这么做的原因是因为这些字符在URL里面有特殊含义。

可以用如下脚本实现Kubernetes Service Account的Token解码:

#!/bin/bash

# base64url解码
decode_base64_url() {
  LEN=$((${#1} % 4))
  RESULT="$1"
  if [ $LEN -eq 2 ]; then
    RESULT+='=='
  elif [ $LEN -eq 3 ]; then
    RESULT+='='
  fi
  echo "$RESULT" | tr '_-' '/+' | base64 -d
}

# 解码JWT
decode_jwt()
{
  JWT_RAW=$1
  for line in $(echo "$JWT_RAW" | awk -F '.' '{print $1,$2}'); do
    RESULT=$(decode_base64_url "$line")
    echo "$RESULT" | python -m json.tool
  done
}

# 获取k8s sa token
get_k8s_sa_token()
{
  NAME=$1
  TOKEN_NAME=$(kubectl get sa "$NAME" -o jsonpath='{.secrets[0].name}')
  kubectl get secret "${TOKEN_NAME}" -o jsonpath='{.data.token}' | base64 -d
}

main()
{
  NAME=$1
  if [[ -z $NAME ]]; then
    echo "Usage: $0 <secret_name>"
    exit 1
  fi
  TOKEN=$(get_k8s_sa_token "$NAME")
  decode_jwt "$TOKEN"
}

main "$@"

从解码的数据可见,JWT Token的颁发机构为kubernetes/serviceaccount,颁发对象为SA对应的虚拟用户system:serviceaccount:default:test,除此之外还存储着其他的SA信息,如SA name、namespace、uuid等。这里需要注意的是我们发现JWT Token中没有exp字段,即意味着只要这个SA存在,这个Token就是永久有效的。

通过如下方式配置kubeconfig:

TOKEN_NAME=$(kubectl get serviceaccounts ${SA_NAME} -n ${namespace} -o jsonpath={.secrets[0].name})
TOKEN=$(kubectl get secret "${TOKEN_NAME}" -n ${namespace} -o jsonpath={.data.token} | base64 -d)
kubectl config set-credentials "${USERNAME}" --token="$TOKEN"

为了验证test-sa,在刚刚创建的int32bit-rolebinding的subjects增加了ServiceAccount test-sa。和预期一样,test-sa能够读取pod列表但没有删除pod权限。

service account除了可以用于集群外认证外,其还有一个最大的特点是可以通过Pod.spec.serviceAccountName把token attach到Pod中。在 Pod的Spec 中指明 ServiceAccount ,service account本来是系统服务使用的账户,当我们创建service account时,它会自带一个secret,这个secret就是token。我们通过这个token就能来访问 ApiServer,访问系统资源。

此时Kubernetes会自动把SA的Token通过volume的形式挂载到/run/secrets/kubernetes.io/serviceaccount目录上,从而Pod可以读取token调用Kubernetes API.

到这里为止,service account可能是Kubernetes目前最完美的认证方案了,既能支持集群外的客户端认证,又支持集群内的Pod关联授权。

但事实上,service account并不是设计用来给普通user认证的,默认enabled,通常被 pod 所使用,是给集群内部服务使用的,目前虽然token是永久有效的。

Bootstrap Tokens

bootstrap token则是由Kubernetes动态生成的,通过Secret形式存储,并且具有一定的生命周期,一旦过期就会失效。不像静态token,只要启动参数不变,token就不会变化。

我们使用kubeadm会生成一个token:

# kubeadm token list
TOKEN                     TTL       EXPIRES                USAGES                   DESCRIPTION   EXTRA GROUPS
bpjp71.6ckt2g3o3hso3gn4   23h       2019-12-15T11:58:13Z   authentication,signing   <none>        system:bootstrappers:kubeadm:default-node-token

Token有两个部分组成,由.分割,前面部分为Token ID bpjp71,后面部分为Token Secret 6ckt2g3o3hso3gn4。Token默认TTL为一天,对应的group为system:bootstrappers:kubeadm:default-node-token,对应User为system:bootstrap:${Token ID}。

kubeadm创建一个Token会对应在Kubernetes的kube-system namespace创建一个secret,secret名为bootstrap-token-${TOKEN_ID},这里为bootstrap-token-bpjp71。

# kubectl get secret bootstrap-token-bpjp71 -n kube-system -o yaml --export
apiVersion: v1
data:
  auth-extra-groups: c3lzdGVtOmJvb3RzdHJhcHBlcnM6a3ViZWFkbTpkZWZhdWx0LW5vZGUtdG9rZW4=
  expiration: MjAxOS0xMi0xNVQxMTo1ODoxM1o=
  token-id: YnBqcDcx
  token-secret: NmNrdDJnM28zaHNvM2duNA==
  usage-bootstrap-authentication: dHJ1ZQ==
  usage-bootstrap-signing: dHJ1ZQ==
kind: Secret
metadata:
  name: bootstrap-token-bpjp71
type: bootstrap.kubernetes.io/token

此时可以通过如下命令生成config:

kubectl config set-credentials bootstrap \
  --user bootstrap \
  --token bpjp71.6ckt2g3o3hso3gn4

为了验证boostrap token,我们把用户添加到int32bit-role中,注意对应的虚拟User名。

# kubectl  describe  rolebindings int32bit-rolebinding
Name:         int32bit-rolebinding
Labels:       <none>
Annotations:  <none>
Role:
  Kind:  Role
  Name:  int32bit-role
Subjects:
  Kind   Name                     Namespace
  ----   ----                     ---------
  Group  int32bit
  User   system:bootstrap:bpjp71

这种token主要用于临时授权使用,比如kubeadm初始化集群时会生成一个bootstrap token,这个token具有创建certificatesigningrequests权限,从而新Node能够发起CSR请求,请求客户端证书。

Static Token

静态token认证和静态密码原理几乎完全一样,唯一不同的是静态token通过token-auth-file指定token文件,文件内容也是和静态密码一样,认证时头部格式为Authorization: Bearer ${Token},

因此其优点和缺点也和静态密码完全一样,这里不再赘述。

3、HTTP Basic Auth

静态密码是最简单的认证方式,只需要在api-server启动时指定使用的密码本路径即可:

--basic-auth-file=/etc/static_secret/passwd

Static Password File: 以参数 --basic-auth-file=<SOMEFILE> 指明 basic auth file 的位置。
这个 basic auth file 以 csv 文件的形式存在,里面至少包含三个信息:password、username、uid(user id)、group(一些k8s常用的系统角色),比如NoMoreSecret,int32bit-1,1000,"int32bit" ,此时定义了一个用户int32bit-1,静态密码为NoMoreSecret,所属Group为intt32bit。

使用http请求认证的时候,认证头部为Basic base64encode(${username}:${password})。

通过静态密码的唯一优势是简单,其缺点也是非常明显:

  • 静态密码是明文,非常不安全,还有可能被暴力破解。
  • 非常不灵活,增加或者删除用户,必须手动修改静态密码文件并重启所有的api-server服务。

这种方式在实际场景中很少被使用,不建议生产环境使用。

集成外部认证系统

Kubernetes最强大的功能是支持集成第三方Id Provider(IdP),主流的如AD、LADP以及OpenStack Keystone等,毕竟专业的人做专业的事。

总结

1、静态密码和静态token认证策略的优点是非常简单,缺点是非常不安全和不灵活,不推荐使用。

2、x509证书认证本身的安全性保障没有问题,最大的问题是不支持证书回收,意味着一旦证书颁发出去就很难在回收过来。这种认证策略适合集群内部组件之间的认证通信。

3、bootstrap token适合需要临时授权的场景,如集群初始化。

4、service account基于JWT认证,JWT包含的字段比较简单,没有有效期和aud字段,存在安全隐患,不适用于普通用户认证,适用于集群内的Pod向api-server认证,如kube-proxy和flannel需要调用api-server监控service和pod的状态变化。

5、OpenID Connect(oidc)以及webhook可集成企业已有的身份认证系统,如AD、LDAP,其特点是安全、灵活、功能全面,并且身份认证与Kubernetes集群解耦合,非常适用于普通用户的认证,推荐使用。

授权机制(Authorization)

当用户通过认证后,k8s 的授权机制将对用户的行为等进行授权检查。换句话说,就是对这个请求本身,是否对某资源、某 namespace、某操作有权限限制。

若要开启某种模式,需要在 APIServer 启动时,设置参数 –authorization-mode=RBAC。授权机制目前有 4 种模式:RBAC、ABAC、Node、Webhook。

比如我们在自定义CRD开发的时候,就需要使用到RBAC对新对象进行授权,才能使用。

1、RBAC

基于角色的权限访问控制,就是对某个用户赋予某个角色,而这个角色通常决定了对哪些资源拥有怎样的权限。

最基本的概念

  • Role:角色,它其实是一组规则,定义了一组对 Kubernetes API 对象的操作权限。
  • Subject:被作用者,既可以是“人”,也可以是“机器”,也可以是你在 Kubernetes 里定义的“用户”。
  • RoleBinding:定义了“被作用者”和“角色”的绑定关系。

而这三个概念,其实就是整个 RBAC 体系的核心所在。

首先需要用户,所以我们正常创建sa,然后给他授权

1、创建sa

创建一个 ServiceAccount 很简单,只需要指定其所在 namespace 和 name 即可。

apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: hdls
  name: hdls-sa

2、定义角色

RBAC 中最重要的概念就是 Role 和 RoleBinding。

  • Role 定义了一组对 Kubernetes API 对象的操作权限。
  • RoleBinding 则定义的是具体的 ServiceAccount 和 Role 的对应关系。

实例

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: hdls
  name: hdls-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
  • namespace: 在这里仅限于逻辑上的“隔离”,并不会提供任何实际的隔离或者多租户能力;
  • rules:定义的是权限规则,允许“被作用者”,对 hdls 下面的 Pod 对象,进行 GET 和 LIST 操作;
    • apiGroups:为 “” 代表 core API Group;
    • resources:指的是资源类型
    • verbs: 指的是具体的操作,当前 Kubernetes(v1.11)里能够对 API 对象进行的所有操作有 “get”, “list”, “watch”, “create”, “update”, “patch”, “delete”。

3、绑定角色

RoleBinding 则定义的是具体的 ServiceAccount 和 Role 的对应关系。

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: hdls-rolebinding
  namespace: hdls
subjects:
- kind: ServiceAccount
  name: hdls-sa
  apiGroup: ""
roleRef:
  kind: Role
  name: hdls-role
  apiGroup: ""
  • 这个 RoleBinding 对象里定义了一个 subjects 字段,即“被作用者”。它的类型是 ServiceAccount,就是上面创建的 sa。这个 subjects 还可以是 User 和 Group,User 是指 k8s 里的用户,而 Group 是指 ServiceAccounts。
  • roleRef 字段是用来直接通过名字,引用我们前面定义的 Role 对象(hdls-role),从而定义了 Subject 和 Role 之间的绑定关系。

Role 和 RoleBinding 对象都是 Namespaced 对象,它们只对自己的 Namespace 内的资源有效。

而某个 Role 需要对于非 Namespaced 对象(比如:Node),或者想要作用于所有的 Namespace 的时候,我们需要使用 ClusterRole 和 ClusterRoleBinding 去做授权。

这两个 API 对象的用法跟 Role 和 RoleBinding 完全一样。只不过,它们的定义里,没有了 Namespace 字段。

Kubernetes 已经内置了很多个为系统保留的 ClusterRole,它们的名字都以 system: 开头。一般来说,这些系统级别的 ClusterRole,是绑定给 Kubernetes 系统组件对应的 ServiceAccount 使用的。

Kubernetes 还提供了四个内置的 ClusterRole 来供用户直接使用:

  • cluster-admin:整个集群的最高权限。如果在 ClusterRoleBinding 中使用,意味着在这个集群中的所有 namespace 中的所有资源都拥有最高权限。
  • admin:管理员权限。如果在 RoleBinding 中使用,意味着在某个 namespace 中,对大部分资源拥有读写权限,包括创建 Role 和 RoleBinding 的权限,但没有对资源 quota 和 namespace 本身的写权限。
  • edit:写权限。在某个 namespace 中,拥有对大部分资源的读写权限,但没有对 Role 和 RoleBinding 的读写权限。
  • view:读权限。在某个 namespace 中,仅拥有对大部分资源的读权限,没有对 Role 和 RoleBinding 的读权限,也没有对 seccrets 的读权限。

2、ABAC

基于属性的权限访问控制。若要开启该模式,需要在 APIServer 启动时,开启 –authorization-policy-file=<SOME_FILENAME> 和 –authorization-mode=ABAC 两个参数。

json 对象的格式来定义权限

与 Yaml 文件一致,必须描述的属性有 apiVersion、kind、spec,而 spec 里描述了具体的用户、资源和行为。看个例子:

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

这就描述了用户 bob 只有在 namespace projectCaribou 下对 pod 的读权限。

3、Node

Node 授权机制是一种特殊的模式,是 kubelet 发起的请求授权。开启该模式,需要开启参数 –authorization-mode=Node。

通过启动 –enable-admission-plugins=…,NodeRestriction,…,来限制 kubelet 访问 node,endpoint、pod、service以及secret、configmap、PV 和 PVC 等相关的资源。

4、Webhook

Webhook 模式是一种 HTTP 回调模式,是一种通过 HTTP POST 方式实现的简单事件通知。该模式需要 APIServer 配置参数 –authorization-webhook-config-file=<SOME_FILENAME>,HTTP 配置文件的格式跟 kubeconfig 的格式类似。

准入控制(Admission Controllers)

在一个请求通过了认证机制和授权认证后,需要经过最后一层筛查,即准入控制。这个准入控制模块的代码通常在 APIServer 中,并被编译到二进制文件中被执行。这一层安全检查的意义在于,检查该请求是否达到系统的门槛,即是否满足系统的默认设置,并添加默认参数。

准入控制以插件的形式存在 开启的方式为:

kube-apiserver --enable-admission-plugins=NamespaceLifecycle,LimitRanger ...

关闭的方式为:

kube-apiserver --disable-admission-plugins=PodNodeSelector,AlwaysDeny ...

常用的准入控制插件有:

AlwaysAdmit:允许所有请求通过,被官方反对,因为没有实际意义;
AlwaysPullImages:将每个 pod 的 image pull policy 改为 always,在多租户的集群被使用;
AlwaysDeny:禁止所有请求通过,被官方反对,因为没有实际意义;
DefaultStorageClass:为每个 PersistentVolumeClaim 创建默认的 PV;
DefaultTolerationSeconds:如果 pod 对污点 node.kubernetes.io/not-ready:NoExecute 和 node.alpha.kubernetes.io/unreachable:NoExecute 没有容忍,为其创建默认的 5 分钟容忍 notready:NoExecute 和unreachable:NoExecute;
LimitRanger:确保每个请求都没有超过其 namespace 下的 LimitRange,如果在 Deployment 中使用了 LimitRange 对象,该准入控制插件必须开启;
NamespaceAutoProvision:检查请求中对应的 namespace 是否存在,若不存在自动创建;
NamespaceExists:检查请求中对应的 namespace 是否存在,若不存在拒绝该请求;
NamespaceLifecycle:保证被删除的 namespace 中不会创建新的资源;
NodeRestriction:不允许 kubelet 修改 Node 和 Pod 对象;
PodNodeSelector:通过读取 namespace 的注解和全局配置,来控制某 namespace 下哪些 label 选择器可被使用;
PodPreset:满足预先设置的标准的 pod 不允许被创建;
Priority:通过 priorityClassName 来决定优先级;
ResourceQuota:保证 namespace 下的资源配额;
ServiceAccount:保证 ServiceAccount 的自动创建,如果用到 ServiceAccount,建议开启;

dynamic Admission Controllers

TODO

使用模式

目前最常见的使用姿势

  • 某一个服务或业务方要接入进来使用
  • 给他们创建一个 SA,再在 rbac 中创建关联的 role(分配权限),把这个 SA 丢给他们

因为我们不太可能去做云服务平台,对外提供给个人用户,内部服务和业务团队,通过 SA 接入已经足够

这个 SA 创建后,只拥有读取 pod 的权限,也可以限制为只拥有某个 namespace 下的 pod 的权限,你要接入其他用户,再建一个 SA。

kubectl 可以通过 SA 配置访问到 apiserver,我们可以参考kubectl的方式去调用相关的API,这个其实就是云平台做的东西。

内部使用sa

创建一个 SA,只拥有读取 pod 的权限,然后通过 kubectl config 的配置去使用这个 SA 访问 apiserver

1、创建sa

MacBook-Pro:exercise chunyinjiang$ cat sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: jcy
  name: jcy-sa
MacBook-Pro:exercise chunyinjiang$ kubectl get sa -n jcy
NAME      SECRETS   AGE
default   1         39h
jcy-sa    1         39h

2、创建role

MacBook-Pro:exercise chunyinjiang$ cat role.yaml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
    namespace: jcy
    name: jcy-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
MacBook-Pro:exercise chunyinjiang$ kubectl get role -n jcy
NAME       CREATED AT
jcy-role   2020-06-10T11:29:37Z

3、创建rolebinding

MacBook-Pro:exercise chunyinjiang$ cat rolebinding.yaml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: jcy-rolebinding
  namespace: jcy
subjects:
- kind: ServiceAccount
  name: jcy-sa
  apiGroup: ""
roleRef:
  kind: Role
  name: jcy-role
  apiGroup: ""

MacBook-Pro:exercise chunyinjiang$ kubectl get rolebinding -n jcy
NAME              ROLE            AGE
jcy-rolebinding   Role/jcy-role   38h

4、kubeconfig配置sa

TOKEN_NAME=$(kubectl get serviceaccounts jcy-sa -o jsonpath={.secrets[0].name})
MacBook-Pro:exercise chunyinjiang$ echo $TOKEN_NAME
jcy-sa-token-f5lsr

TOKEN=$(kubectl get secret "${TOKEN_NAME}" -n jcy -o jsonpath={.data.token} | base64 -d)
MacBook-Pro:exercise chunyinjiang$ echo $TOKEN
eyJhbGciOiJSUzI1NiIsImtpZCI6Ilgwa2JRbE5kbFU1UXhRSkZBa0lQX1l4V0VrVkZZbTJVX3hFRS1CbnlRejQifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJqY3kiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiamN5LXNhLXRva2VuLWY1bHNyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImpjeS1zYSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImFkMjg0NzAxLTFiMWUtNGFkOC1hZWE3LTI5YzgxNDljNDJmOCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpqY3k6amN5LXNhIn0.DeHgbaW5nDDakGxdV448_BOwrSI_Ez2mI8Ii-aztcZMVYJvo2_tWafAWAGm8s3VROcRxgPH8YroCKnllQBL_odJw55ZWhow_KBfOdAh3yKYbF9rNy2BLCV_n_T_qJdSm6M1eXD4_49dhPr3WqEhB5oDqZCXVpa7D0WExtorkiSctYmzfTFcRz2O8fnGExnhtz05p3HnqjyqpbiZQnP1qx6jJd1zlu4T3zgmmx4BLsqSHNVBhsW4Zhz-RmpO91vYzWQDYK6Mp9Yo97rtZofRy2J-6PJ8eI3RslsueWI_JBRPj8MAV-BwolOwXCgoH5ewGyPUveBPVF7Hqok-e_WaNpQ


kubectl config set-credentials system:serviceaccount:jcy:jcy-sa --token="$TOKEN"
User "system:serviceaccount:jcy:jcy-sa" set.

kubectl config set-context jcy-context --cluster=minikube --namespace=jcy --user=system:serviceaccount:jcy:jcy-sa

kubectl config use-context jcy-context

5、验证是否生效,以及是否只有读 pod 的权限

MacBook-Pro:exercise chunyinjiang$ kubectl get pods
No resources found in jcy namespace.
MacBook-Pro:exercise chunyinjiang$ kubectl get pods -n default
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:jcy:jcy-sa" cannot list resource "pods" in API group "" in the namespace "default"
MacBook-Pro:exercise chunyinjiang$ kubectl create ns jay
Error from server (Forbidden): namespaces is forbidden: User "system:serviceaccount:jcy:jcy-sa" cannot create resource "namespaces" in API group "" at the cluster scope
MacBook-Pro:exercise chunyinjiang$ kubectl get sa -n jcy
Error from server (Forbidden): serviceaccounts is forbidden: User "system:serviceaccount:jcy:jcy-sa" cannot list resource "serviceaccounts" in API group "" in the namespace "jcy"

多租户的实现

多租户也是基于sa+rbac的基础上实现的,首先用户信息需要单独的系统进行存储,这个可以接入外部的已经成熟的系统,也可以自己开发,然后每一个真实的用户对应一个sa,这个时候sa就是正在的用户,每个sa在rbac的基础对namespace角色操作进行关联,完成对应用户的权限管理。