在云原生时代,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权限的钥匙挂在门口,还贴上了“欢迎使用”的标签。