在云原生时代,Ubuntu云镜像启动时,cloud-init是第一个被调用的“初始化管家”。它负责主机名设置、用户创建、SSH密钥注入、磁盘挂载等一系列核心操作。然而,绝大多数运维人员习惯性地只关注“功能是否跑通”,而忽略了cloud-init配置文件本身就是一条极其隐蔽但权限极高的攻击链路。一旦攻击者控制了user-data或meta-data的写入点,或者通过模板注入篡改了cloud-init指令,整台云主机的初始信任链就会在开机瞬间崩塌。
cloud-init的权限模型与攻击面cloud-init以root权限运行,这是它的设计使然,因为它需要修改系统配置、创建用户、写入敏感文件。但这也意味着,任何被cloud-init执行的指令都天然具备最高权限。攻击面主要集中在三个环节:云平台元数据服务(IMDS)的未授权访问、自定义脚本注入、以及配置文件中的明文敏感信息泄露。在Ubuntu系统中,cloud-init的配置通常分布在/etc/cloud/cloud.cfg和/etc/cloud/cloud.cfg.d/目录下,而用户自定义数据则通过云厂商的user-data接口传入。如果云主机内部的防火墙规则没有严格限制对169.254.169.254的访问,任何在主机上拿到低权限shell的攻击者都可以直接查询到包含初始化指令的user-data内容,甚至在某些配置下可以篡改后续的per-boot指令。
加固第一步:锁定元数据服务访问Ubuntu系统默认的iptables或nftables规则并不会阻止非root用户访问链路本地地址169.254.169.254。这是一个必须立即修复的配置缺陷。你可以通过cloud-init本身的bootcmd指令来提前部署防火墙规则,但更安全的做法是在操作系统镜像打包阶段就写入一条udev规则或内核参数。具体操作上,推荐在/etc/cloud/cloud.cfg.d/99-lockdown.cfg中添加如下bootcmd指令,确保在cloud-init初始化网络模块之后、执行用户脚本之前就封锁元数据端点:
#cloud-config bootcmd: - [ iptables, -A, OUTPUT, -d, 169.254.169.254, -p, tcp, --dport, 80, -m, owner, ! --uid-owner, root, -j, DROP ] - [ ip6tables, -A, OUTPUT, -d, fe80::a9fe:a9fe, -p, tcp, --dport, 80, -m, owner, ! --uid-owner, root, -j, DROP ]
这段规则的含义很明确:只有root用户才能访问元数据服务的HTTP端口,其他任何用户尝试访问都会被直接丢弃。需要注意的是,如果你使用的是IMDSv2,还需要额外考虑PUT请求的封锁,因为IMDSv2需要先PUT获取token。但核心思路不变,就是利用owner模块进行进程级别的权限隔离。对于使用nftables的Ubuntu新版本,等效规则也需要同步部署。
加固第二步:禁用不必要的cloud-init模块cloud-init的功能模块非常庞大,从磁盘分区到chef/puppet集成,很多模块在生产环境中根本用不到。每一个启用的模块都是一个潜在的执行单元。Ubuntu的默认配置在/etc/cloud/cloud.cfg中列出了大量模块,你需要根据实际业务需求进行裁剪。例如,如果你不使用Puppet、Chef、Salt这类配置管理工具,就应该在cloud.cfg中明确禁用它们。查看当前启用的模块列表:
cloud-init query --format "'{{.cloud_init_modules}}' '{{.cloud_config_modules}}' '{{.cloud_final_modules}}'"
然后在/etc/cloud/cloud.cfg.d/99-minimal.cfg中覆盖modules配置,只保留核心模块:
#cloud-config cloud_init_modules: - migrator - seed_random - bootcmd - write-files - growpart - resizefs - disk_setup - mounts - set_hostname - update_hostname - update_etc_hosts - users-groups - ssh cloud_config_modules: - ssh-import-id - locale - set-passwords - timezone - ntp - runcmd cloud_final_modules: - package-update-upgrade-install - write-files-deferred - final-message - power-state-change
这个最小化模块列表去掉了phone-home、puppet、chef、mcollective等高风险模块。特别要警惕的是phone-home模块,它会向外部URL回传实例数据,如果在私有云环境中误配置了公网地址,就会造成信息泄露。另外,write-files模块虽然常用,但如果user-data中定义了写入敏感文件的操作,必须配合严格的权限设置。
加固第三步:user-data脚本的完整性校验与最小权限很多团队习惯在user-data中直接写runcmd或shell脚本来完成软件安装和配置下发。这种做法的风险在于,如果云管平台存在越权漏洞,攻击者可以修改其他用户的user-data模板,注入恶意指令。Ubuntu系统上cloud-init执行user-data时不会做任何签名校验,它完全信任传入的数据。因此,你必须在应用层建立一道校验机制。一种可行的方案是在runcmd脚本的开头嵌入一个HMAC签名,然后在脚本内部自行校验:
#cloud-config
runcmd:
- |
#!/bin/bash
EXPECTED_SIGN="a1b2c3d4e5f6..."
PAYLOAD=$(tail -n +3 $0 | head -n -1)
ACTUAL_SIGN=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "your-secret-key" | awk '{print $NF}')
if [ "$ACTUAL_SIGN" != "$EXPECTED_SIGN" ]; then
echo "Integrity check failed. Aborting."
exit 1
fi
# 下面是真正的初始化逻辑
apt-get update && apt-get install -y nginx
这种方式虽然增加了复杂度,但在多租户云环境中,这是防止user-data被篡改的最后一道防线。密钥可以通过云厂商的安全参数存储服务注入,不要硬编码在脚本中。另外,runcmd中的所有命令都应该遵循最小权限原则,避免使用curl | bash这种管道执行方式,因为中间人攻击或源站被黑都会导致任意代码执行。
加固第四步:敏感信息的硬编码清理cloud-init配置文件中最常见的安全问题就是明文密码、API密钥、数据库连接串的直接出现。在/etc/cloud/cloud.cfg和user-data中,ssh_pwauth如果设置为true并且明文指定了密码,那么任何能读取这些文件的人都能直接拿到root密码。即使你使用chpasswd模块设置了加密密码,SHA-512哈希值本身也可能被离线破解。正确的做法是彻底禁用密码登录,只使用SSH密钥认证。在cloud.cfg中强制写入:
#cloud-config
ssh_pwauth: false
password: RANDOM
chpasswd:
expire: true
users:
- name: ubuntu
ssh-authorized-keys:
- ssh-rsa AAAAB3NzaC1... your-public-key
sudo: ['ALL=(ALL) NOPASSWD:ALL']
lock_passwd: true
对于必须传递的API密钥或证书,应该使用cloud-init的write_files模块配合permissions参数,确保文件只能被特定用户读取,并在使用后通过post-run指令删除:
#cloud-config
write_files:
- path: /etc/secrets/api.key
content: |
sk-xxxxxxxxxxxxxxxxxxxx
permissions: '0600'
owner: root:root
runcmd:
- /usr/local/bin/consume-secret.sh
- shred -u /etc/secrets/api.key
这里用shred而不是简单的rm,是为了覆盖磁盘上的数据块,防止被恢复。对于特别敏感的密钥,建议直接使用云厂商的密钥管理服务,通过实例角色临时获取,不要在cloud-init中传递任何长期凭证。
加固第五步:日志脱敏与审计cloud-init的执行日志默认存放在/var/log/cloud-init.log和/var/log/cloud-init-output.log中。这两个文件是排查初始化问题的关键,但也是敏感信息泄露的重灾区。user-data中的脚本输出、错误信息、甚至某些变量值都会被原样记录。Ubuntu系统默认的日志权限是0644,这意味着任何登录系统的用户都可以读取。你需要修改cloud-init的日志输出级别,并在生产环境中将日志文件权限收紧。在/etc/cloud/cloud.cfg.d/99-logging.cfg中配置:
#cloud-config output: all: '>> /var/log/cloud-init-output.log' init: '>> /var/log/cloud-init.log' config: '>> /var/log/cloud-init.log' final: '>> /var/log/cloud-init.log'
然后通过bootcmd或系统服务将日志文件权限改为0600:
chmod 0600 /var/log/cloud-init*.log
更彻底的做法是将cloud-init日志重定向到内存文件系统,在初始化完成后直接丢弃。但这会影响故障排查效率,需要在安全性和可运维性之间权衡。另外,如果你的合规要求严格,应该将cloud-init日志接入集中式日志平台,并配置告警规则,监控异常的runcmd执行、非预期的模块加载、以及元数据服务的非法访问尝试。
加固第六步:网络与存储阶段的安全控制cloud-init在网络配置阶段同样存在风险。如果攻击者能够通过DHCP响应注入恶意DNS服务器地址,或者通过云平台的网络配置接口篡改虚拟网卡的属性,cloud-init会毫无怀疑地应用这些配置。在Ubuntu系统中,你可以在/etc/cloud/cloud.cfg.d/99-network-lockdown.cfg中强制指定DNS服务器,忽略DHCP下发的DNS:
#cloud-config
network:
version: 2
ethernets:
eth0:
dhcp4: true
dhcp4-overrides:
use-dns: false
nameservers:
addresses: [8.8.8.8, 1.1.1.1]
对于挂载的存储卷,cloud-init的mounts模块会自动处理/etc/fstab条目。如果云平台允许用户挂载快照或共享存储,必须确保文件系统在挂载前经过fsck检查,并且挂载选项中使用noexec、nosuid、nodev来限制可执行文件和特殊权限位的生效。在user-data中定义挂载时,显式指定这些安全选项:
#cloud-config mounts: - [ "/dev/xvdf", "/data", "ext4", "defaults,noexec,nosuid,nodev", "0", "2"]加固第七步:实例终止前的清理与响应
云主机的生命周期结束时,cloud-init不会自动执行清理操作。这意味着磁盘快照或镜像中可能残留着初始化阶段写入的敏感信息。你应该利用cloud-init的power-state-change模块,在实例关机或终止前触发一个清理脚本。在/etc/cloud/cloud.cfg.d/99-cleanup.cfg中配置:
#cloud-config power_state: delay: "+5" mode: poweroff message: "System is powering down for cleanup" timeout: 30 condition: True
然后在runcmd或bootcmd中注册一个systemd服务,监听关机信号,执行密钥销毁、日志清除、磁盘擦写等操作。这个服务必须在cloud-init完成所有初始化步骤后才能启动,确保不会误删正在使用的文件。
总结:将安全嵌入初始化基因Ubuntu系统cloud-init的安全加固不是一个一次性的配置动作,而是需要贯穿镜像制作、实例启动、运行监控、终止清理的全生命周期。核心原则只有三条:最小化模块和权限、校验一切外部输入、脱敏所有持久化输出。当你把这几条原则落实到cloud.cfg.d目录下的每一个配置片段中,云主机的初始信任基座才会真正牢固。在云原生攻击手法日益精细化的今天,忽视cloud-init的安全配置,无异于把服务器root权限的钥匙挂在门口,还贴上了“欢迎使用”的标签。
