在Ubuntu容器宿主机上,安全隔离和资源限制从来不是默认就能做好的事情。很多人以为跑个Docker或者LXC就万事大吉,实际上默认配置下的容器逃逸风险、资源抢占问题比你想象中严重得多。容器共享内核的特性决定了它做不到虚拟机级别的隔离,但通过正确的内核参数、安全模块和资源控制机制,完全可以把风险降到生产可用的程度。
用户命名空间是隔离的第一道防线默认情况下,容器内的root用户和宿主机上的root用户是同一个UID 0,这意味着一旦容器进程突破限制,它拥有的就是宿主机上的root权限。解决这个问题的核心手段是启用用户命名空间。在Ubuntu上,LXD默认启用了用户命名空间,但Docker需要手动配置。编辑/etc/docker/daemon.json,加入userns-remap配置项,Docker会为容器内的root映射一个宿主机上的非特权用户,比如UID 100000起步。这样即使容器内的root执行了恶意操作,在宿主机上也只是个普通用户,连写/etc目录的权限都没有。验证是否生效很简单,在容器内执行whoami显示root,然后在宿主机上用ps aux查看对应进程,你会看到进程的实际用户是dockremap之类的高位UID。
Linux capabilities的精细化裁剪容器默认拥有一组capabilities,但绝大多数应用根本不需要这些权限。比如CAP_SYS_ADMIN这个能力几乎等于半个root,能挂载文件系统、修改内核参数,是容器逃逸的重灾区。你应该在容器启动时明确drop掉所有capabilities,然后只添加必需的。Docker的做法是用--cap-drop=ALL --cap-add=NET_BIND_SERVICE这样的参数,只给容器绑定低端口的权限。对于LXC容器,可以在配置文件中用lxc.cap.drop和lxc.cap.keep来精确控制。一个典型的无状态Web应用容器,保留NET_BIND_SERVICE和CHOWN就足够了,其他全部裁剪掉。这样做的好处不仅是安全,还能防止容器内的进程意外修改系统级配置。
seccomp系统调用过滤的实战配置即使限制了capabilities,容器内的进程仍然可以调用大量系统调用,其中不少是容器场景下完全用不到的,比如mount、kexec_load、reboot这些。seccomp的作用就是定义一个系统调用白名单或黑名单。Docker默认的seccomp配置文件已经禁用了大约44个危险系统调用,但你可以进一步收紧。比如如果你的应用不需要ptrace能力,就把ptrace相关调用也禁掉。自定义seccomp配置文件的写法是JSON格式,列出每个系统调用的名字和动作。对于Go语言编写的应用,通常连clone和unshare的新命名空间创建都可以禁掉,因为Go的并发模型不依赖这些。部署时用--security-opt seccomp=/path/to/profile.json指定自定义文件即可。LXC同样支持seccomp,在配置里用lxc.seccomp.profile指定。
AppArmor强制访问控制的容器化策略Ubuntu默认集成了AppArmor,Docker和LXC都会自动为容器生成一个AppArmor配置文件。但默认配置比较宽松,你可以编写自定义策略来限制容器能访问的文件路径、网络操作等。比如一个只处理HTTP请求的容器,你可以在AppArmor策略里明确禁止它访问/proc/sysrq-trigger、/sys/kernel/debug等敏感路径,禁止它执行/bin/mount、/sbin/iptables等命令。策略文件放在/etc/apparmor.d/目录下,加载后用--security-opt apparmor=your-profile来应用。AppArmor的complain模式很适合调试,先用aa-complain让违规操作只记录不阻止,确认无误后再切换到enforce模式。
cgroup v2的资源硬限制配置Ubuntu 22.04及更高版本默认使用cgroup v2,资源控制的接口统一到了/sys/fs/cgroup下面。CPU限制方面,不要只依赖cpu.shares这种相对权重,因为它在系统空闲时允许容器用满所有CPU,应该同时设置cpu.max来硬限制。比如echo "200000 100000" > cpu.max表示每100毫秒周期内最多使用200毫秒的CPU时间,也就是最多占用2个核心。内存限制更关键,memory.max设置硬上限,memory.high设置软上限,当内存使用超过high值时,系统会开始回收内存但不杀死进程,超过max才会触发OOM。关键是要同时设置memory.swap.max,如果不设置,默认swap是内存的两倍,会导致容器在内存压力下疯狂swap。建议把memory.swap.max设成和memory.max一样大,等于禁用swap,这样行为更可预测。
# 为Docker容器设置cgroup v2资源限制的systemd drop-in配置示例 # /etc/systemd/system/docker.service.d/override.conf [Service] Delegate=yes MemoryAccounting=yes CPUAccounting=yes磁盘I/O和块设备限制的落地方案
容器对磁盘的争抢往往被忽视,但一个容器疯狂写日志能把整个宿主机的磁盘IO打满。cgroup v2的io控制器可以限制每个容器的IOPS和吞吐量。在/sys/fs/cgroup/system.slice/docker-<container-id>.scope/下面,通过io.max文件设置限制。格式是"设备号 rbps=限制 wbps=限制 riops=限制 wiops=限制"。设备号可以用lsblk -o MAJ:MIN查看。比如限制容器对sda盘的写速度为10MB/s,就是"8:0 wbps=10485760"。对于LXC容器,可以直接在配置文件中用lxc.cgroup2.io.max来设置。另外,容器的存储驱动选择也影响隔离性,overlay2配合quota可以做到每个容器层的空间限制,但需要XFS或ext4开启project quota支持。
网络命名空间的深度隔离默认的bridge网络模式下,容器之间可以通过IP直接通信,这在多租户场景下是不可接受的。你应该为每个容器创建独立的网络命名空间,并且通过防火墙规则限制容器间的通信。Docker的--icc=false可以禁止容器间通信,但这只是iptables层面的限制,容器网络接口本身还是能互相看到的。更彻底的做法是使用macvlan或ipvlan驱动,让每个容器直接获得宿主机网络段的独立IP,同时配合宿主机上的iptables或nftables规则,只允许容器访问指定的外部地址。对于需要严格网络隔离的场景,考虑用--network=none启动容器,然后手动配置veth pair和路由,这样你对网络拓扑有完全的控制权。
PID命名空间和进程数限制容器默认共享宿主机的PID命名空间吗?Docker和LXC默认都是隔离的,容器内的PID 1在宿主机上看到的是另一个PID。但很多人不知道的是,容器内的进程仍然可以耗尽宿主机的PID资源。Linux内核的PID最大值默认是32768或更高,一个fork炸弹容器就能让宿主机无法创建新进程。cgroup的pids控制器专门解决这个问题,设置pids.max来限制容器内的进程数。Docker用--pids-limit参数,LXC用lxc.cgroup2.pids.max。建议所有生产容器都设置这个值,根据应用类型,无状态API服务设个200-500就够了,数据库类设1000-2000。
只读根文件系统和tmpfs的正确用法容器的根文件系统应该设为只读,这是防止攻击者修改系统文件或植入后门的基础措施。Docker用--read-only参数,LXC用lxc.rootfs.options=ro。但只读根文件系统会带来一个问题,应用通常需要写临时文件、日志、运行时的pid文件等。这时候就要用tmpfs挂载这些需要写权限的目录。关键是要给tmpfs加上合适的挂载选项,避免它无限膨胀。用--tmpfs /tmp:rw,noexec,nosuid,size=256M这样的参数,既限制了大小,又禁止了执行和suid位。对于日志目录,建议直接输出到stdout/stderr让容器运行时收集,而不是在容器内写文件,这样连tmpfs都不需要。
特权模式的彻底禁用和替代方案任何情况下都不要在生产环境使用--privileged参数,它等于把所有安全机制全部关闭,容器内的root就是宿主机的root。很多教程为了方便让你加--privileged,这是极其危险的做法。如果容器确实需要访问特定设备,用--device参数精确指定设备路径,同时配合cgroup的设备控制器限制访问权限。如果容器需要修改内核参数,用--sysctl单独放开需要的参数,而不是整个/proc/sys可写。需要加载内核模块的场景,应该在宿主机启动时就加载好,容器内只需要使用即可。实在需要特殊权限的场景,拆分成多个单职责容器,每个只给最小权限。
运行时安全监控和异常检测配置好隔离和限制只是第一步,运行时监控同样重要。falco是一个云原生的运行时安全工具,它通过内核模块或eBPF探针监控系统调用,能检测到容器内的异常行为,比如启动shell、修改敏感文件、建立意外网络连接等。在Ubuntu上安装falco后,默认规则集就能覆盖大部分常见攻击模式。配合自定义规则,比如监控特定容器内执行mount系统调用、访问/etc/shadow等操作,可以及时发现入侵行为。日志输出建议接入集中式日志系统,设置告警规则,不要等出了问题才去翻日志。
内核参数加固的checklist宿主机内核参数的配置直接影响所有容器的安全性。vm.swappiness应该设成1而不是默认的60,让内核尽量不swap,因为swap到磁盘的数据可能被其他容器读取。kernel.kptr_restrict=2防止内核指针泄露,kernel.dmesg_restrict=1限制普通用户查看内核日志,net.core.bpf_jit_harden=2加固BPF JIT引擎防止注入攻击。这些参数可以通过/etc/sysctl.d/下的配置文件统一管理,每次宿主机重启后自动生效。对于多租户环境,kernel.unprivileged_userns_clone应该设为0,禁止非特权用户创建新的用户命名空间,这是很多内核提权漏洞的利用路径。
把这些措施组合起来,你的Ubuntu容器宿主机就具备了生产级别的安全隔离和资源控制能力。安全是个持续的过程,定期审计容器配置、更新内核补丁、review运行时日志,才能保持防护的有效性。
