所谓 mTLS( Mutual TLS)是指在通信时除了客户端会验证服务端证书确认其是否合法之外,服务端也会验证客户端的证书是否合法以确认客户端的合法性。
在常见的浏览器访问网站的场景下,通过 HTTPS 协议,浏览器会对服务器进行身份验证。但在微服务架构中,服务之间的访问通常需要各自进行身份认证,比如常见的 HTTP Basic、JWT 方式认证,这些基本都是应用级别的处理,需要在程序内部做处理。
使用 mTLS 有如下优点:
可以同时满足加密传输和身份认证
独立于应用之外,与具体语言无关
当然 mTLS 也有不足:
证书管理过于复杂。假设有一对客户端与服务端通信,如果使用自签名证书我们需要 ca 私钥证书、服务端私钥证书、客户端私钥证书共 6 个文件需要进行管理。如果服务变多证书数量也会随之增多,从而增加管理成本
证书更新时需要重启应用
Kubernetes 各个组件之间的通信采用的是 mTLS 认证方式,服务端与客户端都需要各自进行身份认证。Kubernetes 中各个组件通信情况如图:
Server 端包括
Client 端证书包括:
Kubernetes 使用的是自签名证书,以我们使用 kubeadm 为例,在部署时 kubeadm 会自动帮我们生成证书,Kubernetes 用到的证书大致如下:
# ubuntu @ VM-0-7-ubuntu in /etc/kubernetes/pki
$ ll
total 60K
-rw-r--r-- 1 root root 1.2K Nov 12 18:30 apiserver-etcd-client.crt
-rw------- 1 root root 1.7K Nov 12 18:30 apiserver-etcd-client.key
-rw-r--r-- 1 root root 1.2K Nov 12 18:30 apiserver-kubelet-client.crt
-rw------- 1 root root 1.7K Nov 12 18:30 apiserver-kubelet-client.key
-rw-r--r-- 1 root root 1.3K Nov 12 18:30 apiserver.crt
-rw------- 1 root root 1.7K Nov 12 18:30 apiserver.key
-rw-r--r-- 1 root root 1.1K Nov 12 18:30 ca.crt
-rw------- 1 root root 1.7K Nov 12 18:30 ca.key
drwxr-xr-x 2 root root 4.0K Nov 12 18:30 etcd
-rw-r--r-- 1 root root 1.1K Nov 12 18:30 front-proxy-ca.crt
-rw------- 1 root root 1.7K Nov 12 18:30 front-proxy-ca.key
-rw-r--r-- 1 root root 1.1K Nov 12 18:30 front-proxy-client.crt
-rw------- 1 root root 1.7K Nov 12 18:30 front-proxy-client.key
-rw------- 1 root root 1.7K Nov 12 18:30 sa.key
-rw------- 1 root root 451 Nov 12 18:30 sa.pub
# ubuntu @ VM-0-7-ubuntu in /etc/kubernetes/pki/etcd
$ ll
total 32K
-rw-r--r-- 1 root root 1.1K Nov 12 18:30 ca.crt
-rw------- 1 root root 1.7K Nov 12 18:30 ca.key
-rw-r--r-- 1 root root 1.2K Nov 12 18:30 healthcheck-client.crt
-rw------- 1 root root 1.7K Nov 12 18:30 healthcheck-client.key
-rw-r--r-- 1 root root 1.2K Nov 12 18:30 peer.crt
-rw------- 1 root root 1.7K Nov 12 18:30 peer.key
-rw-r--r-- 1 root root 1.2K Nov 12 18:30 server.crt
-rw------- 1 root root 1.7K Nov 12 18:30 server.key
$ sudo ls -l /var/lib/kubelet/pki
total 12
kubelet-client-2021-11-12-18-30-55.pem
-rw-r--r-- 1 root root 2287 Nov 12 18:30 kubelet.crt
-rw------- 1 root root 1679 Nov 12 18:30 kubelet.key
Kubeadm 生成的 ca 证书有效期为 10 年,其他证书为 1 年,可以通过 sudo kubeadm certs check-expiration
命令查看证书的过期情况。可以通过自动脚本定时更新或者修改 kubeadm 源码将证书有效期延长。
$ sudo kubeadm certs check-expiration
[check-expiration] Reading configuration from the cluster...
[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
admin.conf Nov 12, 2022 10:30 UTC 354d
apiserver Nov 12, 2022 10:30 UTC 354d ca noapiserver-etcd-client Nov 12, 2022 10:30 UTC 354d etcd-ca
apiserver-kubelet-client Nov 12, 2022 10:30 UTC 354d ca
controller-manager.conf Nov 12, 2022 10:30 UTC 354d
etcd-healthcheck-client Nov 12, 2022 10:30 UTC 354d etcd-ca
etcd-peer Nov 12, 2022 10:30 UTC 354d etcd-ca
etcd-server Nov 12, 2022 10:30 UTC 354d etcd-ca
front-proxy-client Nov 12, 2022 10:30 UTC 354d
scheduler.conf Nov 12, 2022 10:30 UTC 354d
CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
ca Nov 10, 2031 10:30 UTC 9y no
etcd-ca Nov 10, 2031 10:30 UTC 9y no
front-proxy-ca Nov 10, 2031 10:30 UTC 9y no
apiserver 收到外部请求时,首先要需要经过一系列的验证后才能确认是否允许请求继续执行。主要有三步:
对于身份验证,Kubernetes 中身份信息可以分为两类:
ServiceAccount 是 Kubernetes 内部通信使用的账户信息,每个 namesapce 下都会有一个名为 default
的默认 Service Account,在没有单独设置时 Pod 默认使用该 ServiceAccout 作为服务账户。Service Account 会生成对应的 Secret 存储其 token。
$ kubectl get serviceaccounts
NAME SECRETS AGE
default 1 10d
$ kubectl get secrets
NAME TYPE DATA AGE
default-token-c7bv9 kubernetes.io/service-account-token 3 10d
$ kubectl describe secrets default-token-c7bv9
Name: default-token-c7bv9
Namespace: default
Labels: <none>
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 517fd6b1-6441-4817-bf16-14ef37175da2
Type: kubernetes.io/service-account-token
Data
====
ca.crt: 1099 bytes
namespace: 7 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLMkVMNm5mMkFhYmQyMUdCVXp3OGdiZEs1dkdRQ3NNR0JWT0RZblIzYkkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tYzdidjkiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjUxN2ZkNmIxLTY0NDEtNDgxNy1iZjE2LTE0ZWYzNzE3NWRhMiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.C2gGaqrF1effQy_9e48VGh06Ks1ihwR3Q6gHezBBZ51WmD2Sg4Pt0WASZEpJ8swPLXUCo13UaL_y2b3dXOwcjWDOApFsPttDZQtfjiIDn_Wt0RMCKTUNr9ft8_GcM2Xjt8Bnz_mev-NZFwBBJC1vhJn2u-XQLfsp0XiHVTsls0JlPdtZjBOAvlxTQtM9LbMb2o5flEXLCHEGiKNkrYczS7SDNFfrOUNcdDbJHUhifAynOm0bSFIWTG9R0CYHvM3oTJyLSLHuSjqZjpMfNev_4V27AWfSTWg1rwC3Bhj3FNzQSEriQCg1rt9t-Bq58AbJR4vrj2dQa6vT5FP6xQVULA
CA_CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
Pod 会将 Secret 作为卷挂在进来,Service Account 的 Secrets 的 token 文件会被 mount 到 pod 里的下面这个位置。 在 pod 里可以使他们来访问 API,为便于操作可以先在 Pod 里设置几个环境变量。
$ ls -l /var/run/secrets/kubernetes.io/serviceaccount/
lrwxrwxrwx 1 root root 13 Aug 31 03:24 ca.crt -> ..data/ca.crt
lrwxrwxrwx 1 root root 16 Aug 31 03:24 namespace -> ..data/namespace
lrwxrwxrwx 1 root root 12 Aug 31 03:24 token -> ..data/token
我们也可以直接使用该 token 与 apiserver 通信。
$ curl -k https://172.19.0.7:6443/api
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/api\"",
"reason": "Forbidden",
"details": {
},
"code": 403
}
$ curl -k https://172.19.0.7:6443/api -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLMkVMNm5mMkFhYmQyMUdCVXp3OGdiZEs1dkdRQ3NNR0JWT0RZblIzYkkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tYzdidjkiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjUxN2ZkNmIxLTY0NDEtNDgxNy1iZjE2LTE0ZWYzNzE3NWRhMiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.C2gGaqrF1effQy_9e48VGh06Ks1ihwR3Q6gHezBBZ51WmD2Sg4Pt0WASZEpJ8swPLXUCo13UaL_y2b3dXOwcjWDOApFsPttDZQtfjiIDn_Wt0RMCKTUNr9ft8_GcM2Xjt8Bnz_mev-NZFwBBJC1vhJn2u-XQLfsp0XiHVTsls0JlPdtZjBOAvlxTQtM9LbMb2o5flEXLCHEGiKNkrYczS7SDNFfrOUNcdDbJHUhifAynOm0bSFIWTG9R0CYHvM3oTJyLSLHuSjqZjpMfNev_4V27AWfSTWg1rwC3Bhj3FNzQSEriQCg1rt9t-Bq58AbJR4vrj2dQa6vT5FP6xQVULA"
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "172.19.0.7:6443"
}
]
}
除了每个 namespace 默认的服务账户外,我们可以自己创建 Service Account。
$ kubectl create serviceaccount build-robot
serviceaccount/build-robot created
# ubuntu @ VM-0-7-ubuntu in ~ [9:45:08]
$ kubectl get serviceaccounts/build-robot -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: "2021-11-23T01:45:08Z"
name: build-robot
namespace: default
resourceVersion: "881093"
uid: 0160e851-cae1-4927-a524-58c3a379ee05
secrets:
- name: build-robot-token-d9p58
创建完成后我们可以在创建 Pod 时通过修改 spec.serviceAccountName 属性来指定 ServiceAccount。通过自己创建 ServiceAccount 并结合 RBAC 授权可以针对特殊的 Pod 进行单独的权限控制。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: vault-token
serviceAccountName: build-robot
Kubernetes 虽然有用户的概念,但它认为用户是由集群无关的服务进行管理的,因此并没有提供类似 kubectl create user 的 API 来创建用户。
当外部访问 apiserver 时,Kubernetes 可以使用的认证方式有:
这里比较常用的方式是客户端证书,其他方式使用可以参考这份索引 文档。
Kubernetes 内置了 Certificate Signing Request
对象来执行证书签名。当外部用户想请求 Kubernetes 时,可以使用私钥来生成 CSR 证书签名请求,然后交给 Kuberetes 签发。流程如下
# 创建私钥
$ openssl genrsa -out Jane.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
.............................+++++
.........................................................+++++
e is 65537 (0x010001)
# 生成 CSR
$ openssl req -new -key Jane.key -out Jane.csr
...
Common Name (e.g. server FQDN or YOUR name) []:Jane
Email Address []:
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
$ cat Jane.csr | base64 | tr -d "\n"
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ21UQ0NBWUVDQVFBd1ZERUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeApJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpERU5NQXNHQTFVRUF3d0VTbUZ1ClpUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5tb3dZUkdpbHlWSkVIbkxaUU0KSFEvQWRveG9CNmJUN2YvSjFuc2xBYXZEYm9Sc3BKdjBBcGh6a05RYXJDU1E1SDRYVjR2OGZDdDVmeGFyL294agpmUXVyWDNrbXk1SHpJTGFod0svWXUvWU01djhacG53S3J3RmpmTzVpVC9rRmhyOUF0VkhWL0ZMajBhZURzUHRaCjlaemduUXUwbUUxcmc5WWZBUVFxOHo5UjB5bGFxQ0V2SU9HVU5FRzBrNGN2K0lDNE96KzZjQmIyUGhLLzFKc3kKcUg3V3RONnIraDI0S0FveXExZDFSY1NIU0ppbVgwbkExNlFCYjRuRVFGc0xJaUtXSXRxQ1JXUm9WT2dqSDhMUQpOV1ZlQmRKRVh6MWxIYXhzVE56OEo0QVhUZGFTLzcwSDhMRXhCT3ppMXNXMFB1aldPRC8xRkVhc2dqY1NNUG9uCnVua0NBd0VBQWFBQU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ2Z3ck5menl1blFtaUVBaXpxdzN6VGh0UkIKdjZtSmZVL2tNZTN1eHVDZm1MR3Y4OXpvZ3k3SWQxM25pdTE5Zzgzdy82UktkRFI1QVhXODk0L3daQi9CQjQwcgoyU3pQcmk5L3hMUkFManZnbWY0d1NhaDIyUnRJUjJYZGNVelJZL2V4V2w4ajJVV0w5Mkwxci82bWNjSVdrT1BUCnNmUWVYWWYxZThnNVk5Q1VSbThTNlloeURQOXFCQzk3QjEwenovNU1SN0YxdmtxMTI2OEtEek9GbWtWSnJLMkEKaXFNQk4xdkRSVkJURFVISFJ4V1lwTUpTSHJrOFRlVHhLVE1RNG12WGxCMzNUbElIZlU4L1ZOMEtQNFU5d0k5egpLNEI1NWFFc3QxUW5TWkw3cDlxMisvb20rdmtZUC9RblU4M3I4RlBFVTJ4R2ZzSTR0WFpzNkdseGZyVzMKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==%
CSR 生成后我们通过 base64 编码拿到其内容,并创建 Kubernetes 的 CSR 对象:
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: Jane
spec:
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ21UQ0NBWUVDQVFBd1ZERUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeApJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpERU5NQXNHQTFVRUF3d0VTbUZ1ClpUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5tb3dZUkdpbHlWSkVIbkxaUU0KSFEvQWRveG9CNmJUN2YvSjFuc2xBYXZEYm9Sc3BKdjBBcGh6a05RYXJDU1E1SDRYVjR2OGZDdDVmeGFyL294agpmUXVyWDNrbXk1SHpJTGFod0svWXUvWU01djhacG53S3J3RmpmTzVpVC9rRmhyOUF0VkhWL0ZMajBhZURzUHRaCjlaemduUXUwbUUxcmc5WWZBUVFxOHo5UjB5bGFxQ0V2SU9HVU5FRzBrNGN2K0lDNE96KzZjQmIyUGhLLzFKc3kKcUg3V3RONnIraDI0S0FveXExZDFSY1NIU0ppbVgwbkExNlFCYjRuRVFGc0xJaUtXSXRxQ1JXUm9WT2dqSDhMUQpOV1ZlQmRKRVh6MWxIYXhzVE56OEo0QVhUZGFTLzcwSDhMRXhCT3ppMXNXMFB1aldPRC8xRkVhc2dqY1NNUG9uCnVua0NBd0VBQWFBQU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ2Z3ck5menl1blFtaUVBaXpxdzN6VGh0UkIKdjZtSmZVL2tNZTN1eHVDZm1MR3Y4OXpvZ3k3SWQxM25pdTE5Zzgzdy82UktkRFI1QVhXODk0L3daQi9CQjQwcgoyU3pQcmk5L3hMUkFManZnbWY0d1NhaDIyUnRJUjJYZGNVelJZL2V4V2w4ajJVV0w5Mkwxci82bWNjSVdrT1BUCnNmUWVYWWYxZThnNVk5Q1VSbThTNlloeURQOXFCQzk3QjEwenovNU1SN0YxdmtxMTI2OEtEek9GbWtWSnJLMkEKaXFNQk4xdkRSVkJURFVISFJ4V1lwTUpTSHJrOFRlVHhLVE1RNG12WGxCMzNUbElIZlU4L1ZOMEtQNFU5d0k5egpLNEI1NWFFc3QxUW5TWkw3cDlxMisvb20rdmtZUC9RblU4M3I4RlBFVTJ4R2ZzSTR0WFpzNkdseGZyVzMKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400 # one day
usages:
- client auth
$ kubectl apply -f k8s-csr-jane.yaml
certificatesigningrequest.certificates.k8s.io/Jane created
新创建的 CSR 处于 Pending 状态,需要批准后才能使用。
$ kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
Jane 6s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Pending
$ kubectl certificate approve Jane
certificatesigningrequest.certificates.k8s.io/Jane approved
$ kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
Jane 4m18s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Approved,Issued
批准后 CSR 中就有了证书信息:
$ kubectl get csr Jane -o yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"certificates.k8s.io/v1","kind":"CertificateSigningRequest","metadata":{"annotations":{},"name":"Jane"},"spec":{"expirationSeconds":86400,"request":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ21UQ0NBWUVDQVFBd1ZERUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeApJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpERU5NQXNHQTFVRUF3d0VTbUZ1ClpUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5tb3dZUkdpbHlWSkVIbkxaUU0KSFEvQWRveG9CNmJUN2YvSjFuc2xBYXZEYm9Sc3BKdjBBcGh6a05RYXJDU1E1SDRYVjR2OGZDdDVmeGFyL294agpmUXVyWDNrbXk1SHpJTGFod0svWXUvWU01djhacG53S3J3RmpmTzVpVC9rRmhyOUF0VkhWL0ZMajBhZURzUHRaCjlaemduUXUwbUUxcmc5WWZBUVFxOHo5UjB5bGFxQ0V2SU9HVU5FRzBrNGN2K0lDNE96KzZjQmIyUGhLLzFKc3kKcUg3V3RONnIraDI0S0FveXExZDFSY1NIU0ppbVgwbkExNlFCYjRuRVFGc0xJaUtXSXRxQ1JXUm9WT2dqSDhMUQpOV1ZlQmRKRVh6MWxIYXhzVE56OEo0QVhUZGFTLzcwSDhMRXhCT3ppMXNXMFB1aldPRC8xRkVhc2dqY1NNUG9uCnVua0NBd0VBQWFBQU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ2Z3ck5menl1blFtaUVBaXpxdzN6VGh0UkIKdjZtSmZVL2tNZTN1eHVDZm1MR3Y4OXpvZ3k3SWQxM25pdTE5Zzgzdy82UktkRFI1QVhXODk0L3daQi9CQjQwcgoyU3pQcmk5L3hMUkFManZnbWY0d1NhaDIyUnRJUjJYZGNVelJZL2V4V2w4ajJVV0w5Mkwxci82bWNjSVdrT1BUCnNmUWVYWWYxZThnNVk5Q1VSbThTNlloeURQOXFCQzk3QjEwenovNU1SN0YxdmtxMTI2OEtEek9GbWtWSnJLMkEKaXFNQk4xdkRSVkJURFVISFJ4V1lwTUpTSHJrOFRlVHhLVE1RNG12WGxCMzNUbElIZlU4L1ZOMEtQNFU5d0k5egpLNEI1NWFFc3QxUW5TWkw3cDlxMisvb20rdmtZUC9RblU4M3I4RlBFVTJ4R2ZzSTR0WFpzNkdseGZyVzMKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==","signerName":"kubernetes.io/kube-apiserver-client","usages":["client auth"]}}
creationTimestamp: "2021-11-24T22:47:57Z"
name: Jane
resourceVersion: "1127112"
uid: 6f0b5433-e1d0-4f89-bbf1-a14fc1d0ad55
spec:
expirationSeconds: 86400
groups:
- system:masters
- system:authenticated
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ21UQ0NBWUVDQVFBd1ZERUxNQWtHQTFVRUJoTUNRVlV4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeApJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpERU5NQXNHQTFVRUF3d0VTbUZ1ClpUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5tb3dZUkdpbHlWSkVIbkxaUU0KSFEvQWRveG9CNmJUN2YvSjFuc2xBYXZEYm9Sc3BKdjBBcGh6a05RYXJDU1E1SDRYVjR2OGZDdDVmeGFyL294agpmUXVyWDNrbXk1SHpJTGFod0svWXUvWU01djhacG53S3J3RmpmTzVpVC9rRmhyOUF0VkhWL0ZMajBhZURzUHRaCjlaemduUXUwbUUxcmc5WWZBUVFxOHo5UjB5bGFxQ0V2SU9HVU5FRzBrNGN2K0lDNE96KzZjQmIyUGhLLzFKc3kKcUg3V3RONnIraDI0S0FveXExZDFSY1NIU0ppbVgwbkExNlFCYjRuRVFGc0xJaUtXSXRxQ1JXUm9WT2dqSDhMUQpOV1ZlQmRKRVh6MWxIYXhzVE56OEo0QVhUZGFTLzcwSDhMRXhCT3ppMXNXMFB1aldPRC8xRkVhc2dqY1NNUG9uCnVua0NBd0VBQWFBQU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ2Z3ck5menl1blFtaUVBaXpxdzN6VGh0UkIKdjZtSmZVL2tNZTN1eHVDZm1MR3Y4OXpvZ3k3SWQxM25pdTE5Zzgzdy82UktkRFI1QVhXODk0L3daQi9CQjQwcgoyU3pQcmk5L3hMUkFManZnbWY0d1NhaDIyUnRJUjJYZGNVelJZL2V4V2w4ajJVV0w5Mkwxci82bWNjSVdrT1BUCnNmUWVYWWYxZThnNVk5Q1VSbThTNlloeURQOXFCQzk3QjEwenovNU1SN0YxdmtxMTI2OEtEek9GbWtWSnJLMkEKaXFNQk4xdkRSVkJURFVISFJ4V1lwTUpTSHJrOFRlVHhLVE1RNG12WGxCMzNUbElIZlU4L1ZOMEtQNFU5d0k5egpLNEI1NWFFc3QxUW5TWkw3cDlxMisvb20rdmtZUC9RblU4M3I4RlBFVTJ4R2ZzSTR0WFpzNkdseGZyVzMKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==
signerName: kubernetes.io/kube-apiserver-client
usages:
- client auth
username: kubernetes-admin
status:
certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURPakNDQWlLZ0F3SUJBZ0lSQU9MQzB0MXpQVUdIdkU0Z3ZJRUova0l3RFFZSktvWklodmNOQVFFTEJRQXcKRlRFVE1CRUdBMVVFQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TVRFeE1qUXlNalEzTVRKYUZ3MHlNVEV4TWpVeQpNalEzTVRKYU1GUXhDekFKQmdOVkJBWVRBa0ZWTVJNd0VRWURWUVFJRXdwVGIyMWxMVk4wWVhSbE1TRXdId1lEClZRUUtFeGhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXhEVEFMQmdOVkJBTVRCRXBoYm1Vd2dnRWkKTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEWnFNR0VSb3BjbFNSQjV5MlVEQjBQd0hhTQphQWVtMCszL3lkWjdKUUdydzI2RWJLU2I5QUtZYzVEVUdxd2trT1IrRjFlTC9Id3JlWDhXcS82TVkzMExxMTk1CkpzdVI4eUMyb2NDdjJMdjJET2IvR2FaOENxOEJZM3p1WWsvNUJZYS9RTFZSMWZ4UzQ5R25nN0Q3V2ZXYzRKMEwKdEpoTmE0UFdId0VFS3ZNL1VkTXBXcWdoTHlEaGxEUkJ0Sk9ITC9pQXVEcy91bkFXOWo0U3Y5U2JNcWgrMXJUZQpxL29kdUNnS01xdFhkVVhFaDBpWXBsOUp3TmVrQVcrSnhFQmJDeUlpbGlMYWdrVmthRlRvSXgvQzBEVmxYZ1hTClJGODlaUjJzYkV6Yy9DZUFGMDNXa3YrOUIvQ3hNUVRzNHRiRnREN28xamcvOVJSR3JJSTNFakQ2SjdwNUFnTUIKQUFHalJqQkVNQk1HQTFVZEpRUU1NQW9HQ0NzR0FRVUZCd01DTUF3R0ExVWRFd0VCL3dRQ01BQXdId1lEVlIwagpCQmd3Rm9BVTdMQXF5b3RONUltOFBQZnFlTEgwVmMvcjdNb3dEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRjljCnIyQWVuYVl1UDBvcmlLZTU5QjgwaWk2WUErbGdBelowU2lwdnhTYzlQbDBsZ3NuN01ibGtQdkc3MGM4S3UyRVIKRXl5WE9WcjFjcVhjSE1DNDk3b0hHQUM5L2ZDcitUc3lLT2x4L1A5TWxCOTRZdC9ZMStvd2drUndzajFnSnVHTQorbENlbUcyKy9yZCtWbTlHeEh0c3pxODZHa0tDNHVzV0dKMmhkZGVDYVV2OEdjZk9KMCtUT1orM3ZwNExIWmZ2CmFuakNEK2R2RzIxVW5SUXM2eUQyZUNDVG0ydVhVQkdNSnlkajFEUlNERis5b0ZRS2hvZVVPMnhUaWFpdFZEeTgKNy9RODZVb2hMS1lGQUdWTmFSazRDdDh0N2hMeFFVaU9USUcxajNUbi96T0lZd256aURCV0dhZ1RKOVNMbGk3RQovcGRlTWFReEFiMHdBMnkvbnlzPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
conditions:
- lastTransitionTime: "2021-11-24T22:52:12Z"
lastUpdateTime: "2021-11-24T22:52:12Z"
message: This CSR was approved by kubectl certificate approve.
reason: KubectlApprove
status: "True"
type: Approved
我们可以通过 base64 将 CSR 中的证书解码导出然后在 作为登陆凭证配合在 kubeconfig 或者 RBAC 授权中。
$ kubectl get csr Jane -o jsonpath='{.status.certificate}' | base64 -d > Jane.crt
有了用户信息后,我们可以通过 curl 或者 kubectl 访问集群了,我们可以在发起请求时配置证书或者在请求头中设置 token 进行验证。
curl -k https://172.19.0.7:6443/api -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLMkVMNm5mMkFhYmQyMUdCVXp3OGdiZEs1dkdRQ3NNR0JWT0RZblIzYkkifQ..."
$ kubectl get pods --server https://172.19.0.7:6443 \
> --client-key admin.key \
> --client-certificate admin.crt \
> --certificate-authority ca.crt
但每次都这样访问的话非常不方便,尤其是需要用不同身份访问不同集群时,Kubernetes 提供了 kubeconfig 配置文件让我们可以更方便的配置对集群的访问。
$ kubectl get pods --kubeconfig /etc/kubernetes/admin.conf
NAME READY STATUS RESTARTS AGE
php-apache-d4cf67d68-xsbbt 1/1 Running 2 (44h ago) 7d15h
默认的 kubeconfig 配置文件位于 ~/.kube/config ,一般安装完成后我们会将 /etc/kubernetes/admin.conf 文件复制过来,这样我们就可以通过 kubectl 直接访问集群了。
可以通过查看文件或者 kubectl config view 命令查看
$ kubectl config view
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://172.19.0.7:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
Kubeconfig 文件可以分为三部分:
通过添加不同的集群和用户,并设置不同的上下文,我们就可以在同一个终端对不同的集群进行访问。
除了直接修改文件外我们可以通过 kubectl config 命令来动态操作 kubeconfig 配置。
$ kubectl config
Modify kubeconfig files using subcommands like "kubectl config set current-context my-context"
The loading order follows these rules:
1. If the --kubeconfig flag is set, then only that file is loaded. The flag may only be set once and no merging takes
place.
2. If $KUBECONFIG environment variable is set, then it is used as a list of paths (normal path delimiting rules for
your system). These paths are merged. When a value is modified, it is modified in the file that defines the stanza. When
a value is created, it is created in the first file that exists. If no files in the chain exist, then it creates the
last file in the list.
3. Otherwise, ${HOME}/.kube/config is used and no merging takes place.
Available Commands:
current-context Display the current-context
delete-cluster Delete the specified cluster from the kubeconfig
delete-context Delete the specified context from the kubeconfig
delete-user Delete the specified user from the kubeconfig
get-clusters Display clusters defined in the kubeconfig
get-contexts Describe one or many contexts
get-users Display users defined in the kubeconfig
rename-context Rename a context from the kubeconfig file
set Set an individual value in a kubeconfig file
set-cluster Set a cluster entry in kubeconfig
set-context Set a context entry in kubeconfig
set-credentials Set a user entry in kubeconfig
unset Unset an individual value in a kubeconfig file
use-context Set the current-context in a kubeconfig file
view Display merged kubeconfig settings or a specified kubeconfig file
Usage:
kubectl config SUBCOMMAND [options]
比如我每次查看 order 命名空间下的资源对象都需要加 -n 指定 namespace 比较麻烦,我可以新加一个 context 使得每次默认访问 easemesh 命名空间下的对象。
1. 新建 context
$ kubectl config set-context order-checker --cluster=kubernetes --user=kubernetes-admin --namespace=order
Context "order-checker" created.
$ kubectl config current-context
kubernetes-admin@kubernetes
2. 切换 context
$ kubectl config use-context order-checker
Switched to context "order-checker".
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
order-control-plane-0 1/1 Running 0 24h
order-operator-6754847bb7-fcglb 2/2 Running 4 (44h ago) 7d15h
$ kubectl config current-context
order-checker
在 CKA/CKAD 考试中一般会提供若干个集群供我们操作,会频繁的用到 kubectl config use-context 来切换上下文。
有了身份认证后还需要权限认证来确认请求者是否有权限执行操作:
认证方式 | 实现方式 | 使用方式 |
---|---|---|
Node 授权 | apiserver 内置 | 内部使用(kubelet) |
ABAC | 静态文件 | 已弃用 |
RBAC | Kuberetes 对象 | 用户/管理员授权 |
WebHook | 外部服务 | |
Always Dency/Always Allow | apiserver 内置 | 测试时使用 |
基于角色的访问控制(Role-Based Access Control, 即”RBAC”)使用rbac.authorization.k8s.io
API Group 实现授权决策,允许管理员通过 Kubernetes API 动态配置策略。要启用 RBAC,需使用 --authorization-mode=RBAC
启动 API Server。
Kubernetes 中 RBAC 的核心是通过 Role/ClusterRole、RoleBinding/ClusterRoleBind 来完成授权策略的定义:
Role 代表对某个单一命名空间下的访问权限,而如果相对整个集群内的某些资源拥有权限,则需要通过 ClusterRole 实现。
以下是在 ”default” 命名空间中一个 Role 对象的定义,用于授予对 pod 的读访问权限:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""] # 空字符串"" 表明使用 core API group
resources: ["pods"]
verbs: ["get", "watch", "list", “”]
下面例子是 ClusterRole 的定义示例,用于授予用户对某一特定命名空间,或者所有命名空间中的 secret(取决于其绑定方式)的读访问权限:
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
# 鉴于 ClusterRole 是集群范围对象,所以这里不需要定义 "namespace" 字段
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
权限集合的定义在 rules 中,包含三部分:
apiGroups
API 组,资源所在的组,比如 Job 对象在 batch 组,Deploymet 在 app 组。可以通过 kubectl api-resources 命令查看其 apiversion 中的组。如果是空字符串代表 core 组。
resources
具体的资源列表,比如 pods,cronjobs 等。大多数资源由代表其名字的字符串表示,例如”pods”,但有一些 Kubernetes API 还 包含了“子资源”,比如 pod 的 logs。在 Kubernetes 中,pod logs endpoint 的 URL 格式为:
GET /api/v1/namespaces/{namespace}/pods/{name}/log
在这种情况下,”pods” 是命名空间资源,而 “log” 是 pods 的子资源。为了在 RBAC 的角色中表示出这一点,我们需要使用斜线来划分资源 与子资源。如果需要角色绑定主体读取 pods 以及 pod log,您需要定义以下角色:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
namespace: default
name: pod-and-pod-logs-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
另外可以通过设置 resourceNames
表示针对某个特定的对象的操作权限,此时请求所使用的动词不能是 list、watch、create 或者 deletecollection,因为资源名不会出现在 create、list、watch 和 deletecollection 等的 API 请求中。
除了针对 API Object 这些资源外,我们还需要对其他非资源类的 API 进行授权,此时需要通过 nonResourceURLs 资源来指定 URL 进行授权。下面是一个示例,表示对 “/healthz” 及其所有子路径有”GET” 和”POST” 的请求权限。
rules:
- nonResourceURLs: ["/healthz", "/healthz/*"] # 在非资源 URL 中,'*' 代表后缀通配符
verbs: ["get", "post"]
verbs
一系列动词集合,代表允许对资源执行的操作,本质上可以发起的请求类型。动词选项和 HTTP 请求对应如下:
verb | 含义 | HTTP 请求 |
---|---|---|
get | 获取单个资源 | GET,HEAD |
list | 获取一组资源 | GET,HEAD |
watch | 获取单个资源 | GET,HEAD |
create | 创建资源 | POST |
update | 更新资源 | PUT |
patch | 更新资源 | PATCH |
delete | 局部更新资源 | DELETE |
deletecollection | 删除一组资源 | DELETE |
通过 kubectl get clusterrole
或 kubectl get role --all-namespace
可以查出 K8s 的所有的 ClusterRole 和 Role。通过 kubectl describe clusterrole
可以看到这些role在哪些资源上有什么样的权限。
下面是一些示例:
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
··`
- 允许读取”pods” 以及读写”jobs”:
```yaml
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch", "extensions"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["my-config"]
verbs: ["get"]
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
角色绑定将一个角色中定义的各种权限授予一个或者一组用户。 角色绑定包含了一组相关 subject 主体, subject 包括用户 User、用户组 Group、或者服务账户 Service Account 以及对被授予角色的引用。
在命名空间中可以通过 RoleBinding 对象授予权限,而集群范围的权限授予则通过 ClusterRoleBinding 对象完成。
ClusterRole 可以通过 RoleBinding 进行角色绑定,但仅对 RoleBinding 所在命名空间有效。
下面示例是一些示例:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: read-secrets-global
subjects:
- kind: Group
name: manager
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: secret-reader
apiGroup: rbac.authorization.k8s.io
除了定义 yaml 文武兼外,对于 rolebinding 的操作可以通过命令行方便的完成,下面是一些示例:
在某一特定命名空间内授予 Role 或者 ClusterRole。示例如下:
# 在名为"acme" 的命名空间中将 admin ClusterRole 授予用户"bob":
kubectl create rolebinding bob-admin-binding --clusterrole=admin --user=bob --namespace=acme
# 在名为"acme" 的命名空间中将 view ClusterRole 授予服务账户"myapp":
kubectl create rolebinding myapp-view-binding --clusterrole=view --serviceaccount=acme:myapp --namespace=acme
在整个集群中授予 ClusterRole,包括所有命名空间。示例如下:
# 在整个集群范围内将 cluster-admin ClusterRole 授予用户"root":
kubectl create clusterrolebinding root-cluster-admin-binding --clusterrole=cluster-admin --user=root
# 在整个集群范围内将 system:node ClusterRole 授予用户"kubelet":
kubectl create clusterrolebinding kubelet-node-binding --clusterrole=system:node --user=kubelet
# 在整个集群范围内将 view ClusterRole 授予命名空间"acme" 内的服务账户"myapp":
kubectl create clusterrolebinding myapp-view-binding --clusterrole=view --serviceaccount=acme:myapp
请求在完成认证和授权之后,对象在被持久化到 etcd 之前,还需要通过一系列的准入控制器进行验证。如同我们业务系统一样,除了需要验证登陆用户的身份、操作权限外,还需要验证用户的操作对不对,比如提交一个表单,要看下必填项是否都填了,手机号码的格式是否正确等,甚至还可能要拦截请求做额外的处理,比如注入请求头做流量着色。
Kubernetes 也一样,需要对外部提交的请求做校验、拦截修改等操作。所谓准入控制器就是一系列的插件,每个插件都有其特定的功能,比如允许哪些请求进入,限定对资源的使用,设定 Pod 的安全策略等。它们作为看门人(gatekeeper)来对发送到 Kubernetes 做拦截验证,从而实现对集群使用方式的管理,
图片来自 https://sysdig.com/blog/kubernetes-admission-controllers/
准入控制器的操作有两种:
一个准入控制器可以是只执行 mutating 或者 validating ,也可以两个都执行,先执行 mutating 在执行 validating。
Kubernetes 本身已经提供了很多的准入控制器插件,可以通过以下命令查看:
$ kube-apiserver -h | grep enable-admission-plugins
CertificateApproval, CertificateSigning, CertificateSubjectRestriction, DefaultIngressClass, DefaultStorageClass, DefaultTolerationSeconds, LimitRanger, MutatingAdmissionWebhook, NamespaceLifecycle, PersistentVolumeClaimResize, Priority, ResourceQuota, RuntimeClass, ServiceAccount, StorageObjectInUseProtection, TaintNodesByCondition, ValidatingAdmissionWebhook
每个准入控制器插件基本都是实现了某一特定的功能,在启动 kube-apiserver 时可以通过设置 --enable-admission-plugins,--disable-admission-plugins
参数来启动或者禁用某些注入控制器。已有的控制器类型可以参考 文档。
在 Kubernetes 默认的准入控制器中,有两个特殊的控制器:
它们以 WebHook 的方式提供扩展能力,我们可以在集群中创建相关的 WebHook 配置并在配置中选择想要关注的资源对象,这样对应的资源对象在执行操作时就可以触发 WebHook,然后我们可以编写具体的响应代码实现准入控制。
MutatingAdmissionWebhook
用来修改用户的请求,执行 mutating 操作,比如修改镜像、添加注解、注入 SideCar 等。
ValidatingAdmissionWebhook
则只能用来做校验,比如检查命名规范,检查镜像的使用。 MutatingAdmissionWebhook 会在 ValidatingAdmissionWebhook 前执行。
想要自定义准入控制策略,集群需要满足以下条件:
为了实现自定义的 WebHook,我们主要需要两步操作:
所谓 WebHook Server 就是 在 webhook 触发时的响应服务:
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "" ,
"allowed": true
}
}
因此,我们的任务就是编写一个 HTTP 服务,来接收 AdmissionReview 的请求并返回 AdmissionReview 响应。
有了 WebHook server 后,我们就可以创建配置来指定要选择的资源以及响应服务了。
Kubernetes 提供了 MutatingWebhookConfiguration
和 ValidatingWebhookConfiguration
两种 API 对象来让我们动态的创建准入控制的配置。顾名思义,前者用来拦截并修改请求,后者用来验证请求是否正确。下面是一个 ValidatingWebhookConfiguration
配置示例:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: "pod-policy.example.com"
webhooks:
- name: "pod-policy.example.com"
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
scope: "Namespaced"
clientConfig:
service:
namespace: "example-namespace"
name: "example-service"
caBundle: "Ci0tLS0tQk...<`caButLS0K"
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
timeoutSeconds: 5
可以看到 API 对象就是用来定义一系列的 webhook 的,每个 webhook 的配置包含以下主要字段:
rules
每个webhook 需要设置一系列规则来确认某个请求是否需要发送给 webhook。每个规则可以指定一个或多个 operations、apiGroups、apiVersions 和 resources 以及资源的 scope:
下面示例表示匹配针对 apps/v1 和 apps/v1beta1 组中 deployments 和 replicasets 资源的 CREATE 或 UPDATE 请求
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["apps"]
apiVersions: ["v1", "v1beta1"]
resources: ["deployments", "replicasets"]
scope: "Namespaced"
objectSelector
根据发送对象的标签来判断是否拦截,如下面示例,任何带有 foo=bar 标签的对象创建请求都会被拦截触发 webhook。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
objectSelector:
matchLabels:
foo: bar
rules:
- operations: ["CREATE"]
apiGroups: ["*"]
apiVersions: ["*"]
resources: ["*"]
scope: "*"
...
namespaceSelector
匹配命名空间,根据资源对象所在的命名空间作拦截,下面是 EaseMesh 的示例,
针对带有 mesh.megaease.com/mesh-service
标签的,除 easemesh、kube-system、kube-public 之外的命名空间中的对象,如果对象符合 rules 中的定义,则会将请求发送到 webhook server。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
...
webhooks:
- name: mesh-injector.megaease.com
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values:
- easemesh
- kube-system
- kube-public
- key: mesh.megaease.com/mesh-service
operator: Exists
clientConfig
这里配置的就是我们的 Webhook Server 访问地址以及验证信息。当一个请求经由上述选择规则确定要发送到 webhook 后,就会根据 clientConfig 中配置的信息向我们的 WebHook Server 发送请求。
WebHook Server 可以分为集群内和集群外部的服务,如果是集群外部的服务需要配置访问 URL,示例如下:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
clientConfig:
url: "https://my-webhook.example.com:9443/my-webhook-path"
...
如果是集群内部的服务,则可以通过配置服务名后通过 Kubernetes 的 DNS 访问到,下面是 EaseMesh 中的示例:
➜ ~ |>kubectl get svc -n easemesh
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
easemesh-operator-service ClusterIP 10.233.53.203 > 8443/TCP,9090/TCP
clientConfig:
caBundle: LS0...tCg==
service:
Name: easemesh-operator-service
Namespace: easemesh
Path: /mutate
Port: 9090
下面是官方文档中提供的一个例子 admission-controller-webhook-demo ,
该应用的目的是要限制 Pod 的权限,尽量避免以 root 用户的身份运行:
var runAsNonRoot *bool
var runAsUser *int64
if pod.Spec.SecurityContext != nil {
runAsNonRoot = pod.Spec.SecurityContext.RunAsNonRoot
runAsUser = pod.Spec.SecurityContext.RunAsUser
}
// Create patch operations to apply sensible defaults, if those options are not set explicitly.
var patches []patchOperation
if runAsNonRoot == nil {
patches = append(patches, patchOperation{
Op: "add",
Path: "/spec/securityContext/runAsNonRoot",
// The value must not be true if runAsUser is set to 0, as otherwise we would create a conflicting
// configuration ourselves.
Value: runAsUser == nil || *runAsUser != 0,
})
if runAsUser == nil {
patches = append(patches, patchOperation{
Op: "add",
Path: "/spec/securityContext/runAsUser",
Value: 1234,
})
}
} else if *runAsNonRoot == true && (runAsUser != nil && *runAsUser == 0) {
// Make sure that the settings are not contradictory, and fail the object creation if they are.
return nil, errors.New("runAsNonRoot specified, but runAsUser set to 0 (the root user)")
}
上面是主要的 mutating 逻辑代码,下面是启动一个 http server 来处理请求,将上面的方法传进去作为 handler 。
func main() {
certPath := filepath.Join(tlsDir, tlsCertFile)
keyPath := filepath.Join(tlsDir, tlsKeyFile)
mux := http.NewServeMux()
mux.Handle("/mutate", admitFuncHandler(applySecurityDefaults))
server := &http.Server{
// We listen on port 8443 such that we do not need root privileges or extra capabilities for this server.
// The Service object will take care of mapping this port to the HTTPS port 443.
Addr: ":8443",
Handler: mux,
}
log.Fatal(server.ListenAndServeTLS(certPath, keyPath))
}
func main() {
certPath := filepath.Join(tlsDir, tlsCertFile)
keyPath := filepath.Join(tlsDir, tlsKeyFile)
mux := http.NewServeMux()
mux.Handle("/mutate", admitFuncHandler(applySecurityDefaults))
server := &http.Server{
// We listen on port 8443 such that we do not need root privileges or extra capabilities for this server.
// The Service object will take care of mapping this port to the HTTPS port 443.
Addr: ":8443",
Handler: mux,
}
log.Fatal(server.ListenAndServeTLS(certPath, keyPath))
}
将上面的程序部署到集群
$ kubectl get service -n webhook-demo
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webhook-server ClusterIP 10.108.206.71 <none> 443/TCP 7s
$ kubectl get pods -n webhook-demo
NAME READY STATUS RESTARTS AGE
webhook-server-69c78cb569-s9dw6 1/1 Running 0 10s
Webhook Server 部署好就可以配置准入控制的配置了,因为要修改请求,因此要创建 MutatingWebhookConfiguration,下面是创建好的配置内容:
$ kubectl describe mutatingwebhookconfigurations demo-webhook
Name: demo-webhook
Namespace:
Labels: <none>
Annotations: <none>
API Version: admissionregistration.k8s.io/v1
Kind: MutatingWebhookConfiguration
Metadata:
Webhooks:
Admission Review Versions:
v1
v1beta1
Client Config:
Ca Bundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURQekNDQWllZ0F3SUJBZ0lVZUNs
Service:
Name: webhook-server
Namespace: webhook-demo
Path: /mutate
Port: 443
Failure Policy: Fail
Match Policy: Equivalent
Name: webhook-server.webhook-demo.svc
Namespace Selector:
Object Selector:
Reinvocation Policy: Never
Rules:
API Groups:
API Versions:
v1
Operations:
CREATE
Resources:
pods
Scope: *
Side Effects: None
Timeout Seconds: 10
Events: <none>
可以看到其 Rule 是当收到 Pod 的创建请求时,会将发送请求到我们的 Webhook Server。
下面测试 Pod 的创建,首先是默认情况下,如果不设置会自动配置 runAsNoneRoot 和 runAsUser
apiVersion: v1
kind: Pod
metadata:
name: pod-with-defaults
labels:
app: pod-with-defaults
spec:
restartPolicy: OnFailure
containers:
- name: busybox
image: busybox
command: ["sh", "-c", "echo I am running as user $(id -u)"]
$ kubectl get pods pod-with-defaults -o yaml
apiVersion: v1
kind: Pod
metadata:
labels:
app: pod-with-defaults
name: pod-with-defaults
namespace: default
spec:
containers:
- command:
- sh
- -c
- echo I am running as user $(id -u)
image: busybox
imagePullPolicy: Always
name: busybox
securityContext:
runAsNonRoot: true
runAsUser: 1234
如果是 runAsNonRoot 如果设置为 true,但是 runAsUser 设置 设置为 0,请求会被拦截并报错:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-conflict
labels:
app: pod-with-conflict
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 0
containers:
- name: busybox
image: busybox
command: ["sh", "-c", "echo I am running as user $(id -u)"]
$ kubectl apply -f examples/pod-with-conflict.yaml
Error from server: error when creating "examples/pod-with-conflict.yaml": admission webhook "webhook-server.webhook-demo.svc" denied the request: runAsNonRoot specified, but runAsUser set to 0 (the root user)
可以看到请求被正确拦截了, 以上是动态准入控制的简单介绍,更多的细节可以参考官方文档 和博客。
上述 RBAC、准入控制等策略都是针对 api-server 的安全访问控制,如果外部攻击者攻破了 API Server 的访问控制成功部署了 Pod,并在容器中运行攻击代码,依然是可以对我们的系统造成损害。因此我们还需要设置 Pod 的操作权限,不能让 Pod “为所欲为”
。
Pod 有自己的网络、PID、IPC 命名空间,因此同一 Pod 中的容器可以共享网络,可以进行进程间通信以及只看到自己的进程树。如果某些 Pod 需要使用宿主机默认的命令空间,则需要额外进行设置。
网络命名空间
可以通过 hostNetwork: true 配置来使 Pod 直接使用网络的命名空间。
apiVersion: v1
kind: Pod
metadata:
name: pod-with-host-network
spec:
hostNetwork: true
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
这样 Pod 创建后其网络用的就是宿主机的网络,在 Pod 中执行 ifconfig 命令查看网络设备会看到其所在宿主机的网络设备列表。
$ kubectl exec -it pod-with-host-network -- ifconfig
eth0 Link encap:Ethernet HWaddr 52:54:00:22:84:B5
inet addr:172.19.0.3 Bcast:172.19.15.255 Mask:255.255.240.0
inet6 addr: fe80::5054:ff:fe22:84b5/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:5089655 errors:0 dropped:0 overruns:0 frame:0
TX packets:5061521 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1952900135 (1.8 GiB) TX bytes:1062809752 (1013.5 MiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:1334609 errors:0 dropped:0 overruns:0 frame:0
TX packets:1334609 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:159996559 (152.5 MiB) TX bytes:159996559 (152.5 MiB)
...
像 Kubernetes 的控制平面组件 kube-apiserver 等都是设置了该选项,从而使得它们的行为与不在 Pod 中运行时相同。
$ kubectl get pods -n kube-system kube-apiserver-vm-0-7-ubuntu -o yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver-vm-0-7-ubuntu
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
image: k8s.gcr.io/kube-apiserver:v1.22.3
imagePullPolicy: IfNotPresent
...
hostNetwork: true
另外还可以通过设置 hostPort 使容器使用所在节点的主机端口而不是直接共享命名空间。
apiVersion: v1
kind: Pod
metadata:
name: kubia-hostport
spec:
containers:
- image: luksa/kubia
name: kubia
ports:
- containerPort: 8080
hostPort: 9000
protocol: TCP
这样当访问 Pod 所在节点上的 9000 端口时会访问到 Pod 中容器,因为要占用主机端口,因此如果有多个副本的话这些副本不能被调度到同一个节点。
该功能最初主要是用来暴露通过 DaemonSet 在每个节点上运行的服务,后来也用来做 Pod 的调度,保证相同的 Pod 不能被部署到同一个节点,现在已经被 Pod 非亲和的调度方式所取代。
除了网络命名空间,Pod 还可以直接使用宿主机的 IPC 和 PID 命名空间,从而看到宿主机所有的进程,以及与宿主机的进程进行通信。
apiVersion: v1
kind: Pod
metadata:
name: pod-with-host-pid-and-ipc
spec:
hostPID: true
hostIPC: true
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
除了使用宿主机的命名空间,还是设置安全上下文来定义 Pod 或者容器的特权和访问控制设置。包含但不限于一下配置:
下面是一些使用示例:
设置容器的安全上下文
– 设置非 root 用户执行
容器运行的用户可以在构建镜像时指定,如果攻击者获取到 Dockerfile 并设置的 root 用户,如果 Pod 挂载了宿主机目录,此时就会对宿主机的目录有完整的访问权限。如果是非 root 用户则不会有完整的权限。
apiVersion: v1
kind: Pod
metadata:
name: pod-run-as-non-root
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsNonRoot: true
如果容器获取内核的完整权限,需要在宿主机能做任何事,就可以设置为特权模式。
apiVersion: v1
kind: Pod
metadata:
name: pod-privileged
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
privileged: true
像 kube-proxy 需要修改 iptables 规则,因此就开启了特权模式。
$ kubectl get pods kube-proxy-fschm -n kube-system -o yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-proxy-fschm
namespace: kube-system
spec:
image: k8s.gcr.io/kube-proxy:v1.22.3
imagePullPolicy: IfNotPresent
name: kube-proxy
securityContext:
privileged: true
除了赋予完整权限的特权模式,我们还可以细粒度的添加或者删除内核操作权限,下面是一个例子,允许修改系统时间,但不允许容器修改文件的所有者。
apiVersion: v1
kind: Pod
metadata:
name: pod-add-settime-capability
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
capabilities:
add:
- SYS_TIME
drop:
- CHOWN
除了为容器单独设置上下文,一部分配置可以在 Pod 层面设置,表示对 Pod 中所有的容器生效,如果容器也设置了则会覆盖掉 Pod 的设置,另外 Pod 也要独有的上下文配置,可以参考 官方文档,这里就不做赘述了。
安全上下文是创建 Pod 的用户指定的,除此之外我们还需要在集群层面来保证用户不能滥用相关的权限。因此之前 Kubernetes 提供了集群层面 PSP(PodSecurityPolicy)对象来让集群员来定义用户的 Pod 能否使用各种安全相关的特性。
可以通过 PSP 来统一批量设置相关的安全设置,然后通过 RBAC 为不同的用户赋予不同 PSP,然后在创建 Pod 时指定用户,就可以实现针对不用的 Pod 应用不同的安全策略了。下面是一个例子:
通过创建一个 PSP 同时指定用户、是否使用宿主机命名空间、启用和禁用内核权限等。
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
hostIPC: false
hostPID: false
hostNetwork: false
hostPorts:
- min: 10000
max: 11000
- min: 13000
max: 14000
privileged: false
readOnlyRootFilesystem: true
runAsUser:
rule: RunAsAny
fsGroup:
rule: RunAsAny
allowedCapabilities:
- SYS_TIME
defaultAddCapabilities:
- CHOWN
requiredDropCapabilities:
- SYS_ADMIN
- SYS_MODULE
然后可以通过 RBAC 进行设置,鉴于 PSP 已经被弃用,并将在 1.25 版本移除,这里就不多做讲解了。取而代之的是使用新的 PodSecurity 进行安全相关的设置,截止到 1.23 该特性处于 beta 阶段。下面简单看一下
PodSecurity 是一个准入控制器,其由松到紧定义了三种安全级别的策略:
privileged: 特权策略,表示几乎没有限制。提供最大可能范围的权限许可
baseline:基线策略,允许使用默认的(规定最少)Pod 配置。
Restricted:限制性最强的策略,遵循保护 Pod 针对最佳实践。
策略具体关联都的权限控制可以查看 pod-security-standards 文档。有了策略后,我们可以在命名空间上声明针对各个安全策略的处理方式。具体处理方式也有三种:
针对每种方式,Kubernetes 提供了两个标签来指定处理的安全级别和 Kubernetes minor 版本:
pod-security.kubernetes.io/
,model 必须是 enforce、audit、warn 之一,level 必须是 privileged、baseline、restricted 之一。
pod-security.kubernetes.io/
:表示策略执行的版本,必须是 Kubernetes minor 版本或者 latest。
然后就可以在 namespace 上添加标签来进行安全限制了,下面的例子表示,在 my-baseline-namespace
来命名空间创建的 Pod:
apiVersion: v1
kind: Namespace
metadata:
name: my-baseline-namespace
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.23
# We are setting these to our _desired_ `enforce` level.
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.23
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.23
Network Policy 类似于 AWS 的安全组,是一组 Pod 间及与其他网络端点间所允许的通信规则。NetworkPolicy 资源使用 Label 和 Selector 选择 Pod,并定义选定 Pod 所允许的通信规则。
下面是一个 NetworkPolicy 的示例:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978
上述的示例意思是:
对 default 命名空间下带有标签 role=db 的 Pod 进行如下配置
(Ingress 规则)允许以下客户端连接到被选中 Pod 的 6379 TCP 端口:
(Egress 规则)允许被选中 Pod 可以访问以下节点
podSelector
每个 NetworkPolicy 都包括一个 podSelector ,它根据标签选安定一组 Pod 以应用其所定义的规则。示例中表示选择带有 “role=db” 标签的 Pod。如果 podSelector 为空,表示选择namespace下的所有 Pod。
policyTypes
表示定义的规则类型,包含 Ingress 或 Egress 或两者兼具。
Ingress / Egress 下用来限定规则的方式有四种:
同时指定 namespaceSelector 和 podSelector 时,请求需要同时满足任意两个条件。
ingress:
- from:
- namespaceSelector:
matchLabels:
user: alice
podSelector:
matchLabels:
role: client
各自指定时,只满足其中一个条件即可:
ingress:
- from:
- namespaceSelector:
matchLabels:
user: alice
- podSelector:
matchLabels:
role: client
ports
可以被访问的端口或者可以访问的外部端口。在定义时指明协议和端口即可,一般都是 TCP 协议,
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
....
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978
从 1.20 版本开始默认也支持 SCTP 协议,如果想关掉需要修改 apiserver 的启动配置 --feature-gates=SCTPSupport=false
。
另外现在可以指定一组端口,该特性在 1.22 版本处于 beta 状态,使用示例如下:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: multi-port-egress
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 32000
endPort: 32768
这里表示选中的 Pod 可以访问 10.0.0.0/24 网段的 32000 ~ 32768 端口。这里有几个要求:
下面是一些特殊的规则示例:
拒绝所有入站流量
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
允许所有入站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all-ingress
namespace: default
spec:
podSelector: {}
ingress:
- {}
policyTypes:
- Ingress
拒绝所有出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: default
spec:
podSelector: {}
policyTypes:
- Egress
允许所有出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all-egress
spec:
podSelector: {}
egress:
- {}
policyTypes:
- Egress
拒绝所有入站和出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
最后需要注意的是 Network Policy 的功能是由网络插件实现的,因此是否可以使用该特性取决于我们使用的网络插件。像 Calico、Weave 都对该功能做了支持,但 Flannel 本身不支持,需要结合 Calico 使用才性能,参考文档 Installing Calico for policy and flannel (aka Canal) for networking。
以上是对 Kubernetes 安全相关的简单概述,在实际云原生环境里,其安全性按层分需要从所谓的 4C(Cloud, Clusters, Containers, and Code.)四个层面来保证。Kubernetes 只是其中的一层而已。