最小化微服务漏洞
# 最小化微服务漏洞
# 主要内容
❖ Pod安全上下文
❖ Pod安全策略
❖ Secret存储敏感数据
❖ 安全沙箱运行容器
# Pod安全上下文
安全上下文(Security Context)
:K8s对Pod和容器提供的安全机制,可以设置Pod特权和访问控制。
安全上下文限制维度:
自主访问控制(Discretionary Access Control)
:基于用户ID(UID)和组ID(GID),来判定对对象(例如文件) 的访问权限。安全性增强的 Linux(SELinux)
: 为对象赋予安全性标签。- 以
特权模式
或者非特权模式运行。 Linux Capabilities
: 为进程赋予 root 用户的部分特权而非全部特权。AppArmor
:定义Pod使用AppArmor限制容器对资源访问限制Seccomp
:定义Pod使用Seccomp限制容器进程的系统调用AllowPrivilegeEscalation
: 禁止容器中进程(通过 SetUID 或 SetGID 文件模式)获得特权提升。当容器以特权模式 运行或者具有CAP_SYS_ADMIN能力时,AllowPrivilegeEscalation总为True。readOnlyRootFilesystem
:以只读方式加载容器的根文件系统。
# 案例实施
# 案例1:设置容器以普通用户运行
背景:容器中的应用程序默认以root账号运行的,这个root与宿主机root账号是相同的, 拥有大部分对Linux内核的系统调用权限,这样是不安全的,所以我们应该将容器以普 通用户运行,减少应用程序对权限的使用。
可以通过两种方法设置普通用户:
- Dockerfile里使用USER指定运行用户
- K8s里指定spec.securityContext.runAsUser,指定容器默认用户UID
spec:
securityContext:
runAsUser: 1000 # 镜像里必须有这个用户UID
fsGroup: 1000 # 数据卷挂载后的目录属组设置为该组
containers:
- image: lizhenliang/flask-demo:root
name: web
securityContext:
allowPrivilegeEscalation: false # 不允许提权
创建Pod使用安全上下文测试是否改为uuid运行容器:
[root@master01:~] # cat pod-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: test
spec:
securityContext:
runAsUser: 1000
fsGroup: 1000
containers:
- image: lizhenliang/flask-demo:root
name: test
securityContext:
allowPrivilegeEscalation: false
执行yaml之后进入容器查看当前的uuid:
[root@master01:~]# kubectl exec -it test -- bash
python@test:/data/www$ id
uid=1000(python) gid=1000(python) groups=1000(python)
# 案例2:避免使用特权容器
背景:容器中有些应用程序可能需要访问宿主机设备、修改内核等需求,在默认情况下, 容器没这个有这个能力,因此这时会考虑给容器设置特权模式。
启用特权模式:
containers:
- image: lizhenliang/flask-demo:root
name: web
securityContext:
privileged: true / false
启用特权模式就意味着,你要为容器提供了访问Linux内核的所有能力,这是很危险的, 为了减少系统调用的供给,可以使用Capabilities为容器赋予仅所需的能力。
Linux Capabilities:Capabilities 是一个内核级别的权限
,它允许对内核调用权 限进行更细粒度的控制,而不是简单地以 root 身份能力授权。 Capabilities 包括更改文件权限、控制网络子系统和执行系统管理等功能。在 securityContext 中,可以添加或删除 Capabilities,做到容器精细化权限控制。
# 案例3:取消挂载文件系统
容器默认没有挂载文件系统能力,添加SYS_ADMIN增加这个能力
apiVersion: v1
kind: Pod
metadata:
name: busybox
spec:
containers:
- image: busybox
name: test
command:
- sleep
- 24h
securityContext:
capabilities:
add: ["SYS_ADMIN"]
创建进入容器测试挂载权限:
[root@master01:~]# kubectl exec -it busybox -- sh
/ # mount /data/ /tmp/
mount: mounting /data/ on /tmp/ failed: Permission denied
# 案例4:只读挂载权限
只读挂载容器文件系统,防止恶意二进制文件创建
apiVersion: v1
kind: Pod
metadata:
name: busybox
spec:
containers:
- image: busybox
name: test
command:
- sleep
- 24h
securityContext:
readOnlyRootFilesystem: true
创建进入容器测试文件权限:
[root@master01:~]# kubectl exec -it busybox -- sh
/ # mkdir a
mkdir: can't create directory 'a': Read-only file system
# Pod安全策略(PSP)
PodSecurityPolicy(简称PSP)
:Kubernetes中Pod部署时重要的安全校验手段,能够 有效地约束应用运行时行为安全。 使用PSP对象定义一组Pod在运行时必须遵循的条件及相关字段的默认值,只有Pod满足这 些条件才会被K8s接受。
# Pod安全策略限制维度
# 启用准入控制器
Pod安全策略实现为一个准入控制器,默认没有启用,当启用后会强制实施 Pod安全策略,没有满足的Pod将无法创建。因此,建议在启用PSP之前先添加 策略并对其授权。
启用Pod安全策略:
vi /etc/kubernetes/manifests/kube-apiserver.yaml ... --enable-admission-plugins=NodeRestriction,PodSecurityPolicy ... systemctl restart kubelet
# 如何使用PSP资源
用户使用SA (ServiceAccount)创建了一个Pod,K8s会先验证这个SA是否 可以访问PSP资源权限
,如果可以进一步验证Pod配置是否满足PSP规则,任 意一步不满足都会拒绝部署。
因此,需要实施需要有这几点:
- 创建SA服务账号
- 该SA需要具备创建对应资源权限,例如创建Pod、Deployment
- SA使用PSP资源权限:创建Role,使用PSP资源权限,再将SA绑定Role
# 案例实施
# 示例1:禁止创建特权模式的Pod
创建Pod安全策略资源如下:
- 不允许特权Pod
- 运行seLinux的规则
- 指定容器启动的用户ID和主组ID位任何人或者禁止没指定普通用户运行的容器(runAsUser)
- 允许使用挂载类型数据卷
[root@master01:~]# cat psp.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: psp-example
spec:
privileged: false # 不允许特权Pod
# 下面是一些必要的字段
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
runAsUser:
rule: RunAsAny / MustRunAsNonRoot
fsGroup:
rule: RunAsAny
volumes:
- '*'
执行之后查看当前的PSP资源情况:
在后续的1.25之后的版本里面,将会完全废弃Pod安全策略,采用安全上下文 的方式。
[root@master01:~]# kubectl apply -f psp.yaml
Warning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
podsecuritypolicy.policy/psp-example created
[root@master01:~]# kubectl get psp
Warning: policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
NAME PRIV CAPS SELINUX RUNASUSER FSGROUP SUPGROUP READONLYROOTFS VOLUMES
psp-example false RunAsAny RunAsAny RunAsAny RunAsAny false *
# 示例2:禁止创建特权模式流程
# 创建SA
$ kubectl create serviceaccount aliang
# 将SA绑定到系统内置Role
$ kubectl create rolebinding aliang --clusterrole=edit --serviceaccount=default:aliang
# 创建使用PSP权限的Role
$ kubectl create role psp:unprivileged --verb=use --resource=podsecuritypolicy --resource-name=psp-example
# 将SA绑定到Role
$ kubectl create rolebinding aliang:psp:unprivileged --role=psp:unprivileged --serviceaccount=default:aliang
# 创建pod测试
$ kubectl --as=system:serviceaccount:default:psp-sa run nginx --image=nginx
# OPA Gatekeeper
# OPA介绍
PSP不足与状况:
- 将再1.21版本弃用PSP,在1.25版本删除PSP
- 仅支持Pod策略
- 使用复杂,权限模型存在缺陷,控制不明确
OPA(Open Policy Agent)
:是一个开源的、通用策略引擎,可以将策略编写为代码。提供 一个种高级声明性语言-Rego来编写策略,并把决策这一步骤从复杂的业务逻辑中解耦出来。
OPA可以用来做什么?
- 拒绝不符合条件的YAML部署
- 允许使用哪些仓库中的镜像
- 允许在哪个时间段访问系统
- 等...
# OPA工作流程
Gatekeeper 是基于 OPA的一个 Kubernetes 策略解决方案,可替代PSP或者部分RBAC功能。
当在集群中部署了Gatekeeper组件,APIServer所有的创建、更新或者删除操作都会触发 Gatekeeper来处理,如果不满足策略则拒绝。
# 部署OPA Gatekeeper
Gatekeeper的策略由两个资源对象组成:
Template
:策略逻辑实现的地方,使用rego语言Contsraint
:负责Kubernetes资源对象的过滤或者为Template提供输入参数
[root@master01:~/cks/opa]# kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.7/deploy/gatekeeper.yaml
[root@master01:~/cks/opa]# kubectl get pods -n gatekeeper-system
NAME READY STATUS RESTARTS AGE
gatekeeper-controller-manager-66f474f785-kn5fl 1/1 Running 0 32s
gatekeeper-controller-manager-66f474f785-xgz7s 1/1 Running 0 32s
gatekeeper-controller-manager-66f474f785-xxwdm 1/1 Running 0 32s
# 案例实施
# 案例1:禁止容器启用特权
- 需要创建
ConstraintTemplate
的资源 - 使用
rego
的语言编写对启用特权的资源发出警告 - 需要指定类型为
privileged
模式
[root@master01:~/cks/opa]# cat privileged_tpl.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: privileged
spec:
crd:
spec:
names:
kind: privileged
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package admission
violation[{"msg": msg}] { # 如果violation为true说明违反约束
containers = input.review.object.spec.template.spec.containers
c_name := containers[0].name
containers[0].securityContext.privileged # 如果返回false或者没获取到值说明通过
msg := sprintf("提示:'%v'容器禁止启用特权!",[c_name])
}
[root@master01:~/cks/opa]# kubectl get ConstraintTemplate
NAME AGE
privileged 32m
创建约束、如下是约束资源类型:
- Deployment
- DaemonSet
- StatefulSet
[root@master01:~/cks/opa]# cat privileged_constraints.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: privileged
metadata:
name: privileged
spec:
match: # 匹配的资源
kinds:
- apiGroups: ["apps"]
kinds:
- "Deployment"
- "DaemonSet"
- "StatefulSet"
[root@master01:~/cks/opa]# kubectl get constraints
NAME AGE
privileged 31m
创建Deployment开启特权模式再执行创建会发出警告。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
securityContext:
privileged: true
[root@master01:~/cks/opa]# kubectl apply -f deployment.yaml
Error from server ([privileged] 提示:'nginx'容器禁止启用特权!): error when creating "dp.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [privileged] 提示:'nginx'容器禁止启用特权!
# 案例2:只允许使用特定的镜像仓库
- 需要创建
ConstraintTemplate
的资源 - 使用
rego
的语言编写对镜像仓库的信任 - 需要指定类型为
image-check
模式
[root@master01:~/cks/opa]# cat image-check_tpl.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: image-check
spec:
crd:
spec:
names:
kind: image-check
validation:
openAPIV3Schema:
properties: # 需要满足条件的参数
prefix:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package image
violation[{"msg": msg}] {
containers = input.review.object.spec.template.spec.containers
image := containers[0].image
not startswith(image, input.parameters.prefix)
msg := sprintf("提示:'%v'镜像地址不在可信任仓库!", [image])
}
[root@master01:~/cks/opa]# kubectl get constrainttemplate
NAME AGE
image-check 77s
privileged 35m
这里需要配置传递OPA的参数为: "lizhenliang/"
[root@master01:~/cks/opa]# cat image-check_constraints.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: image-check
metadata:
name: image-check
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds:
- "Deployment"
- "DaemonSet"
- "StatefulSet"
parameters: # 传递给opa的参数
prefix: "lizhenliang/"
[root@master01:~/cks/opa]# kubectl get constraints
NAME AGE
image-check.constraints.gatekeeper.sh/image-check 77s
执行测试时使用docker官方的镜像仓库:
由于我们开启了镜像仓库的信任 所以对docker仓库不支持
只能从lizhenliang该仓库进行拉取镜像
[root@master01:~/cks/opa]# kubectl create deployment test --image=nginx
error: failed to create deployment: admission webhook "validation.gatekeeper.sh" denied the request: [image-check] 提示:'nginx'镜像地址不在可信任仓库!
# Secret存储敏感数据
Secret是一个用于存储敏感数据的资源,所有的数据要经过base64编码,数据实际会存储在K8s中Etcd, 然后通过创建Pod时引用该数据。
应用场景:凭据
Pod使用secret数据有两种方式:
变量注入
数据卷挂载
# secret的类型
kubectl create secret 支持三种数据类型:
- docker-registry:存储镜像仓库认证信息
- generic:从文件、目录或者字符串创建,例如存储用户名密码
- tls:存储证书,例如HTTPS证书
# secret的使用
一个 Secret 可以包含 Pod 访问数据库所需的用户凭证。 例如,由用户名和密码组成的数据库连接字符串。 你 可以在本地计算机上,将用户名存储在文件 ./username.txt
中,将密码存储在文件 ./password.txt
中。
[root@master01:~]# echo -n 'admin' > username.txt
[root@master01:~]# echo -n '1a2b3c4d' > password.txt
在这些命令中, -n 标志确保生成的文件在文本末尾不包含额外的换行符。 这一点很重要,因为当 kubectl 读 取文件并将内容编码为 base64 字符串时,多余的换行符也会被编码。
kubectl create secret 命令将这些文件打包成一个 Secret 并在 API 服务器上创建对象。
[root@master01:~]# kubectl create secret generic db-user-pass --from-file=username.txt --from-file=password.txt
secret/db-user-pass created
[root@master01:~]# kubectl get secrets db-user-pass -o yaml
apiVersion: v1
data:
password.txt: MWEyYjNjNGQ=
username.txt: YWRtaW4=
kind: Secret
metadata:
creationTimestamp: "2022-04-13T06:43:03Z"
name: db-user-pass
namespace: default
resourceVersion: "100098"
uid: 440fa829-0135-4ef2-b8dc-9f06f5a2f2a3
type: Opaque
# 解码 Secret
要查看创建的 Secret 的内容,运行以下命令:
[root@master01:~]# kubectl get secrets db-user-pass -o jsonpath='{.data}'
{"password.txt":"MWEyYjNjNGQ=","username.txt":"YWRtaW4="}
[root@master01:~]# echo 'MWEyYjNjNGQ=' | base64 -d
1a2b3c4d
[root@master01:~]# echo 'YWRtaW4=' | base64 -d
admin
# 案例实施
# 示例:将Mysql用户密码保存到Secret中存储
创建secret,mysql-root-password的键值是1a2a3a4a
[root@master01:~]# kubectl create secret generic mysql --from-literal=mysql-root-password=1a2a3a4a
secret/mysql created
[root@master01:~]# kubectl get secrets mysql -o yaml
apiVersion: v1
data:
mysql-root-password: MWEyYTNhNGE=
kind: Secret
metadata:
creationTimestamp: "2022-04-13T07:06:21Z"
name: mysql
namespace: default
resourceVersion: "102066"
uid: 3104256b-5b46-43a4-85ed-acc66be7ccba
type: Opaque
创建Pod测试,使用env从secret中引用变量
[root@master01:~]# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: mysql
name: mysql
spec:
nodeName: master01
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql
key: mysql-root-password
[root@master01:~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
mysql 1/1 Running 0 21s
# 安全沙箱运行容器
# gVisor介绍
所知,容器的应用程序可以直接访问Linux内核的系统调用,容器在安全隔离上还是比较弱,虽然 内核在不断地增强自身的安全特性,但由于内核自身代码极端复杂,CVE 漏洞层出不穷。 所以要想减少这方面安全风险,就是做好安全隔离,阻断容器内程序对物理机内核的依赖。 Google开源的一种gVisor容器沙箱技术就是采用这种思路,gVisor隔离容器内应用和内核之间访 问,提供了大部分Linux内核的系统调用,巧妙的将容器内进程的系统调用转化为对gVisor的访问。
gVisor兼容OCI,与Docker和K8s无缝集成,很方面使用。
项目地址:https://github.com/google/gvisor
# gVisor内核交互
# gVisor架构
# gVisor与Docker集成
gVisor内核要求:
Linux 3.17+ 如果用的是CentOS7则需要升级内核,Ubuntu不需要。
CentOS7内核升级步骤:
[root@master01 ~]# rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
[root@master01 ~]# rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
[root@master01 ~]# yum --enablerepo=elrepo-kernel install kernel-ml-devel kernel-ml –y
[root@master01 ~]# grub2-set-default 0
[root@master01 ~]# reboot
[root@master01 ~]# uname -r
[root@master01 ~]# 5.17.2-1.el7.elrepo.x86_64
gVisor集成Docker配置
参考文档:https://gvisor.dev/docs/user_guide/install/
已经测试过的应用和工具:https://gvisor.dev/docs/user_guide/compatibility/
1、准备gVisor二进制文件
[root@master01 ~]# sha512sum -c runsc.sha512
[root@master01 ~]# rm -f *.sha512
[root@master01 ~]# chmod a+x runsc
[root@master01 ~]# chmod a+x containerd-shim-runsc-v1
[root@master01 ~]# mv runsc containerd-shim-runsc-v1 /usr/local/bin
2、Docker配置使用gVisor
[root@master01 ~]# runsc install # 查看加的配置/etc/docker/daemon.json
[root@master01 ~]# systemctl restart docker
使用runsc运行容器:
[root@master01 ~]# docker run -d --runtime=runsc nginx
使用dmesg验证:
[root@master01 ~]# docker run --runtime=runsc -it nginx dmesg
# gVisor与Containerd集成
如果这里是Docker的环境,需要切换到Containerd容器引擎。
仍然需要将runsc 、containerd-shim-runsc-v1工具移到/usr/local/bin下面
1.准备配置
开启ipv4路由转发配置,重新生效系统配置
[root@master01 ~]# cat > /etc/sysctl.d/99-kubernetes-cri.conf << EOF
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
[root@master01 ~]# sysctl -system
2、安装Containerd
如果默认安装了docker-ce会自动安装containerd的依赖,没有则需要安装containerd。
ubuntu添加docker-ce的repo源之后apt-get install的方式安装
[root@master01 ~]# cd /etc/yum.repos.d
[root@master01 ~]# wget http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
[root@master01 ~]# yum install -y containerd.io
3、修改配置文件
- pause镜像地址
- Cgroup驱动改为systemd
- 增加runsc容器运行时
- 配置docker镜像加速器
修改配置文件默认的containerd的配置文件是空的,所以需要重新生成一个完整的配置文件。
containerd的目录是在 /etc/containerd
[root@master01 ~]# cd /etc/containerd/
[root@master01 containerd]# containerd config default > config.toml
[root@master01 containerd]# vim config.toml
43 [plugins."io.containerd.grpc.v1.cri"]
····
# 修改镜像为国内的镜像
56 sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.2"
····
# 添加一个runsc,指定类型为runsc,这里是跟runc同级
90 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
91 runtime_type = "io.containerd.runsc.v1"
# 修改systemd驱动为true
94 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
103 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
···
114 SystemdCgroup = true
# 添加docker仓库的镜像源,配置阿里镜像加速
139 [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
140 [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
141 endpoint = ["https://b9pmyelo.mirror.aliyuncs.com"]
配置kubelet使用containerd
配置完之后先重启containerd,再重启kubelet,最后如果pod出现报错,重启docker。
[root@master01 ~]# vi /etc/sysconfig/kubelet
KUBELET_EXTRA_ARGS=--container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sock --cgroup-driver=systemd
[root@master01 ~]# systemctl restart containerd
[root@master01 ~]# systemctl restart kubelet
[root@master01 ~]# systemctl restart docker
验证当前的Kubernetes容器基层
以上的步骤需要在所有集群配置
包括crictl连接containerd的配置也要在所有集群运行
[root@master01 ~]# kubectl get node -o wide | awk '{print $1,$2,$5,$NF}' | column -t
NAME STATUS VERSION CONTAINER-RUNTIME
master01 Ready v1.22.1 containerd://1.5.11
master02 Ready v1.22.1 containerd://1.5.11
master03 Ready v1.22.1 containerd://1.5.11
crictl连接containerd
containerd也有 ctr 管理工具,但功能比较简单,一般使用crictl工具检查和调试容器。
项目地址:https://github.com/kubernetes-sigs/cri-tools/
准备crictl连接containerd配置文件:
[root@master01 ~]# cat > /etc/crictl.yaml << EOF
runtime-endpoint: unix:///run/containerd/containerd.sock
EOF
[root@master01 ~]# systemctl restart containerd
下面是docker与crictl命令对照表:
# 案例实施
# 示例: K8s使用gVisor运行容器
- 创建RuntimeClass
- 绑定RuntimeClass
RuntimeClass 是一个用于选择容器运行时配置的特性,容器运行时配置用 于运行 Pod 中的容器。
创建RuntimeClass:
- 这里创建一个名称为
gvisor
的runtimeclass资源 - 可以创建多个
runtimeclass
资源,起到隔离的效果 - handler一定是
runsc
,对应CSI的配置名称
[root@master01 ~]# cat runtime.yaml
apiVersion: node.k8s.io/v1 # RuntimeClass 定义于 node.k8s.io API 组
kind: RuntimeClass
metadata:
name: gvisor # 用来引用 RuntimeClass 的名字
handler: runsc # 对应的 CRI 配置的名称
[root@master01 ~]# kubectl get runtimeclasses
NAME HANDLER AGE
gvisor runsc 72m
创建Pod测试gVisor:
这里使用runtimeClassName
绑定
[root@master01 ~]# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: pod
name: pod
spec:
nodeName: "master01"
runtimeClassName: gvisor
containers:
- image: nginx
name: pod
[root@master01 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
pod 1/1 Running 0 72m