一、先搞懂核心问题:为什么生产环境的容器攻击面必须缩
咱们先聊个最实在的场景:假设你公司的业务跑在容器里,突然有个黑客拿到了某个容器的权限,他能做什么?如果这个容器有root权限、能直接访问宿主机的网络、甚至能改宿主机的内核配置,那整个集群都可能被一锅端。这就是容器的“攻击面”——黑客能用来搞破坏的所有路径的总和。
CRI-O是现在很多生产集群用的容器运行时,和Docker类似但更轻量,专门适配K8s。它默认的配置为了兼容各种业务,留了很多不必要的权限,比如允许容器进程有宿主机的部分能力、能直接写宿主机的文件,这些都是黑客可以利用的漏洞。所以咱们要做的“缩减攻击面”,本质就是把CRI-O的配置改得“只给业务刚好够用的权限,多余的全关掉”。
二、第一步:内核能力裁剪——给容器进程“削权限”
先给大家补个基础:Linux里的“内核能力”,是把root的超级权限拆成了一个个小权限,比如能改网络配置的CAP_NET_ADMIN、能绑定1024以下端口的CAP_NET_BIND_SERVICE。默认情况下,CRI-O给容器进程开了不少内核能力,咱们要做的就是把没用的全关掉。
2.1 怎么看容器进程有哪些内核能力
先看个例子,咱们用K8s创建一个默认的Nginx容器,然后查它的进程能力。 技术栈:Kubernetes(1.24+)、CRI-O(1.23+)、Linux(CentOS 7)
首先创建一个默认的Nginx Pod:
apiVersion: v1
kind: Pod
metadata:
name: default-nginx
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
然后用kubectl exec进入容器,查进程能力:
# 进入容器
kubectl exec -it default-nginx -- sh
# 查当前进程的内核能力(capsh是Alpine里的工具)
capsh --print
执行后会看到类似这样的输出(简化版):
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
这里面的cap_net_raw是什么?是能直接操作网络层的权限,比如伪造IP包,Nginx根本不需要这个权限!这就是多余的攻击面。
2.2 怎么裁剪内核能力
咱们改刚才的Pod配置,把多余的内核能力删掉,只留Nginx必须的:
apiVersion: v1
kind: Pod
metadata:
name: hardened-nginx
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
# 内核能力配置:先把所有默认能力都删了(drop: ALL),再只加Nginx必须的
securityContext:
capabilities:
drop:
- ALL # 删掉所有默认的内核能力
add:
- NET_BIND_SERVICE # Nginx需要绑定80、443端口,必须要这个能力
- SETUID # Nginx运行时需要切换用户(从root改成www-data),必须要这个能力
- SETGID # 同上,切换组的能力
再用capsh查这个硬加固后的容器的能力,会发现只剩这三个了,多余的全没了。
2.3 裁剪内核能力的注意事项
- 先做测试:别直接改生产的配置,先在测试环境跑,比如加了drop: ALL后,Nginx能不能正常启动?如果报错,再查缺什么能力,比如有些业务需要改系统时间,就需要加SYS_TIME。
- 别留多余的:比如你业务不需要绑定1024以下的端口,那NET_BIND_SERVICE也可以删掉,改成绑定8080端口。
三、第二步:进程隔离——让容器进程“出不去”
裁剪内核能力只是让容器进程本身的权限变小,接下来要做的是让容器进程和宿主机、其他容器完全隔离开,哪怕黑客拿到了容器权限,也碰不到外面的东西。
3.1 最基础的隔离:禁用特权模式
很多人可能不知道,K8s的Pod默认有个“特权模式”(privileged),如果开了这个,容器进程就和宿主机的root权限一样,能直接访问宿主机的所有硬件、改内核配置,这是绝对的高危配置。
咱们先看一个开了特权模式的错误配置:
apiVersion: v1
kind: Pod
metadata:
name: bad-privileged-pod
spec:
containers:
- name: test
image: alpine:3.18
securityContext:
privileged: true # 绝对不能开!
如果黑客拿到这个容器的权限,他能直接改宿主机的防火墙、甚至卸载宿主机的内核,整个集群就废了。所以所有生产的Pod,特权模式必须关,默认就是关的,但要确保没人手动开。
3.2 进阶隔离:禁用宿主机网络、IPC、PID命名空间
Linux的命名空间是进程隔离的核心,默认情况下,CRI-O会为每个容器创建独立的网络、IPC(进程间通信)、PID命名空间,也就是容器里的进程看不到宿主机和其他容器的进程、网络。但有些业务为了方便,会把这些命名空间和宿主机共享,比如开了hostNetwork,容器的网络就和宿主机一样,能直接访问宿主机的所有端口,这也是高危的。
咱们看一个正确的隔离配置,把所有和宿主机共享的都关掉:
apiVersion: v1
kind: Pod
metadata:
name: fully-isolated-pod
spec:
# 禁用宿主机网络:容器用独立的网络命名空间
hostNetwork: false
# 禁用宿主机IPC:容器用独立的IPC命名空间
hostIPC: false
# 禁用宿主机PID:容器用独立的PID命名空间
hostPID: false
containers:
- name: nginx
image: nginx:1.25-alpine
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
- SETUID
- SETGID
# 再补一个:禁止容器进程以root用户运行(最后一道防线)
runAsUser: 101 # Nginx的www-data用户的UID是101
runAsGroup: 101 # 对应的GID
这里的runAsUser是关键:哪怕黑客拿到了容器的权限,他用的是普通用户(UID 101),不是root,就算他想改容器里的系统文件,也改不了,因为普通用户没有权限。
3.3 进程隔离的注意事项
- 共享命名空间的场景:比如有些监控业务需要看到宿主机的所有进程,才需要开hostPID,其他业务绝对不能开。
- 普通用户的权限:如果你的业务需要写文件,要给普通用户配置对应的文件权限,比如Nginx需要写日志,那日志目录的权限要改成www-data可写。
四、第三步:CRI-O本身的配置加固——从根源缩攻击面
前面两步是针对业务Pod的配置,接下来要改CRI-O本身的配置,让整个运行时更安全。
4.1 禁用不必要的功能
CRI-O默认开了一些功能,比如允许容器挂载宿主机的文件(bind mount)、允许容器加载内核模块,这些都可以关掉。
咱们看CRI-O的主配置文件(默认路径是/etc/crio/crio.conf),改几个关键的配置:
# 1. 禁用容器加载内核模块的能力(默认是开的)
# 内核模块是宿主机的核心组件,容器绝对不能加载
disable_dev_setgroup = true
# 2. 禁用容器挂载宿主机的/dev目录(默认是开的)
# /dev是宿主机的硬件设备,容器不需要的话就关
devices = []
# 3. 禁用特权模式(默认是允许的)
# 全局禁用特权模式,所有Pod都不能开privileged
allow_privileged = false
# 4. 禁止容器进程修改宿主机的内核参数(默认是允许的)
sysctl_allow_list = []
改完配置后,要重启CRI-O服务生效:
systemctl restart crio
systemctl enable crio
4.2 启用SELinux强化隔离
SELinux是Linux的强制访问控制机制,能给容器进程加额外的权限限制,比如容器进程只能访问自己的文件,不能访问其他容器的。
先检查宿主机的SELinux状态:
sestatus
如果输出的是Enforcing,说明SELinux是开的;如果是Disabled,要改成Enforcing,改完要重启宿主机(生产环境要选维护窗口改)。
然后在CRI-O的配置里启用SELinux:
# 启用SELinux
selinux = true
再改业务Pod的配置,启用SELinux的安全上下文:
apiVersion: v1
kind: Pod
metadata:
name: selinux-nginx
spec:
hostNetwork: false
hostIPC: false
hostPID: false
containers:
- name: nginx
image: nginx:1.25-alpine
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
- SETUID
- SETGID
runAsUser: 101
runAsGroup: 101
# 启用SELinux的安全上下文,容器进程只能访问自己的文件
seLinuxOptions:
level: "s0:c123,c456"
这里的level是SELinux的标签,每个容器用不同的标签,这样容器之间完全不能互相访问。
五、应用场景、优缺点、注意事项总结
5.1 应用场景
- 互联网公司的核心业务集群:比如电商的交易服务、金融的支付服务,这些业务对安全要求极高,必须缩攻击面。
- 云服务商的共享集群:多个租户共用一个K8s集群,必须缩攻击面,防止一个租户的容器被攻破后影响其他租户。
- 合规要求的场景:比如等保2.0、PCI DSS等合规标准,要求容器运行时必须做安全加固。
5.2 技术优缺点
优点
- 大幅降低攻击面:把黑客能利用的路径从几十条缩到几条,甚至一条都没有。
- 符合最小权限原则:只给业务刚好够用的权限,多余的全关掉,从根源上减少风险。
- 兼容性好:这些配置都是K8s和CRI-O的标准配置,不会影响业务的正常运行,只要测试到位。
缺点
- 测试成本高:改配置前要在测试环境跑,确保业务正常,比如裁剪内核能力后,业务会不会报错?跑AsUser后,文件权限会不会有问题?
- 配置复杂:对新手不友好,比如SELinux的标签、内核能力的配置,需要一定的Linux基础。
- 性能损耗:启用SELinux后,容器的进程会有轻微的性能损耗(大概5%以内),对性能要求极高的业务要做测试。
5.3 注意事项
- 所有配置必须先测试:绝对不能直接改生产的配置,先在测试环境跑1-2周,确保业务正常,再推到预生产,最后到生产。
- 定期审计配置:比如每个季度检查一次CRI-O的配置、业务Pod的配置,确保没人改了高危配置(比如开了特权模式)。
- 监控异常:比如监控容器的内核能力、进程的权限,一旦发现有容器开了多余的能力,及时告警。
六、文章总结
缩减CRI-O的攻击面,本质就是“最小权限+完全隔离”:先给容器进程削权限,把没用的内核能力全关掉;再让容器进程和宿主机、其他容器完全隔离开,哪怕黑客拿到了容器权限,也碰不到外面的东西;最后改CRI-O本身的配置,从根源上缩攻击面。
这些配置看起来复杂,但只要一步步来,先测试再上线,就能大幅提升生产环境的安全性,把黑客的攻击路径堵死。
评论
围绕“缩减攻击面:CRI-O在生产环境中的安全加固策略,从内核能力裁剪到进程隔离的具体完整落地实践”参与讨论