背景

一套已经配置好的树莓派系统被完整复制到另一张存储卡上,随后分别在两台树莓派设备上启动。这样做可以节省大量重复配置时间,尤其适合系统中已经预先配置好远程访问、网络脚本、代理服务、Web 服务和若干常驻服务的情况。

不过,完整克隆系统也会带来一个明显问题:两台设备虽然硬件不同,但系统内部身份可能完全相同。

如果不处理,后续可能出现:

主机名重复
machine-id 重复
SSH Host Key 重复
远程访问入口混乱
内网穿透配置冲突
代理节点身份混淆
日志与系统服务识别异常
局域网设备识别混乱

因此,在两台设备正式长期运行前,需要先把系统身份拆分开。

克隆系统后需要处理哪些身份信息

克隆系统后,至少需要检查和处理以下内容:

hostname
/etc/hostname
/etc/hosts
cloud-init 主机名配置
/etc/machine-id
/var/lib/dbus/machine-id
SSH Host Key
远程访问服务配置
内网穿透服务配置
代理服务节点配置

其中,最基础的三项是:

hostname
machine-id
SSH Host Key

这三项分别对应:

设备名称
系统身份
SSH 服务器身份

只改主机名是不够的。
如果 machine-id 和 SSH Host Key 仍然相同,两台设备在系统层面和安全身份层面仍然带有克隆痕迹。

第一次只改主机名为什么失败

一开始使用了常规方式修改主机名:

hostnamectl set-hostname 新主机名

并同步修改:

/etc/hostname
/etc/hosts

当时看起来已经成功,但重启后主机名又恢复成旧值。

这说明问题并不在 hostnamectl 本身,而是系统启动过程中还有其他组件在覆盖主机名。

进一步检查发现,这套系统启用了 cloud-init。
在该类系统中,主机名可能不是只由 /etc/hostname 决定,而是还会受到启动分区中的 cloud-init 配置影响。

关键位置包括:

/boot/firmware/user-data
/etc/cloud/cloud.cfg

如果启动分区的 user-data 中仍然写着旧主机名,同时 cloud-init 没有设置为保留当前主机名,那么重启后系统可能再次把主机名改回旧值。

因此,正确做法不是只改 /etc/hostname,而是要同时处理 cloud-init 的源头配置。

正确修改第一台设备主机名

假设第一台设备的新主机名为:

设备A

在 root 用户下,可以一次性执行:

cp -a /boot/firmware/user-data /boot/firmware/user-data.bak.$(date +%F-%H%M%S)
cp -a /etc/cloud/cloud.cfg /etc/cloud/cloud.cfg.bak.$(date +%F-%H%M%S)
cp -a /etc/hostname /etc/hostname.bak.$(date +%F-%H%M%S)
cp -a /etc/hosts /etc/hosts.bak.$(date +%F-%H%M%S)

sed -i -E 's/^hostname:[[:space:]]*.*/hostname: 设备A/' /boot/firmware/user-data
sed -i -E 's/^preserve_hostname:[[:space:]]*.*/preserve_hostname: true/' /etc/cloud/cloud.cfg

echo 设备A > /etc/hostname
hostnamectl set-hostname 设备A

if grep -qE '^127\.0\.1\.1[[:space:]]+' /etc/hosts; then
  sed -i -E 's/^127\.0\.1\.1[[:space:]].*/127.0.1.1 设备A/' /etc/hosts
else
  echo '127.0.1.1 设备A' >> /etc/hosts
fi

hostname
cat /etc/hostname
hostnamectl
grep -nE 'preserve_hostname|hostname:' /etc/cloud/cloud.cfg /boot/firmware/user-data
grep -nE '127\.0\.1\.1' /etc/hosts

reboot

重启后确认:

hostname
cat /etc/hostname
hostnamectl
grep -nE 'preserve_hostname|hostname:' /etc/cloud/cloud.cfg /boot/firmware/user-data

目标状态是:

hostname 显示为设备A
/etc/hostname 显示为设备A
hostnamectl 中 Static hostname 为设备A
/boot/firmware/user-data 中 hostname 为设备A
/etc/cloud/cloud.cfg 中 preserve_hostname 为 true

这样,主机名才算真正持久修改成功。

重新生成 machine-id

主机名处理完成后,需要重新生成 machine-id。

machine-id 是 systemd 体系中的系统身份标识。
如果两台克隆设备保留相同 machine-id,可能导致 systemd、D-Bus、journald、DHCP 相关身份、日志识别等出现混乱。

在 root 用户下执行:

cp -a /etc/machine-id /root/machine-id.bak.$(date +%F-%H%M%S) 2>/dev/null || true
rm -f /etc/machine-id /var/lib/dbus/machine-id
systemd-machine-id-setup
ln -sf /etc/machine-id /var/lib/dbus/machine-id
cat /etc/machine-id
reboot

重启后确认:

cat /etc/machine-id
ls -l /var/lib/dbus/machine-id
hostnamectl

目标状态是:

/etc/machine-id 已生成新的值
/var/lib/dbus/machine-id 指向 /etc/machine-id
hostnamectl 中显示新的 Machine ID

注意,两台克隆设备的 machine-id 必须不同。

重新生成 SSH Host Key

SSH Host Key 是 SSH 服务器自己的身份密钥。
它不是用户登录用的公钥。

SSH Host Key 通常位于:

/etc/ssh/ssh_host_*

用户登录用的授权公钥通常位于:

/root/.ssh/authorized_keys

或者普通用户的:

~/.ssh/authorized_keys

因此,重新生成 SSH Host Key 不会删除已配置好的登录公钥,也不会破坏公钥登录。
它只会让客户端在下次连接时发现“服务器身份变了”。

在 root 用户下执行:

backup="/root/ssh-hostkey-backup-$(date +%F-%H%M%S)"
mkdir -p "$backup"
cp -a /etc/ssh/ssh_host_* "$backup"/ 2>/dev/null || true
rm -f /etc/ssh/ssh_host_*
ssh-keygen -t rsa -b 8192 -f /etc/ssh/ssh_host_rsa_key -N ''
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ''
chmod 600 /etc/ssh/ssh_host_*_key
chmod 644 /etc/ssh/ssh_host_*_key.pub
systemctl restart ssh
ls -l /etc/ssh/ssh_host_*
systemctl status ssh --no-pager

如果客户端下次连接时提示服务器身份变化,这是正常现象。
在客户端删除旧的 known_hosts 记录后重新连接即可。

示例:

ssh-keygen -R "[远程入口域名]:远程端口"

这里的域名和端口应替换为实际连接入口。公开文章中不应写出真实值。

第二台设备的处理

第二台设备也是从同一份系统复制出来的,所以处理逻辑完全相同。

假设第二台设备的新主机名为:

设备B

root 用户下可以一次性执行:

cp -a /boot/firmware/user-data /boot/firmware/user-data.bak.$(date +%F-%H%M%S)
cp -a /etc/cloud/cloud.cfg /etc/cloud/cloud.cfg.bak.$(date +%F-%H%M%S)
cp -a /etc/hostname /etc/hostname.bak.$(date +%F-%H%M%S)
cp -a /etc/hosts /etc/hosts.bak.$(date +%F-%H%M%S)
cp -a /etc/machine-id /root/machine-id.bak.$(date +%F-%H%M%S) 2>/dev/null || true

sed -i -E 's/^hostname:[[:space:]]*.*/hostname: 设备B/' /boot/firmware/user-data
sed -i -E 's/^preserve_hostname:[[:space:]]*.*/preserve_hostname: true/' /etc/cloud/cloud.cfg

echo 设备B > /etc/hostname
hostnamectl set-hostname 设备B

if grep -qE '^127\.0\.1\.1[[:space:]]+' /etc/hosts; then
  sed -i -E 's/^127\.0\.1\.1[[:space:]].*/127.0.1.1 设备B/' /etc/hosts
else
  echo '127.0.1.1 设备B' >> /etc/hosts
fi

rm -f /etc/machine-id /var/lib/dbus/machine-id
systemd-machine-id-setup
ln -sf /etc/machine-id /var/lib/dbus/machine-id

backup="/root/ssh-hostkey-backup-$(date +%F-%H%M%S)"
mkdir -p "$backup"
cp -a /etc/ssh/ssh_host_* "$backup"/ 2>/dev/null || true
rm -f /etc/ssh/ssh_host_*
ssh-keygen -t rsa -b 8192 -f /etc/ssh/ssh_host_rsa_key -N ''
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ''
chmod 600 /etc/ssh/ssh_host_*_key
chmod 644 /etc/ssh/ssh_host_*_key.pub

hostname
cat /etc/hostname
hostnamectl
cat /etc/machine-id
ls -l /var/lib/dbus/machine-id
ls -l /etc/ssh/ssh_host_*
grep -nE 'preserve_hostname|hostname:' /etc/cloud/cloud.cfg /boot/firmware/user-data
grep -nE '127\.0\.1\.1' /etc/hosts

reboot

重启后确认:

hostname
cat /etc/hostname
hostnamectl
cat /etc/machine-id
ls -l /var/lib/dbus/machine-id
ls -l /etc/ssh/ssh_host_*
grep -nE 'preserve_hostname|hostname:' /etc/cloud/cloud.cfg /boot/firmware/user-data

两台设备最终应满足:

主机名不同
machine-id 不同
SSH Host Key 不同
远程访问入口不同
代理节点身份不同

网络与服务入口的注意事项

两台设备本机监听相同端口并不一定冲突。
因为它们位于不同设备上,各自拥有自己的网络接口和本机端口空间。

真正容易冲突的是远程入口层面的配置,例如:

内网穿透代理名
远程端口
远程域名
子域名
代理节点 ID
客户端节点名称

如果两台设备都连接到同一个远程中转服务,就必须避免使用相同的代理名、远程端口或域名入口。

公开记录中不应写出真实服务名、真实端口、真实域名或真实节点名。
可以统一写成:

远程访问入口A
远程访问入口B
代理节点A
代理节点B
内网穿透规则A
内网穿透规则B

Wi-Fi 与 DHCP 的整理

在其中一台设备上,Wi-Fi 是通过自定义脚本手动连接的。
该脚本大致会完成:

拉起无线网卡
扫描 Wi-Fi
选择 SSID
输入密码
生成临时无线认证配置
启动无线连接进程
启动 DHCP 客户端
调整默认路由优先级

曾经观察到无线网卡上同时存在两个 IPv4 地址。
这通常意味着两个 DHCP 客户端同时管理了同一个接口。

如果后续重新连接后只剩一个 IPv4 地址,并且默认路由正常,则可以暂时不处理。
如果未来要整理,应让同一个网络接口只由一个 DHCP 客户端管理。

操作习惯

本次整理还形成了几个实践习惯:

root 用户下不写 sudo
需要手动编辑时使用 Vim
不要把当前生效和重启后持久生效混为一谈
克隆系统后不要只检查服务是否能跑,还要检查系统身份是否已经拆分

常用确认命令:

hostname
cat /etc/hostname
hostnamectl
cat /etc/machine-id
ls -l /var/lib/dbus/machine-id
ls -l /etc/ssh/ssh_host_*
grep -nE 'preserve_hostname|hostname:' /etc/cloud/cloud.cfg /boot/firmware/user-data

总结

克隆树莓派系统后,不能只改主机名。

更完整的处理流程应该是:

修改 cloud-init 主机名源头
禁止 cloud-init 后续覆盖主机名
修改 /etc/hostname
修改 /etc/hosts
重新生成 machine-id
重新生成 SSH Host Key
区分远程访问入口
区分内网穿透配置
区分代理节点配置

其中最容易忽略的是 cloud-init。
在 Ubuntu Raspberry Pi 系统上,如果启动分区的 user-data 里仍然写着旧主机名,并且 cloud-init 没有设置为保留当前主机名,那么即使 hostnamectl 修改成功,重启后仍可能恢复旧主机名。

因此,克隆系统正式上线前,应先完成系统身份拆分,再处理远程访问和代理服务入口。这样才能避免两台设备在网络、安全身份、日志和远程连接层面互相混淆。

Leave a Reply

Your email address will not be published. Required fields are marked *