裸金属物理机SSL私钥防拖库:TPM2密封+AES加密,Nginx从tmpfs加载

为什么裸金属物理机的SSL私钥更危险?

云服务器有KMS、有VPC隔离,但裸金属物理机你拥有完整硬件,也意味着物理接触、磁盘克隆、备份泄露的风险全在你身上。很多运维把私钥放/etc/nginx/ssl/private.keychmod 600,以为安全。实际上,只要磁盘被挂载(救援模式、磁盘报废、备份被拖),私钥就明文暴露。裸金属物理机通常自带TPM 2.0,这是比软件加密更硬的根信任。

方案:TPM2密封密钥 + AES加密私钥 + tmpfs加载

核心思路:私钥文件在磁盘上永远加密;解密密钥由TPM2密封,只有满足指定PCR(如Secure Boot状态)才能解封;开机时自动解封到tmpfs,Nginx从tmpfs读取私钥。磁盘上只有密文。

1. 确认TPM2可用

ls -l /dev/tpm*
dmesg | grep -i tpm
tpm2_getcap properties-fixed | head -20

如果能看到/dev/tpmrm0,说明TPM2资源管理器可用。轻云互联的裸金属物理机默认开启TPM 2.0且支持Secure Boot,不用进BIOS折腾。

2. 创建TPM2主密钥并密封一个AES密钥

# 生成一个32字节随机密钥
openssl rand -out secret.key 32

# 创建主密钥
tpm2_createprimary -C o -c primary.ctx

# 创建密封对象,绑定PCR 7(Secure Boot状态)
tpm2_createpolicy --policy-pcr -l sha256:7 -L policy.digest -f policy.text
tpm2_create -C primary.ctx -u secret.pub -r secret.priv -L policy.digest -i secret.key -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|decrypt"

# 加载并持久化到句柄0x81000001
tpm2_load -C primary.ctx -u secret.pub -r secret.priv -c secret.ctx
tpm2_evictcontrol -C o -c secret.ctx 0x81000001

注意:PCR 7 只在Secure Boot开启时才稳定。如果BIOS里关闭Secure Boot,PCR 7会变,导致解封失败。所以先在BIOS里开启Secure Boot。

3. 用这个密钥加密SSL私钥

# 假设你的私钥是 /etc/nginx/ssl/private.key
openssl enc -aes-256-cbc -salt -pbkdf2 -in /etc/nginx/ssl/private.key -out /etc/nginx/ssl/private.key.enc -pass file:secret.key

# 删除明文私钥(先备份到安全地方,确认后再删)
shred -u /etc/nginx/ssl/private.key

4. 开机自动解封到tmpfs

写一个systemd服务,在Nginx启动前运行。tmpfs挂载在/dev/shm,重启即消失。

cat > /usr/local/bin/unseal-ssl-key.sh <<'EOF'
#!/bin/bash
set -e
TMP_KEY=/dev/shm/secret.key
TMP_SSL=/dev/shm/private.key
tpm2_unseal -c 0x81000001 > $TMP_KEY
chmod 600 $TMP_KEY
openssl enc -d -aes-256-cbc -pbkdf2 -in /etc/nginx/ssl/private.key.enc -out $TMP_SSL -pass file:$TMP_KEY
chmod 600 $TMP_SSL
rm -f $TMP_KEY
EOF
chmod +x /usr/local/bin/unseal-ssl-key.sh

cat > /etc/systemd/system/unseal-ssl-key.service <<'EOF'
[Unit]
Description=Unseal SSL private key to tmpfs
Before=nginx.service
After=dev-tpmrm0.device

[Service]
Type=oneshot
ExecStart=/usr/local/bin/unseal-ssl-key.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable unseal-ssl-key.service

5. 配置Nginx从tmpfs读取私钥

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /dev/shm/private.key;
    # 其他SSL配置...
}

重启Nginx,验证:

systemctl restart unseal-ssl-key.service
systemctl restart nginx
openssl s_client -connect 127.0.0.1:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject

排错与坑点

  • PCR不匹配:如果更新了BIOS或内核,PCR 7可能变化,导致解封失败。此时需要重新密封。建议保留一个恢复密钥,或者使用tpm2_createpolicy绑定多个PCR(如7+11)。
  • tpm2_unseal权限:如果之前设置了auth,需要tpm2_unseal -c 0x81000001 -p auth。持久化句柄默认需要owner auth,但我们在创建时用了userwithauth,可以空auth。
  • Nginx reload:Nginx reload时不会重新读取私钥,除非重启。所以解封服务在开机时运行一次即可。如果私钥轮转,需要重新加密并重启Nginx。
  • tmpfs大小/dev/shm默认是内存的一半,放私钥足够。但如果系统内存紧张,注意别把大文件放进去。
  • TPM2.0资源管理器:确保/dev/tpmrm0存在,否则tpm2-tools会直接访问/dev/tpm0,可能冲突。

为什么这比直接放磁盘强?

攻击者拿到磁盘,只能看到private.key.enc,没有TPM2和正确的PCR环境无法解封。即使拿到TPM2,PCR策略绑定Secure Boot,篡改引导链会导致解封失败。对于裸金属物理机,这是最低成本的硬件级私钥保护。轻云互联的裸金属物理机自带TPM 2.0,这个方案可以直接落地。

最后,别忘了定期备份TPM的持久化句柄信息?实际上持久化句柄在TPM内部,如果TPM损坏,密钥就丢了。所以建议在创建密封对象时,导出secret.key的备份,用强密码加密后离线保存。或者使用TPM2的备份功能(tpm2_duplicate)复制到另一个TPM。但那是另一个话题了。