写在前面 很多人觉得全栈工程师懂得前端和后端就好了,更资深一些的人知道测试也很重要。但我认为全栈工程师最重要的能力是——部署。
部署是上线前的最后一步,每一项配置都直接关系到网络安全。就算你系统写得再固若金汤,也扛不住黑客入侵服务器后的为所欲为。
集群架构 1. 一个现代服务的最小系统是什么 一个服务的最小系统由前端、后端、数据库三者构成
2. 服务会怎么“暴毙” 前端和后端作为暴露在公网的接口,首当其冲成为攻击目标。而黑客的攻击大致可分为三类:一是注入攻击,比如大名鼎鼎的SQL注入;二是流量窃听,比如通过抓包获取到用户名和密码;三是直接入侵服务器,是最严重的安全事故。
这里就引出了两个概念,一是WAF (Web Application Firewall),这类程序通过代理前端流量来拦截大多数注入攻击,属于傻瓜式一键部署,但是能让你晚上睡个安稳觉。二是加密传输 ,我们通过 TLS/SSL 加密将数据混淆,即使攻击者截获数据包,也无法解密出有用的信息。大家最熟悉的方案就是 HTTPS,最简单的实现方式就是给 Nginx 配置 SSL 证书来代理流量。
那么配置好 WAF 之后,如果黑客绕过 WAF 直接访问源站怎么办?那么,防黑客最有效的方法是什么?防火墙?程序写得足够健壮?110?都不是,防止黑客最有效的方法就是不联网。那你可能要问——不联网,服务怎么对外提供访问?前面其实已经回答了:让 WAF 代理流量,把前端和后端隐藏在内网之中!
这个时候我们就需要网络隔离,将非必要的服务彻底断绝外网。
前端和后端作为服务提供方,交由 WAF 代理流量;而数据库仅与后端通信,因此完全 可以切断外网 。
回到数据库本身——即使服务代码写得固若金汤,物理硬件仍可能因不可抗力而损毁。那么这个时候我就想到了我们小学二年级信息老师都讲过的(毕导乱入)数据的”3-2-1”原则:至少三份副本、存放于两种不同介质、其中一份为异地备份 。所以我们需要将数据库中的数据进行异地备份。
那么现在我们的节点就从三个增加到了五个,即:
flowchart LR
extNet[fa:fa-globe 外部访问]
subgraph dmz[DMZ 隔离区]
waf[fa:fa-shield WAF]
end
subgraph intranet[内网]
subgraph app[应用层]
fe[fa:fa-desktop 前端]
be[fa:fa-server 后端]
end
subgraph data[数据层]
db[(fa:fa-database 数据库)]
bak[(fa:fa-cloud 异地备份)]
end
end
extNet <==>|"HTTPS"| waf
waf -->|"反向代理"| fe
waf -->|"反向代理"| be
fe <-->|"TLS 加密"| be
be -->|"CRUD"| db
db -.->|"定期备份"| bak
3. 完全地……断开公网?絶対。 经常被打的同学都知道,漏洞是防不住的,对付漏洞最有效的方式就是持续更新补丁,但更新就需要连接外网。那我们已经被水泥焊死在内网里的服务们怎么办,软件源从哪来?DNS 解析从哪来?时间校准 NTP 从哪来?服务们吃什么?是啊,吃什么?
噢对了,还有 TLS 证书。内网服务之间要走 HTTPS,但断网之后 Let’s Encrypt 之类的公共 CA 就用不了了——你需要一台内部 CA 来签发证书,这里我们用 step-ca 。
还有一个更深的问题:就算把 DNS / NTP / 软件源 / CA 都自建好了,这些服务自己也需要联网——NTP 要同步上游授时、Docker 源要从 Docker Hub 拉镜像、Yum 源要从上游仓库同步软件包。难道给每台 infra 节点都配一个公网出口?那前面的网络隔离就白做了。
企业级的做法是:只在 DMZ 里放一台正向代理 (Squid),由它统一出站,其他 infra 节点通过 HTTP_PROXY 走代理访问外网。代理本身配置白名单 ACL,只允许 NTP、DNS、Yum、Docker 等必需协议出站,其余一律拒绝。
那么这些不得不联网的基础设施怎么解决呢,那当然是自己 搭了。我去,不早说!
于是我们的拓扑就变成了这样:
flowchart TB
extNet[fa:fa-globe 外部访问]
subgraph dmz[DMZ 隔离区]
waf[fa:fa-shield WAF]
proxy[fa:fa-forward Squid 代理]
end
subgraph intranet[内网]
subgraph app[应用层]
fe[fa:fa-desktop 前端]
be[fa:fa-server 后端]
end
subgraph data[数据层]
db[(fa:fa-database 数据库)]
bak[(fa:fa-cloud 异地备份)]
end
subgraph infra[基础设施]
dns[fa:fa-sitemap DNS]
ntp[fa:fa-clock NTP]
ca[fa:fa-certificate step-ca]
subgraph repo[软件源]
docker[(fa:fa-box Docker源)]
yum[(fa:fa-archive Yum源)]
end
end
end
extNet <==>|"HTTPS"| waf
proxy -->|"出站(白名单 ACL)"| extNet
waf -->|"反向代理"| fe
waf -->|"反向代理"| be
fe <-->|"TLS 加密"| be
be -->|"CRUD"| db
db -.->|"定期备份"| bak
infra -.->|"NTP / DNS"| waf
infra -.->|"HTTP_PROXY 代理出站"| proxy
4. 管理网 好了,现在你有一套固若金汤的服务器集群了,可以开始维护了。
等等……你问我内网的机器怎么登录进去维护?
text 1 2 3 1. 拿着你的笔记本走进机房,记得穿大棉袄 2. 用RJ45接口的超六类双绞线连接内网交换机 3. 去HR领离职金
这太蠢了不是吗。所以怎么在家里也能加班为公司发光发热?答案是再组一套管理网 。
管理网(Management Network)是一个独立于业务网络的管理通道,专门用于运维人员登录、监控和维护内网服务器。它和业务流量走的不是同一条路——用户通过 WAF 访问服务,运维通过管理网访问服务器,互不干扰。
管理网的核心组件有两个:
VPN 网关 :在你的笔记本和公司内网之间建立一条加密隧道,让你在公网上也能”虚拟”地接入内网。常用的有 WireGuard、OpenVPN、IPsec。
堡垒机(Bastion Host / Jump Server) :管理网的唯一入口。所有对内网服务器的 SSH 连接都必须经由堡垒机跳转,它负责认证、授权、操作审计。一句话——想进内网,先过堡垒机。
那么管理网的访问链路是:
flowchart LR
admin[fa:fa-user 运维人员]
subgraph mgmt[管理区]
vpn[fa:fa-lock VPN 网关]
bastion[fa:fa-terminal 堡垒机]
end
subgraph intranet[内网]
fe[fa:fa-desktop 前端]
be[fa:fa-server 后端]
db[(fa:fa-database 数据库)]
end
admin <==>|"VPN 加密隧道"| vpn
vpn -->|"SSH"| bastion
bastion -->|"SSH"| fe
bastion -->|"SSH"| be
bastion -->|"SSH"| db
运维人员先拨入 VPN 进入管理区,再通过堡垒机跳转到内网各台服务器。堡垒机是唯一的入口,所有操作日志集中留存,出了事知道谁干的。
把业务网和管理网合在一起,完整的拓扑如下:
flowchart TB
extNet[fa:fa-globe 外部访问]
admin[fa:fa-user 运维人员]
subgraph dmz[DMZ 隔离区]
waf[fa:fa-shield WAF]
proxy[fa:fa-forward Squid 代理]
vpn[fa:fa-lock VPN 网关]
end
subgraph intranet[内网]
subgraph app[应用层]
fe[fa:fa-desktop 前端]
be[fa:fa-server 后端]
end
subgraph data[数据层]
db[(fa:fa-database 数据库)]
bak[(fa:fa-cloud 异地备份)]
end
subgraph infra[基础设施]
dns[fa:fa-sitemap DNS]
ntp[fa:fa-clock NTP]
ca[fa:fa-certificate step-ca]
subgraph repo[软件源]
docker[(fa:fa-box Docker源)]
yum[(fa:fa-archive Yum源)]
end
end
subgraph mgmt[管理网]
bastion[fa:fa-terminal 堡垒机]
end
end
extNet <==>|"HTTPS"| waf
admin <==>|"VPN 加密隧道"| vpn
proxy -->|"出站(白名单 ACL)"| extNet
waf -->|"反向代理"| fe
waf -->|"反向代理"| be
vpn -->|"SSH"| bastion
bastion -.->|"SSH 管理"| fe
bastion -.->|"SSH 管理"| be
bastion -.->|"SSH 管理"| db
fe <-->|"TLS 加密"| be
be -->|"CRUD"| db
db -.->|"定期备份"| bak
infra -.->|"NTP / DNS"| waf
infra -.->|"HTTP_PROXY 代理出站"| proxy
到了这一步,整张拓扑就完整了——业务流量 走 WAF 反向代理进入内网,管理流量 走 VPN + 堡垒机独立通道,两套网络职责分离,谁也别越界。
练习案例 俗话说”读万卷书,行万里路”。道理讲了一堆,不动手过一遍很难真正记住。下面就来和我一起做一个图书管理系统的部署案例。
实验环境:VMware Workstation Pro + OpenEuler 24.04 LTS SP3 。
准备网卡 基于上面的拓扑,可以划分出五个网络:
网络名
用途
公网
模拟外部访问
DMZ 隔离区
部署 WAF / VPN / Squid 代理
业务内网
前端、后端、数据库、异地备份
管理网
运维人员 VPN 拨入后的管理通道
基础设施网
DNS / NTP / step-ca / 软件源
对应到 VMware 中,需要配置五张虚拟网卡:
VMnet
类型
网段
用途
VMnet8
NAT
192.168.70.0/24
模拟公网(外部访问)
VMnet2
仅主机(Host-only)
172.16.1.0/24
DMZ 隔离区
VMnet3
仅主机(Host-only)
172.16.2.0/24
业务内网
VMnet4
仅主机(Host-only)
172.16.3.0/24
管理网
VMnet5
仅主机(Host-only)
172.16.4.0/24
基础设施网
每个节点的规划 模板机就绪后,根据前面的拓扑,规划出每个节点的资源配置和所属网络。下面是一张完整的节点清单,供有基础的同学快速对照:
域
节点
vCPU
内存
硬盘
网络配置
DMZ隔离区
dmz-waf (WAF)
2 核
4 GB
系统盘
VMnet2(172.16.1.20) VMnet3(172.16.2.10) VMnet4(172.16.3.30) VMnet5(172.16.4.30) VMnet8(192.168.70.128)
dmz-proxy(Squid正向代理)
1 核
2 GB
系统盘
VMnet2(172.16.1.21) VMnet4(172.16.3.31) VMnet5(172.16.4.10) VMnet8(192.168.70.129)
基础设施
infra-dns(DNS)
1核
2GB
系统盘
VMnet5(172.16.4.20) VMnet4(172.16.3.32)
infra-ntp(ntp)
1核
2GB
系统盘
VMnet5(172.16.4.21) VMnet4(172.16.3.33)
infra-docker(本地docker仓库)
2核
4GB
系统盘 仓库盘:200GB×1
VMnet5(172.16.4.22) VMnet4(172.16.3.34)
infra-yum(本地Yum源)
2核
4GB
系统盘 仓库盘:200GB×1
VMnet5(172.16.4.23) VMnet4(172.16.3.35)
infra-ca(step-ca)
1核
2GB
系统盘
VMnet5(172.16.4.24) VMnet4(172.16.3.36)
业务内网
app-frontend(前端)
2核
4GB
系统盘
VMnet3(172.16.2.20) VMnet4(172.16.3.37) VMnet5(172.16.4.31)
app-backend(后端)
2核
4GB
系统盘
VMnet3(172.16.2.21) VMnet4(172.16.3.38) VMnet5(172.16.4.32)
数据层
data-db(数据库)
2核
4GB
系统盘 数据盘:50GB×5(raid5×4,热备份×1)
VMnet3(172.16.2.22) VMnet4(172.16.3.39) VMnet5(172.16.4.33)
data-backup(异地备份)
1核
2GB
系统盘 数据盘:50GB×5(raid5×4,热备份×1)
VMnet3(172.16.2.23) VMnet4(172.16.3.40) VMnet5(172.16.4.34)
管理网
mgmt-vpn(WireGuard VPN)
2核
2GB
系统盘
VMnet4(172.16.3.20) VMnet2(172.16.1.22) VMnet5(172.16.4.35) VMnet8(192.168.70.130)
mgmt-bastion(堡垒机)
2核
2GB
系统盘
VMnet4(172.16.3.21) VMnet5(172.16.4.36)
内网网络地址标准:
.10 源头
.20 本域
.30+ 用户
模板机 网卡配置好之后,下一步就是装系统了。
首先我们先装 DNS 主机:配置虚拟硬件、装系统、更新软件、开启 BBR、部署服务……
然后是 NTP 主机:配置虚拟硬件、装系统、更新软件、开启 BBR、部署服务……
再然后是……
打住。打开你的 VMware,随便找一个虚拟机,右键 → 管理 → 看到那个大大的”克隆”了吗?我们完全可以装一台机器作为模板 ,后面有新节点时直接克隆就好,省掉一遍遍重复劳动。下面来说说模板机怎么装。
虚拟硬件配置
配置项
说明
vCPU
2 核
内存
4 GB,关闭 swap
网卡
五张虚拟网卡全部挂载:VMnet8(公网)、VMnet2(DMZ)、VMnet3(业务)、VMnet4(管理)、VMnet5(基础设施),VMware 里按此顺序加好
存储
60 GB
CD/DVD
挂载 OpenEuler 安装镜像,安装后移除
UEFI
必须开启 (UEFI 引导更贴合云服务环境)这一步非常容易忘
声卡、USB 控制器等无用设备全部移除,模板机不需要这些。
磁盘分区 安装程序走到磁盘分区这一步,选择手动分区。OpenEuler 走 UEFI 引导,所以需要一个 EFI 分区。整体使用 LVM 方案,方便后续扩容:
挂载点
大小
文件系统
说明
/boot/efi
512M
FAT32
UEFI 引导必需
/boot
1G
XFS
内核和引导文件
/ (LVM)
30G
XFS
系统根分区
/var (LVM)
15G
XFS
日志和可变数据,独立分区防止写满 / 导致宕机
/tmp (LVM)
5G
XFS
临时文件,挂载选项 noexec,nodev,nosuid
剩余约 8.5G 空间留在 LVM 卷组中不分配,后续哪个分区不够了可以直接 lvextend。
注意 :/tmp 的挂载选项很关键——noexec 禁止执行可执行文件,能挡住一大票通过临时目录投递的恶意脚本。
配置模板机 系统装好之后,还需要做一些标准化配置,这样克隆出来的节点不用每次都重新折腾。
0. 固定网卡命名
模板机挂了五张网卡,克隆出来的机器也一样——但问题是刚开机时你根本分不清 ens32 / ens34 / ens35 / ens36 / ens37 分别对应哪张网络。靠 MAC 地址去 VMware 里对也行,但每次克隆完都来一遍太蠢了。
更优雅的做法:模板机里一次性写死 udev 规则,按照 PCI 路径 给网卡重命名。PCI 路径在克隆后不变,所以克隆机开机就能看到 eth-pub、eth-dmz 这样有意义的接口名。
首先把所有网卡切成 DHCP,拿到 IP 就能确定映射关系(VMnet8 NAT 有 DHCP,其他 Host-only 网络也默认启用了):
1 2 3 4 5 6 7 8 for iface in $(ls /sys/class/net | grep -E "^e" ); do sudo nmcli dev set "$iface " managed yes 2>/dev/null sudo nmcli con add con-name "dhcp-$iface " ifname "$iface " type ethernet 2>/dev/null sudo nmcli con up "dhcp-$iface " 2>/dev/null done sleep 5ip -br addr show | grep -E "^e"
根据拿到的 IP 段就能确定映射:
IP 段
对应网络
192.168.70.x
VMnet8 公网
172.16.1.x
VMnet2 DMZ
172.16.2.x
VMnet3 业务
172.16.3.x
VMnet4 管理
172.16.4.x
VMnet5 基础设施
然后查出每张网卡的 PCI 路径:
1 2 3 4 for iface in $(ls /sys/class/net | grep -E "^e" ); do pci=$(basename "$(readlink /sys/class/net/$iface/device) " 2>/dev/null) echo "$iface → $pci " done
输出类似:
1 2 3 4 5 ens33 → pci-0000:02:01.0 # VMnet8 → eth-pub ens34 → pci-0000:02:02.0 # VMnet2 → eth-dmz ens35 → pci-0000:02:03.0 # VMnet3 → eth-app ens36 → pci-0000:02:04.0 # VMnet4 → eth-mgmt ens37 → pci-0000:02:05.0 # VMnet5 → eth-infra
如果你在 VMware 里挂网卡的顺序不一样,实际的 PCI 路径也会不同。对照 DHCP 拿到的 IP 段修改下面 udev 规则里的 PCI 路径即可。
写 udev 规则——用 PCI 路径,不要用 MAC 地址 (MAC 克隆后会变,PCI 路径不变):
1 2 3 4 5 6 7 8 9 sudo tee /etc/udev/rules.d/70-vmnet-names.rules << 'EOF' SUBSYSTEM=="net" , ACTION=="add" , DEVPATH=="*/0000:02:00.0/net/*" , NAME="eth-pub" SUBSYSTEM=="net" , ACTION=="add" , DEVPATH=="*/0000:02:01.0/net/*" , NAME="eth-dmz" SUBSYSTEM=="net" , ACTION=="add" , DEVPATH=="*/0000:02:02.0/net/*" , NAME="eth-app" SUBSYSTEM=="net" , ACTION=="add" , DEVPATH=="*/0000:02:03.0/net/*" , NAME="eth-mgmt" SUBSYSTEM=="net" , ACTION=="add" , DEVPATH=="*/0000:02:04.0/net/*" , NAME="eth-infra" EOF
命名用 eth- 前缀加网络用途后缀,一眼看出这是哪张网,不用再去记 ensXX 映射表。
删掉 NetworkManager 里的 DHCP 连接,重启让 udev 规则生效:
1 2 sudo rm -f /etc/NetworkManager/system-connections/*.nmconnectionsudo reboot
重启后 ip link 看到的就是 eth-pub、eth-dmz、eth-app、eth-mgmt、eth-infra 了。现在只有本地回环和这些裸接口,没网。给 eth-pub 开 DHCP 恢复上网:
1 2 sudo nmcli con add con-name "dhcp-pub" ifname eth-pub type ethernetsudo nmcli con up "dhcp-pub"
后面配模板机的步骤都走 eth-pub 这条线。
1. 加固 /tmp 挂载选项
默认 /tmp 没有安全限制。编辑 /etc/fstab,找到 /tmp 所在行,补齐挂载选项:
1 2 /dev/mapper/openeuler-tmp /tmp xfs noexec,nodev,nosuid 0 0
然后重新挂载使其生效:
1 2 3 4 5 systemctl daemon-reload mount -o remount /tmp
2. 创建 deploy 组和 deploy 用户
生产环境不要用 root 直接操作,建一个专门的部署运维账号:
1 2 3 sudo groupadd deploysudo useradd -m -g deploy -s /bin/bash deploysudo passwd deploy
把 deploy 加入 wheel 组以便 sudo:
1 sudo usermod -aG wheel deploy
生产环境建议把 sudo 权限收敛到具体命令上(visudo 配 Cmnd_Alias),模板机这里先简化处理。
3. SSH 安全加固
用密钥登录替代密码登录。先把你的公钥写入 deploy 用户的 authorized_keys:
1 2 3 4 5 sudo mkdir -p /home/deploy/.sshsudo vi /home/deploy/.ssh/authorized_keys sudo chmod 700 /home/deploy/.sshsudo chmod 600 /home/deploy/.ssh/authorized_keyssudo chown -R deploy:deploy /home/deploy/.ssh
然后新建 SSH 配置片段,禁用密码认证和 root 登录,只允许 deploy 用户密钥登录:
1 2 sudo mkdir -p /etc/ssh/sshd_config.d/sudo vi /etc/ssh/sshd_config.d/99-hardening.conf
1 2 3 4 5 PasswordAuthentication no PermitRootLogin no PubkeyAuthentication yes AllowUsers deploy
检查一下配置语法无报错后重启 sshd:
1 sudo sshd -t && sudo systemctl restart sshd
注意 :公钥一定要在这步之前放进去,否则 PasswordAuthentication no + AllowUsers deploy 生效后你就彻底登不上去了。
4. 更新系统并安装基础包
1 2 sudo dnf update -ysudo dnf install -y vim curl wget tar lsof jq bind-utils chrony tcpdump open-vm-tools
这边建议重启一下,然后卸载旧内核
5. 安装 Docker 和 Docker Compose
所有服务都跑在容器里,模板机先装好 Docker。OpenEuler 没有官方 Docker 源,用 CentOS 的源并强制指定为 el8 兼容版本:
1 2 3 4 5 sudo curl -o /etc/yum.repos.d/docker-ce.repo https://download.docker.com/linux/centos/docker-ce.reposudo sed -i 's/rhel/centos/g' /etc/yum.repos.d/docker-ce.reposudo sed -i 's/$releasever/8/g' /etc/yum.repos.d/docker-ce.reposudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-pluginsudo systemctl enable --now docker
docker-compose-plugin 提供 docker compose(V2),不是旧版 docker-compose(V1)。注意中间是空格不是横杠。
把 deploy 用户加入 docker 组,避免每次 docker 命令都要 sudo:
1 sudo usermod -aG docker deploy
6. 修复 Docker 与 firewalld 的冲突
OpenEuler 的 firewalld 默认使用 nftables 作为后端,而 Docker 直接操作 iptables。两者各自管理防火墙规则,经常出现 firewalld 重载后 Docker 容器的网络全部挂掉的情况(容器 ping 不通外网、发布的端口失效)。
解决方案是让 firewalld 退回 iptables 后端,这样两者操作同一套规则体系,不再互相覆盖。
编辑 /etc/firewalld/firewalld.conf,找到 FirewallBackend 行改为:
1 sudo vim /etc/firewalld/firewalld.conf
1 2 FirewallBackend =iptables
有了vim就是舒服,还是这个好使,哦哦齁齁齁齁齁齁齁
然后确保 Docker daemon 显式开启 iptables 管理:
1 sudo vim /etc/docker/daemon.json
重启顺序很重要——必须先启 firewalld,再启 Docker,这样 Docker 的 iptables 规则才能正确追加到 firewalld 已有的规则之后:
1 2 sudo systemctl restart firewalldsudo systemctl restart docker
验证一下:
1 2 sudo firewall-cmd --state docker run --rm alpine ping -c1 baidu.com
上一步把 deploy 加入了 docker 组,需要刷新登录会话才生效。OpenEuler 有个坑——newgrp 甚至重新登录后 groups 仍然可能显示旧信息,用 exec su -l $USER 强制重建登录会话即可解决。
7. 关闭 GRUB 等待
服务器不需要双系统菜单,把 GRUB 等待时间改为 0,开机直接进系统:
1 2 3 4 sudo vim /etc/default/grubsudo grub2-mkconfig -o /boot/grub2/grub.cfg
8. 清理、关机、拍快照
克隆前最后一步:清理敏感信息,避免每台机器残留相同的机器 ID 和网络标识:
1 2 3 4 5 6 sudo rm -f /etc/machine-idsudo touch /etc/machine-idsudo chmod 444 /etc/machine-idsudo rm -f /etc/ssh/ssh_host_*sudo sed -i '/^UUID/d' /etc/NetworkManager/system-connections/*.nmconnection 2>/dev/null
/etc/machine-id 清空后 systemd 会在下次启动时重新生成;SSH 主机密钥同理,克隆后首次启动会自动创建新的。
关机:
关机后在 VMware 里拍一个快照,命名为 basic ——这是模板机的干净基线,后面如果配置搞砸了可以直接回滚到这里。
后续流程:用 basic 快照克隆出基础设施节点并部署完毕 → 回到模板机,配置它使用基础设施服务(DNS、NTP、CA 证书、Docker 镜像源、Yum 源)→ 拍第二张快照 after-infra 。之后克隆业务节点和管理节点时从 after-infra 起手,就不用每台机器重复配一遍基础设施了。
基础设施节点 要想让我们这一套集群跑起来,直觉上要先搭建基础设施 节点,然而基础设施节点有没有依赖呢,有的,所以我们要部署的第一个节点是squid正向代理
Squid 正向代理(dmz-proxy) 1. 从basic 快照链接克隆一台新机器,按上表补足硬件配置后开机。dmz-proxy 不需要业务网卡,先把 eth-app 对应的网卡在 VMware 设置里移除。开机后配置 IP:
1 2 3 4 5 6 7 8 9 10 11 for iface in eth-pub eth-dmz eth-app eth-mgmt eth-infra; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-dmz" ifname eth-dmz type ethernet ipv4.method manual ipv4.addresses 172.16.1.21/24sudo nmcli con add con-name "net-pub" ifname eth-pub type ethernet ipv4.method manual ipv4.addresses 192.168.70.129/24 ipv4.gateway 192.168.70.2 ipv4.dns "223.5.5.5" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet ipv4.method manual ipv4.addresses 172.16.3.31/24sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet ipv4.method manual ipv4.addresses 172.16.4.10/24sudo nmcli dev set eth-app managed off
最后设置主机名:
1 sudo hostnamectl set-hostname dmz-proxy
2. 创建工作目录和 docker-compose.yml。cache 和 logs 落在宿主机 /var 下(模板机 /var 独立分区,防止缓存写满根目录):
1 2 3 4 sudo mkdir -p /opt/squid/configsudo mkdir -p /var/spool/squid /var/log/squidsudo vi /opt/squid/docker-compose.ymlsudo chmod 755 /opt/squid/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 services: squid: image: ubuntu/squid:latest container_name: squid restart: unless-stopped ports: - "172.16.4.10:3128:3128" volumes: - /opt/squid/config/squid.conf:/etc/squid/squid.conf:ro - /var/log/squid:/var/log/squid - /var/spool/squid:/var/spool/squid ulimits: nofile: soft: 65535 hard: 65535 networks: - proxy-net healthcheck: test: ["CMD" , "sh" , "-c" , "curl -x http://localhost:3128 -s -o /dev/null -w '%{http_code} ' http://baidu.com | grep -qE '^(2|3|4)'" ] interval: 30s timeout: 10s retries: 3 networks: proxy-net: driver: bridge
端口绑在 172.16.4.10 上,只有基础设施网的节点能访问 3128。
容器内 squid 以 proxy 用户运行(Debian/Ubuntu 固定 UID=13),bind mount 的宿主目录需要改属主才能写入:
1 2 3 4 sudo docker run --rm --entrypoint "" ubuntu/squid:latest id proxysudo chown -R 13:13 /var/spool/squid /var/log/squid
3. 编写 squid.conf——白名单 ACL,只放行 Yum 和 Docker 所需的出站协议:
1 sudo vim /opt/squid/config/squid.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 http_port 3128 # === 源 IP 白名单 === acl infra_net src 172.16.4.0/24 acl dmz_net src 172.16.1.0/24 # === 安全端口:只允许 HTTP(80) 和 HTTPS(443) === acl SSL_ports port 443 acl Safe_ports port 80 acl Safe_ports port 443 acl Safe_ports port 1025-65535 acl CONNECT method CONNECT # === 出站目的域名白名单 === # Yum 源(USTC 镜像站) acl allowed_dst dstdomain .ustc.edu.cn # Docker Hub + 私有仓库 acl allowed_dst dstdomain .docker.com .docker.io git.msksbr.com # GitHub(step CLI 等工具链——下载重定向到 githubusercontent.com) acl allowed_dst dstdomain .github.com .githubusercontent.com # NTP 和 DNS 是 UDP,不走 HTTP 代理,留给 iptables SNAT 处理 # === 规则生效顺序 === http_access deny !Safe_ports http_access allow infra_net CONNECT SSL_ports allowed_dst http_access allow infra_net allowed_dst http_access allow dmz_net CONNECT SSL_ports allowed_dst http_access allow dmz_net allowed_dst http_access deny all # === 文件描述符硬限制(Docker 容器内 ulimit -n 可能为极大值导致 OOM)=== max_filedescriptors 65535 # === 缓存 === cache_dir ufs /var/spool/squid 100 16 256 maximum_object_size 1024 MB cache_mem 256 MB coredump_dir /var/spool/squid # === 日志 === access_log daemon:/var/log/squid/access.log squid cache_log /var/log/squid/cache.log # === 隐藏 Squid 版本号 === httpd_suppress_version_string on
NTP(UDP 123)和 DNS(UDP 53)不走 HTTP 代理,因此不在这里配置。infra-ntp 和 infra-dns 的出站走 dmz-proxy 本身的防火墙 SNAT,后续部署这两个节点时会配。
4. 启动并验证:
1 2 cd /opt/squiddocker compose up -d
dmz-proxy 自身测试代理是否可用(172.16.4.10 绑的就是本机):
1 curl -x http://172.16.4.10:3128 -I https://mirrors.ustc.edu.cn
返回 HTTP 200 说明代理正常工作。docker logs squid -f 可以看到实时请求日志。
5. Squid 跑通后锁门——配置 firewalld,只允许基础设施网访问 3128,管理网 SSH,其余一律拒绝:
1 2 3 4 5 6 7 8 9 10 11 12 sudo firewall-cmd --permanent --change-interface=eth-pub --zone=publicsudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="tcp" port="3128" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
public 区默认只允许出站,不用额外配——proxy 需要对外发包,正是这个行为。
PowerDNS 权威 DNS(infra-dns) 在开始部署 DNS 之前,先在 dmz-proxy 上打开 NAT 转发——DNS 和 NTP 都是 UDP 协议,不走 HTTP 代理,但需要经 proxy 出站访问上游(如 223.5.5.5):
0. dmz-proxy 开启 IP 转发和 NAT:
1 2 3 4 5 6 7 8 9 echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-forwarding.confsudo sysctl -p /etc/sysctl.d/99-forwarding.confsudo firewall-cmd --permanent --zone=public --add-masqueradesudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i eth-infra -o eth-pub -j ACCEPTsudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i eth-pub -o eth-infra -m state --state RELATED,ESTABLISHED -j ACCEPTsudo firewall-cmd --reload
验证:在其他 infra 节点上把默认网关指向 proxy(172.16.4.10)、DNS 设为 223.5.5.5,ping 223.5.5.5 能通即表示 NAT 生效。
1. 从 basic 快照链接克隆 infra-dns,移除 eth-dmz/eth-app/eth-pub 对应的网卡(只保留 eth-infra + eth-mgmt),开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 12 13 for iface in eth-infra eth-mgmt; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.20/24 \ ipv4.gateway 172.16.4.10 ipv4.dns "223.5.5.5" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.32/24 sudo hostnamectl set-hostname infra-dns
2. 创建目录和 docker-compose.yml。数据库和日志落在 /var 分区:
1 2 3 4 5 sudo mkdir -p /opt/pdns/configsudo mkdir -p /var/lib/powerdns /var/log/pdnssudo chown -R 953:953 /var/lib/powerdns /var/log/pdnssudo vim /opt/pdns/docker-compose.ymlsudo chmod 755 /opt/pdns/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 services: pdns-auth: image: powerdns/pdns-auth-48 container_name: pdns-auth restart: unless-stopped volumes: - /opt/pdns/config/pdns.conf:/etc/powerdns/pdns.conf:ro - /var/lib/powerdns:/var/lib/powerdns - /var/log/pdns:/var/log/pdns networks: - dns-net pdns-recursor: image: powerdns/pdns-recursor-53 container_name: pdns-recursor restart: unless-stopped ports: - "172.16.4.20:53:53/tcp" - "172.16.4.20:53:53/udp" volumes: - /opt/pdns/config/recursor.yml:/etc/powerdns/recursor.yml:ro - /var/log/pdns:/var/log/pdns networks: - dns-net networks: dns-net: driver: bridge
Auth 不暴露任何端口——由 Recursor 接收 DNS 查询(53),内部域转发给 Auth,外网递归上游。所有管理操作通过 docker compose exec pdns pdnsutil 直接在容器内执行。Auth 容器内 pdns 用户 UID=953(Alpine 固定值)。
3. 编写 Auth 配置 pdns.conf——SQLite3 后端:
1 sudo vim /opt/pdns/config/pdns.conf
1 2 3 4 5 6 7 8 9 10 launch =gsqlite3gsqlite3-database =/var/lib/powerdns/pdns.sqlite3default-soa-content =ns1.internal.msksbr.com hostmaster.internal.msksbr.com 0 10800 3600 604800 3600 setgid =pdnssetuid =pdns
编写 Recursor 配置 recursor.yml:
1 sudo vim /opt/pdns/config/recursor.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 incoming: listen: - 0.0 .0 .0 :53 allow_from: - 172.16 .0 .0 /16 recursor: system_resolver_ttl: 3600 forward_zones: - zone: internal.msksbr.com forwarders: - pdns-auth:53 forward_zones_recurse: - zone: "." forwarders: - 223.5 .5 .5 dnssec: validation: off logging: quiet: true
allow-from 允许所有内网段递归查询。DNSSEC 关闭——内网 zone 未签名,开启会导致 SERVFAIL。
4. 初始化 SQLite 数据库——PowerDNS 不会自动建 schema,需要手动执行:
1 2 3 4 5 6 7 8 9 cd /opt/pdnssudo docker run --rm \ -v /var/lib/powerdns:/var/lib/powerdns \ --entrypoint /bin/sh powerdns/pdns-auth-48 \ -c "sqlite3 /var/lib/powerdns/pdns.sqlite3 < /usr/local/share/doc/pdns/schema.sqlite3.sql" sudo chown 953:953 /var/lib/powerdns/pdns.sqlite3sudo docker compose up -dsleep 3
1 2 sudo docker compose exec pdns-auth pdnsutil create-zone internal.msksbr.com ns1.internal.msksbr.com
1 2 3 4 5 6 7 8 9 10 11 12 13 14 sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. proxy A 172.16.4.10sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. dns A 172.16.4.20sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. ntp A 172.16.4.21sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. docker A 172.16.4.22sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. yum A 172.16.4.23sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. ca A 172.16.4.24sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. waf A 172.16.3.30sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. frontend A 172.16.2.20sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. backend A 172.16.2.21sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. db A 172.16.2.22sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. backup A 172.16.2.23sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. vpn A 172.16.3.20sudo docker compose exec pdns-auth pdnsutil add-record internal.msksbr.com. bastion A 172.16.3.21
5. 验证 DNS 解析:
1 2 3 4 dig @172.16.4.20 proxy.internal.msksbr.com +short dig @172.16.4.20 baidu.com +short
内网返回 172.16.4.10,外网返回百度 IP,说明 Auth + Recursor 配对正常工作。其他节点只需把 DNS 指到 172.16.4.20,内外网解析一站搞定,不需要 /etc/resolv.conf 里配双 DNS。
6. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 11 12 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="tcp" port="53" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="udp" port="53" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
NTP(infra-ntp) NTP 不走容器——chronyd 需要 SYS_TIME capability 才能调整系统时钟,容器化要么给 --privileged 要么放弃。宿主机直接跑最稳妥。
1. 从 basic 快照链接克隆 infra-ntp,移除 eth-dmz/eth-app/eth-pub(只保留 eth-infra + eth-mgmt),开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 for iface in eth-infra eth-mgmt; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.21/24 \ ipv4.gateway 172.16.4.10 ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.33/24 sudo hostnamectl set-hostname infra-ntp
DNS 指向刚部署的 infra-dns(172.16.4.20),内网域名和外网递归一站解决。
2. 配置 chronyd——上游授时走阿里云和腾讯云 NTP,监听 infra 口为内网提供服务:
chronyd 在模板机阶段已安装(dnf install -y chrony),直接编辑配置:
1 2 sudo cp /etc/chrony.conf{,.bak}sudo vim /etc/chrony.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pool ntp.aliyun.com iburst pool ntp.tencent.com iburst bindaddress 172.16.4.21 allow 172.16.0.0/16 local stratum 10 logdir /var/log/chrony log measurements statistics tracking makestep 1.0 3
makestep 1.0 3:前 3 次更新若偏移 >1 秒则直接跳变,之后走 slewing 缓慢修正,避免日志时间线倒退。
3. 启动并验证:
1 2 3 4 sudo systemctl enable chronydsudo systemctl restart chronyd chronyc sources -v chronyc tracking
sources -v 看到上游带 ^* 标记说明已同步。tracking 确认 Stratum 不再为 0。
本机验证:
4. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="udp" port="123" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
chronyd 出站同步上游走 proxy NAT,internal zone 默认放行出站,无需额外规则。
5. 跨节点验证(firewalld 放行之后才能通):
1 2 3 4 sudo systemctl stop chronydsudo chronyd -q "server 172.16.4.21 iburst" sudo systemctl start chronyd
无报错即为 NTP 服务可达。
step-ca 内部 CA(infra-ca) 内网服务之间走 HTTPS 需要 TLS 证书。断网后 Let’s Encrypt 等公共 CA 不可用,自建一台内部 CA 用 ACME 协议签发。
1. 从 basic 快照链接克隆 infra-ca,移除 eth-dmz/eth-app/eth-pub(只保留 eth-infra + eth-mgmt),开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 for iface in eth-infra eth-mgmt; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.24/24 \ ipv4.gateway 172.16.4.10 ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.36/24 sudo hostnamectl set-hostname infra-ca
2. 创建目录和 docker-compose.yml:
1 2 3 4 5 6 sudo mkdir -p /opt/step-ca/configsudo mkdir -p /opt/step-ca/secretssudo mkdir -p /opt/step-ca/certssudo mkdir -p /var/lib/step-ca /var/log/step-casudo vim /opt/step-ca/docker-compose.ymlsudo chmod 755 /opt/step-ca/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 services: step-ca: image: smallstep/step-ca:latest container_name: step-ca restart: unless-stopped ports: - "172.16.4.24:9000:9000" volumes: - /opt/step-ca/config:/home/step/config:ro - /opt/step-ca/secrets:/home/step/secrets - /opt/step-ca/certs:/home/step/certs:ro - /var/lib/step-ca:/home/step/db - /var/log/step-ca:/var/log/step networks: - ca-net networks: ca-net: driver: bridge
3. 初始化 CA——生成根证书、中间证书和 provisioner:
1 2 openssl rand -base64 24 | sudo tee /opt/step-ca/secrets/password > /dev/null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 sudo docker run --rm \ --user 0 \ -v /opt/step-ca:/home/step \ smallstep/step-ca:latest \ step ca init \ --name "Internal CA" \ --dns ca.internal.msksbr.com \ --address :9000 \ --provisioner admin \ --password-file /home/step/secrets/password sudo mv /opt/step-ca/db /var/lib/step-casudo docker run --rm --entrypoint "" smallstep/step-ca:latest id stepsudo chown -R 1000:1000 /opt/step-ca/secrets /opt/step-ca/certs /var/lib/step-ca /var/log/step-ca
--dns ca.internal.msksbr.com 用于证书中包含 CA 的域名。--provisioner admin 创建一个名为 admin 的 provisioner,后续用 step ca certificate 签发节点证书时需提供对应密码。
4. 启动并验证:
1 2 cd /opt/step-cadocker compose up -d
验证 CA 健康状态和根证书指纹:
1 2 docker compose exec step-ca step ca health docker compose exec step-ca step certificate fingerprint /home/step/certs/root_ca.crt
签发一张测试证书验证全链路:
1 2 3 4 cat /opt/step-ca/secrets/password | docker compose exec -T step-ca \ step ca certificate test.internal.msksbr.com /tmp/test.crt /tmp/test.key \ --provisioner admin --password-file /dev/stdin docker compose exec step-ca step certificate inspect /tmp/test.crt --short
step ca certificate 走 ACME 协议向 CA 签发证书,--provisioner admin 指定认证身份,密码走 stdin 传入。无报错且 DNS name 为 test.internal.msksbr.com 即全链路通过。
5. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="tcp" port="9000" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
Docker Registry 双容器方案(infra-docker) 两个 registry:2 各司其职:
端口
模式
用途
5000
标准读写
Gitea 私有镜像 cron 同步后推入
5001
pull-through proxy
代理 Docker Hub,registry-mirrors 指向这里
集群节点只需配一个 registry-mirrors,Docker Hub 走 5001 透明代理,私有镜像走 5000 本地存储。互不干扰。
1. 从 basic 快照链接克隆 infra-docker,移除 eth-dmz/eth-app/eth-pub(只保留 eth-infra + eth-mgmt)。额外挂一块 200GB 数据盘。开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 for iface in eth-infra eth-mgmt; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.22/24 \ ipv4.gateway 172.16.4.10 ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.34/24 sudo hostnamectl set-hostname infra-docker
2. 200GB 数据盘走 LVM,挂载到 /var/lib/registry:
1 2 3 4 5 6 7 8 9 lsblk sudo pvcreate /dev/sdbsudo vgcreate vg_registry /dev/sdbsudo lvcreate -l 100%FREE -n lv_registry vg_registrysudo mkfs.xfs /dev/vg_registry/lv_registrysudo mkdir -p /var/lib/registrysudo mount /dev/vg_registry/lv_registry /var/lib/registryecho '/dev/vg_registry/lv_registry /var/lib/registry xfs defaults 0 0' | sudo tee -a /etc/fstab
LVM 方便后续在线扩容——缓存满时加一块盘 vgextend → lvextend → xfs_growfs,不停服务。
3. 创建目录和配置文件:
1 sudo mkdir -p /opt/registry/{config,proxy}
标准 registry 配置文件(5000,接受 push):
1 2 sudo vim /opt/registry/config/main.ymlsudo chmod 644 /opt/registry/config/main.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 version: "0.1" log: level: info storage: filesystem: rootdirectory: /var/lib/registry maintenance: uploadpurging: enabled: true age: 168h interval: 24h dryrun: false http: addr: :5000
proxy registry 配置文件(5001,只读代理 Docker Hub):
1 2 sudo vim /opt/registry/config/proxy.ymlsudo chmod 644 /opt/registry/config/proxy.yml
1 2 3 4 5 6 7 8 9 10 version: "0.1" log: level: info storage: filesystem: rootdirectory: /var/lib/registry-proxy http: addr: :5001 proxy: remoteurl: https://registry-1.docker.io
5000 没有 proxy 配置——标准可读可写。5001 配了 proxy.remoteurl——自动变成 pull-through cache,本地有缓存直接返回,没有则代理到 Docker Hub 并缓存。
docker-compose.yml:
1 2 sudo vim /opt/registry/docker-compose.ymlsudo chmod 755 /opt/registry/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 services: registry-main: image: registry:2 container_name: registry-main restart: unless-stopped ports: - "172.16.4.22:5000:5000" volumes: - /opt/registry/config/main.yml:/etc/docker/registry/config.yml:ro - /var/lib/registry/main:/var/lib/registry networks: - registry-net registry-proxy: image: registry:2 container_name: registry-proxy restart: unless-stopped ports: - "172.16.4.22:5001:5001" environment: - HTTP_PROXY=http://172.16.4.10:3128 - HTTPS_PROXY=http://172.16.4.10:3128 - NO_PROXY=172.16.0.0/16,localhost volumes: - /opt/registry/config/proxy.yml:/etc/docker/registry/config.yml:ro - /var/lib/registry/proxy:/var/lib/registry networks: - registry-net healthcheck: test: ["CMD" , "sh" , "-c" , "wget -q -O /dev/null http://localhost:5001/v2/ || exit 1" ] interval: 30s timeout: 10s retries: 3 networks: registry-net: driver: bridge
只有 registry-proxy 需要 HTTP_PROXY——它要出站访问 Docker Hub。registry-main 不走代理,只在内网收 push / 响应 pull。
创建两个容器的存储子目录:
1 sudo mkdir -p /var/lib/registry/{main,proxy}
4. 私有镜像同步脚本:
git.msksbr.com/msksbr/* 上的私有镜像,定时从 Gitea 拉取并推入 5000:
1 2 3 sudo mkdir -p /opt/registry/sync.dsudo vim /opt/registry/sync.d/sync-gitea.shsudo chmod +x /opt/registry/sync.d/sync-gitea.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/bash set -eGITEA_UPSTREAM="git.msksbr.com" LOCAL_REGISTRY="172.16.4.22:5000" IMAGES=("bookmgr:latest" "bookmgr-client:latest" ) for img in "${IMAGES[@]} " ; do echo "=== syncing ${img} ===" docker pull "${GITEA_UPSTREAM} /msksbr/${img} " docker tag "${GITEA_UPSTREAM} /msksbr/${img} " "${LOCAL_REGISTRY} /msksbr/${img} " docker push "${LOCAL_REGISTRY} /msksbr/${img} " echo "=== done ${img} ===" done
后续有新的私有镜像,加到 IMAGES 数组里即可。
部署 systemd timer 每晚凌晨 3 点自动同步:
1 sudo vim /etc/systemd/system/sync-gitea.service
1 2 3 4 5 6 7 8 9 [Unit] Description =Sync Gitea images to local registryAfter =docker.serviceRequires =docker.service[Service] Type =on eshotUser =deployExecStart =/bin/bash /opt/registry/sync.d/sync-gitea.sh
1 sudo vim /etc/systemd/system/sync-gitea.timer
1 2 3 4 5 6 7 8 9 [Unit] Description =Sync Gitea images daily at 3 am[Timer] OnCalendar =dailyPersistent =true [Install] WantedBy =timers.target
1 2 sudo systemctl daemon-reloadsudo systemctl enable --now sync-gitea.timer
Persistent=true:如果凌晨 3 点机器没开机,下次启动时立刻补跑一次。
5. 启动并验证:
1 2 3 cd /opt/registrydocker compose up -d sleep 3
Docker daemon 默认对远程 registry 走 HTTPS,内网 registry 还没配 TLS(统一后面加),先标记 5000 为 insecure。5001 不需要——它是 registry-mirrors 的目标,daemon 对 mirror 容忍 HTTP:
1 sudo vim /etc/docker/daemon.json
1 2 3 4 { "iptables" : true , "insecure-registries" : [ "172.16.4.22:5000" , "172.16.4.22:5001" ] }
1 sudo systemctl restart docker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 docker ps --format "table {{.Names}}\t{{.Status}}" docker pull 172.16.4.22:5001/library/nginx:alpine docker rmi 172.16.4.22:5001/library/nginx:alpine docker pull 172.16.4.22:5001/library/nginx:alpine sudo systemctl start sync-gitea.servicesudo journalctl -u sync-gitea.service -fcurl -s http://172.16.4.22:5000/v2/_catalog curl -s http://172.16.4.22:5001/v2/_catalog
6. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 11 12 13 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="tcp" port="5000" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="tcp" port="5001" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
本地 Yum 源(infra-yum) nginx:alpine 托管 RPM 静态文件,宿主机 reposync + createrepo_c 定时从 USTC 同步。只保留最新版本,--delete 自动清过期包,200GB 绰绰有余。
1. 从 basic 快照链接克隆 infra-yum,移除 eth-dmz/eth-app/eth-pub(只保留 eth-infra + eth-mgmt)。额外挂一块 200GB 数据盘。开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 for iface in eth-infra eth-mgmt; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.23/24 \ ipv4.gateway 172.16.4.10 ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.35/24 sudo hostnamectl set-hostname infra-yum
2. 200GB 数据盘走 LVM,挂载到 /var/www/repos:
1 2 3 4 5 6 7 8 lsblk sudo pvcreate /dev/sdbsudo vgcreate vg_yum /dev/sdbsudo lvcreate -l 100%FREE -n lv_yum vg_yumsudo mkfs.xfs /dev/vg_yum/lv_yumsudo mkdir -p /var/www/repossudo mount /dev/vg_yum/lv_yum /var/www/reposecho '/dev/vg_yum/lv_yum /var/www/repos xfs defaults 0 0' | sudo tee -a /etc/fstab
3. 安装同步工具,创建目录:
1 2 sudo dnf install -y dnf-utils createrepo_csudo mkdir -p /opt/yum
4. nginx 容器托管静态文件:
1 2 sudo vim /opt/yum/docker-compose.ymlsudo chmod 755 /opt/yum/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 services: nginx: image: nginx:alpine container_name: yum-nginx restart: unless-stopped ports: - "172.16.4.23:80:80" volumes: - /opt/yum/nginx.conf:/etc/nginx/conf.d/default.conf:ro - /var/www/repos:/var/www/repos:ro networks: - yum-net networks: yum-net: driver: bridge
1 sudo vim /opt/yum/nginx.conf
1 2 3 4 5 6 server { listen 80 ; server_name _; root /var/www/repos; autoindex on ; }
1 2 cd /opt/yumdocker compose up -d
5. 配置上游 USTC 源并写同步脚本:
先把 basic 快照残留的默认源全部清掉:
1 sudo rm -f /etc/yum.repos.d/*.repo
OpenEuler 的四个仓库直连 USTC,debuginfo / source / update-source 全部跳过——运行时不需要调试符号和源码:
1 sudo vim /etc/yum.repos.d/ustc.repo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [openeuler-os] name =OpenEuler OS (USTC)baseurl =https://mirrors.ustc.edu.cn/openeuler/openEuler-24.03 -LTS-SP3/OS/x86_64 /enabled =1 gpgcheck =0 [openeuler-everything] name =OpenEuler Everything (USTC)baseurl =https://mirrors.ustc.edu.cn/openeuler/openEuler-24.03 -LTS-SP3/everything/x86_64 /enabled =1 gpgcheck =0 [openeuler-epol] name =OpenEuler EPOL (USTC)baseurl =https://mirrors.ustc.edu.cn/openeuler/openEuler-24.03 -LTS-SP3/EPOL/main/x86_64 /enabled =1 gpgcheck =0 [openeuler-update] name =OpenEuler Update (USTC)baseurl =https://mirrors.ustc.edu.cn/openeuler/openEuler-24.03 -LTS-SP3/update/x86_64 /enabled =1 gpgcheck =0
Docker CE 不在 USTC 上,按模板机的方式从官方获取 repo 文件——跟模板机装 Docker 时一模一样:
1 2 3 sudo curl -o /etc/yum.repos.d/docker-ce.repo https://download.docker.com/linux/centos/docker-ce.reposudo sed -i 's/rhel/centos/g' /etc/yum.repos.d/docker-ce.reposudo sed -i 's/$releasever/8/g' /etc/yum.repos.d/docker-ce.repo
两套 repo 文件、两个上游(USTC + Docker 官方),reposync 会遍历所有 enabled 的 repo,统一拉进本地 nginx 托管。集群节点只需配一个本地源地址就能同时拿到系统包和 Docker 包。
同步脚本:
1 2 sudo vim /opt/yum/sync.shsudo chmod +x /opt/yum/sync.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #!/bin/bash set -eexport HTTP_PROXY=http://172.16.4.10:3128export HTTPS_PROXY=http://172.16.4.10:3128REPO_BASE=/var/www/repos declare -A REPOSREPOS[openeuler-os]="${REPO_BASE} /openeuler-os" REPOS[openeuler-everything]="${REPO_BASE} /openeuler-everything" REPOS[openeuler-epol]="${REPO_BASE} /openeuler-epol" REPOS[openeuler-update]="${REPO_BASE} /openeuler-update" REPOS[docker-ce-stable]="${REPO_BASE} /docker-ce" for repoid in "${!REPOS[@]} " ; do dest="${REPOS[$repoid]} " echo "=== syncing ${repoid} ===" mkdir -p "$dest " reposync --repoid="$repoid " \ --newest-only \ --delete \ --download-metadata \ --download-path="$dest " createrepo_c --update "$dest " echo "=== done ${repoid} ===" done
--download-metadata 会下载上游的 repodata/,但 createrepo_c --update 之后会重生成,确保索引一致。--newest-only 是精简要害——只保留每个包的最新一个版本,不然全版本同步轻松破 500GB。
首次同步耗时较长,先手动跑一把:
1 2 sudo mkdir -p /var/log/yumsudo /opt/yum/sync.sh 2>&1 | sudo tee /var/log/yum/sync.log
部署 systemd timer,每周日凌晨 2 点自动同步:
1 sudo vim /etc/systemd/system/sync-yum.service
1 2 3 4 5 6 7 8 9 [Unit] Description =Sync Yum repos from USTCWants =network-on line.target[Service] Type =on eshotExecStart =/bin/bash /opt/yum/sync.shStandardOutput =append:/var/log/yum/sync.logStandardError =append:/var/log/yum/sync.log
1 sudo vim /etc/systemd/system/sync-yum.timer
1 2 3 4 5 6 7 8 9 [Unit] Description =Sync Yum repos weekly on Sunday 2 am[Timer] OnCalendar =Sun *-*-* 02 :00 :00 Persistent =true [Install] WantedBy =timers.target
1 2 sudo systemctl daemon-reloadsudo systemctl enable --now sync-yum.timer
每周同步一次足够——系统更新不用追着上游跑,月底巡检时一起跑就行。Persistent=true:如果周日凌晨机器没开,下次开机立刻补跑。
6. 验证:
1 2 3 4 5 6 7 8 9 10 curl -s -o /dev/null -w '%{http_code}' http://172.16.4.23/ curl -s http://172.16.4.23/ | head -20 sudo mv /etc/yum.repos.d/ustc.repo /tmp/sudo vim /etc/yum.repos.d/local.repo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 [openeuler-os] name =Local OpenEuler OSbaseurl =http://172.16 .4.23 /openeuler-osenabled =1 gpgcheck =0 [openeuler-everything] name =Local OpenEuler Everythingbaseurl =http://172.16 .4.23 /openeuler-everythingenabled =1 gpgcheck =0 [openeuler-epol] name =Local OpenEuler EPOLbaseurl =http://172.16 .4.23 /openeuler-epolenabled =1 gpgcheck =0 [openeuler-update] name =Local OpenEuler Updatebaseurl =http://172.16 .4.23 /openeuler-updateenabled =1 gpgcheck =0 [docker-ce] name =Local Docker CEbaseurl =http://172.16 .4.23 /docker-ceenabled =1 gpgcheck =0
1 2 sudo dnf clean allsudo dnf install -y htop
htop 装完无误后把 ustc.repo 恢复——infra-yum 自己仍需要 USTC 作为上游才能 reposync:
1 2 3 sudo mv /tmp/ustc.repo /etc/yum.repos.d/sudo rm /etc/yum.repos.d/local.reposudo dnf clean all
infra-yum 节点本身的 dnf install 仍然走 USTC(它自己就是源服务器,不需要绕一圈拉自己的源)。集群里其他节点配本地源——这个统一在 after-infra 快照环节处理。
7. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="tcp" port="80" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
统一配置 TLS and after-infra 基础设施全部就绪后,把根证书分发到所有节点并逐个信任,然后在 infra-docker 和 infra-yum 上通过 ACME 在本机签发各自的服务证书,配置自动续签。最后回到模板机接入所有基础设施、拍 after-infra 快照。
之前的做法是在 infra-ca 上手工签发证书、再把 cert + key 通过物理机 scp 到目标节点。但这样一来,每加一个新服务就要来回传文件;step-ca 默认证书有效期只有 24h,总不能每天手动续签一遍。
正确的做法是借助 ACME:让每个节点自己向 CA 签发并续签证书。step-ca 原生支持 ACME 协议,只需把 step CLI 装到各个节点上即可。
分发根证书(一次性) 先在物理机上从 infra-ca 拉取根证书,再推到所有节点。这一步只做一次——后续由 step CLI 走 ACME 自动签发,无需再传文件。
在物理机 上:
1 2 3 4 5 6 scp deploy@172.16.3.36:/opt/step-ca/certs/root_ca.crt . for ip in 31 32 33 34 35 36; do scp root_ca.crt deploy@172.16.3.$ip :/tmp/ done
infra-ca 节点上根证书已在 /opt/step-ca/certs/ 但未做系统级信任,一样要跑 cp + update-ca-trust。模板机留到 after-infra 再处理。
每个节点上执行系统级信任:
1 2 sudo cp /tmp/root_ca.crt /etc/pki/ca-trust/source/anchors/sudo update-ca-trust
验证——任意节点上能通过 HTTPS 访问 CA:
1 2 curl -s -o /dev/null -w '%{http_code}' https://ca.internal.msksbr.com:9000/health
infra-docker 启用 TLS 证书由 infra-ca 签发,域名 docker.internal.msksbr.com 在 DNS 里已经指向 172.16.4.22。两个 registry 容器共享同一张证书(端口不同不区分)。
0. 把 step RPM 纳入本地 Yum 源:
先把 step CLI 的 RPM 拉到 infra-yum 本地源里,这样所有节点都能 dnf install -y step,不用每台都 curl。
在 infra-yum 上:
1 2 3 4 cd /opt/yumsudo mkdir -p /var/www/repos/stepsudo vim sync-step.shsudo chmod +x sync-step.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #!/bin/bash set -eREPO_DIR="/var/www/repos/step" LOG_FILE="/var/log/yum-sync.log" export https_proxy=http://172.16.4.10:3128LATEST=$(curl -s https://api.github.com/repos/smallstep/cli/releases/latest \ | grep '"tag_name"' | cut -d'"' -f4) VERSION="${LATEST#v} " RPM_FILE="step-cli-${VERSION} -1.x86_64.rpm" DOWNLOAD_URL="https://github.com/smallstep/cli/releases/download/${LATEST} /${RPM_FILE} " if [ -f "${REPO_DIR} /${RPM_FILE} " ]; then echo "[$(date) ] step CLI ${VERSION} already synced, skip." | tee -a "$LOG_FILE " exit 0 fi curl -Lo "/tmp/${RPM_FILE} " "$DOWNLOAD_URL " sudo mv "/tmp/${RPM_FILE} " "$REPO_DIR /" sudo find "$REPO_DIR " -name 'step-cli-*.rpm' ! -name "$RPM_FILE " -deletesudo createrepo_c --update "$REPO_DIR " echo "[$(date) ] step CLI ${VERSION} synced." | tee -a "$LOG_FILE "
LATEST 走 GitHub API 拿最新 tag,VERSION=${LATEST#v} 剥掉前缀 v。find ... -delete 清理旧 RPM 只保留最新版,createrepo_c --update 增量更新元数据。
加到 cron 每周同步一次,追新版本自动入库:
1 2 3 sudo tee /etc/cron.d/step-sync << 'EOF' 0 5 * * 1 root /opt/yum/sync-step.sh EOF
然后回到各节点的 Yum 配置里补上 step repo。会后文模板机 after-infra 的 local.repo 会一并加上;这里 infra-docker 和 infra-yum 自己先单独加:
1 2 3 4 5 6 7 sudo tee /etc/yum.repos.d/step.repo << 'EOF' [step] name=Local Step CLI baseurl=http://172.16.4.23/step enabled=1 gpgcheck=0 EOF
此时 infra-yum 还没上 TLS,URL 用 http://172.16.4.23。等后面 infra-yum 切到 HTTPS 后,再改回 https://yum.internal.msksbr.com/step。
1 2 sudo dnf makecachesudo dnf install -y step-cli
1. Bootstrap——连接 CA:
1 2 3 4 5 FP=$(step certificate fingerprint /etc/pki/ca-trust/source/anchors/root_ca.crt) step ca bootstrap --ca-url https://ca.internal.msksbr.com:9000 --fingerprint "$FP "
step ca bootstrap 连上 CA 后下载根证书并写入本地配置。之后 step ca certificate 和 step ca renew 会自动走这个 CA URL,无需每次指定。
2. 签发证书——在 infra-docker 本机执行:
签发证书需要 provisioner 密码,先从 infra-ca 取过来。在物理机 上:
1 2 scp deploy@172.16.3.36:/opt/step-ca/secrets/password ./ca-password scp ca-password deploy@172.16.3.34:/tmp/
回到 infra-docker :
1 2 3 4 5 6 7 8 9 10 11 12 sudo mkdir -p /opt/registry/certsstep ca certificate docker.internal.msksbr.com \ /opt/registry/certs/docker.crt \ /opt/registry/certs/docker.key \ --provisioner admin --password-file /tmp/ca-password sudo chmod 600 /opt/registry/certs/docker.keysudo chmod 644 /opt/registry/certs/docker.crtrm -f /tmp/ca-password
step ca certificate 走 ACME 向 CA 签发证书,--provisioner admin 指明认证身份。证书直接生成在 infra-docker 本机,无需物理机中转。
3. 更新 docker-compose.yml——两个容器各加 TLS 环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 services: registry-main: image: registry:2 container_name: registry-main restart: unless-stopped ports: - "172.16.4.22:5000:5000" environment: - REGISTRY_HTTP_TLS_CERTIFICATE=/certs/docker.crt - REGISTRY_HTTP_TLS_KEY=/certs/docker.key volumes: - /opt/registry/config/main.yml:/etc/docker/registry/config.yml:ro - /var/lib/registry/main:/var/lib/registry - /opt/registry/certs:/certs:ro networks: - registry-net registry-proxy: image: registry:2 container_name: registry-proxy restart: unless-stopped ports: - "172.16.4.22:5001:5001" environment: - REGISTRY_HTTP_TLS_CERTIFICATE=/certs/docker.crt - REGISTRY_HTTP_TLS_KEY=/certs/docker.key - HTTP_PROXY=http://172.16.4.10:3128 - HTTPS_PROXY=http://172.16.4.10:3128 - NO_PROXY=172.16.0.0/16,localhost volumes: - /opt/registry/config/proxy.yml:/etc/docker/registry/config.yml:ro - /var/lib/registry/proxy:/var/lib/registry - /opt/registry/certs:/certs:ro networks: - registry-net healthcheck: test: ["CMD" , "sh" , "-c" , "wget -q -O /dev/null https://localhost:5001/v2/ || exit 1" ] interval: 30s timeout: 10s retries: 3
healthcheck 改为 https://localhost,HTTP 已不再暴露。
重启:
1 2 cd /opt/registrydocker compose up -d
4. 信任 CA 证书——Docker daemon 需要认可 step-ca 签发的证书:
1 2 3 4 sudo mkdir -p /etc/docker/certs.d/docker.internal.msksbr.com:5000sudo mkdir -p /etc/docker/certs.d/docker.internal.msksbr.com:5001sudo cp /etc/pki/ca-trust/source/anchors/root_ca.crt /etc/docker/certs.d/docker.internal.msksbr.com:5000/ca.crtsudo cp /etc/pki/ca-trust/source/anchors/root_ca.crt /etc/docker/certs.d/docker.internal.msksbr.com:5001/ca.crt
系统级信任(update-ca-trust)对 Docker daemon 无效——Docker 用自己的一套证书目录,必须单独复制 CA 证书。
5. 去掉 insecure-registries——现在走 HTTPS 了:
1 sudo vim /etc/docker/daemon.json
1 sudo systemctl restart docker
6. 自动续签——systemd timer:
证书有效期 24h,过期前需续签。创建 systemd service + timer,每天自动续签并重启容器。
1 sudo vim /etc/systemd/system/docker-cert-renew.service
1 2 3 4 5 6 7 8 9 [Unit] Description =Renew Docker TLS certificateAfter =network-on line.targetWants =network-on line.target[Service] Type =on eshotExecStart =/usr/local/bin/step ca renew --force /opt/registry/certs/docker.crt /opt/registry/certs/docker.keyExecStartPost =/usr/bin/docker compose -f /opt/registry/docker-compose.yml restart
1 sudo vim /etc/systemd/system/docker-cert-renew.timer
1 2 3 4 5 6 7 8 9 10 11 [Unit] Description =Daily renewal of Docker TLS certificateRequires =docker-cert-renew.service[Timer] OnCalendar =dailyPersistent =true RandomizedDelaySec =3600 [Install] WantedBy =timers.target
1 2 sudo systemctl daemon-reloadsudo systemctl enable --now docker-cert-renew.timer
step ca renew 用现有证书向 CA 发起续签(不需要 provisioner 密码),--force 强制续签即使未临期。ExecStartPost 重启 registry 容器加载新证书。RandomizedDelaySec 错开各节点续签时机,避免同时打到 CA。
7. 验证:
1 2 3 4 5 6 7 docker pull docker.internal.msksbr.com:5000/msksbr/bookmgr:latest docker tag docker.internal.msksbr.com:5001/library/alpine docker.internal.msksbr.com:5000/test/alpine docker push docker.internal.msksbr.com:5000/test/alpine docker pull docker.internal.msksbr.com:5001/library/alpine
infra-yum 启用 TLS 域名 yum.internal.msksbr.com 在 DNS 里已经指向 172.16.4.23。步骤与 infra-docker 一致:装 step CLI → bootstrap → 本机签发 → 配置 nginx → 续签 timer。
0. 安装 step CLI + Bootstrap:
RPM 上一步已放入本地 yum 源,这里直接本地安装:
1 2 3 4 5 sudo dnf install -y /var/www/repos/step/step-cli-*.rpmFP=$(step certificate fingerprint /etc/pki/ca-trust/source/anchors/root_ca.crt) step ca bootstrap --ca-url https://ca.internal.msksbr.com:9000 --fingerprint "$FP "
根证书已在上一步”分发根证书”中信任,否则 bootstrap 会报 TLS 握手失败。
1. 签发证书:
在物理机 上传 provisioner 密码:
1 2 scp ca-password deploy@172.16.3.35:/tmp/
回到 infra-yum :
1 2 3 4 5 6 7 8 9 10 sudo mkdir -p /opt/yum/certsstep ca certificate yum.internal.msksbr.com \ /opt/yum/certs/yum.crt \ /opt/yum/certs/yum.key \ --provisioner admin --password-file /tmp/ca-password sudo chmod 600 /opt/yum/certs/yum.keysudo chmod 644 /opt/yum/certs/yum.crtrm -f /tmp/ca-password
2. nginx 改 HTTPS:
1 sudo vim /opt/yum/nginx.conf
1 2 3 4 5 6 7 8 server { listen 443 ssl; server_name _; ssl_certificate /certs/yum.crt; ssl_certificate_key /certs/yum.key; root /var/www/repos; autoindex on ; }
3. docker-compose.yml 加证书挂载、端口改 443:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 services: nginx: image: docker.internal.msksbr.com:5001/library/nginx:alpine container_name: yum-nginx restart: unless-stopped ports: - "172.16.4.23:443:443" volumes: - /opt/yum/nginx.conf:/etc/nginx/conf.d/default.conf:ro - /var/www/repos:/var/www/repos:ro - /opt/yum/certs:/certs:ro networks: - yum-net networks: yum-net: driver: bridge
重启:
1 2 cd /opt/yumdocker compose up -d
4. 自动续签——systemd timer(重启 nginx 容器加载新证书):
1 sudo vim /etc/systemd/system/yum-cert-renew.service
1 2 3 4 5 6 7 8 9 [Unit] Description =Renew Yum TLS certificateAfter =network-on line.targetWants =network-on line.target[Service] Type =on eshotExecStart =/usr/local/bin/step ca renew --force /opt/yum/certs/yum.crt /opt/yum/certs/yum.keyExecStartPost =/usr/bin/docker compose -f /opt/yum/docker-compose.yml restart
1 sudo vim /etc/systemd/system/yum-cert-renew.timer
1 2 3 4 5 6 7 8 9 10 11 [Unit] Description =Daily renewal of Yum TLS certificateRequires =yum-cert-renew.service[Timer] OnCalendar =dailyPersistent =true RandomizedDelaySec =4800 [Install] WantedBy =timers.target
1 2 sudo systemctl daemon-reloadsudo systemctl enable --now yum-cert-renew.timer
RandomizedDelaySec 取 4800s(80 分钟),和 infra-docker 的 3600s 错开,CA 不会在同一时刻被多个节点续签请求淹没。
5. 本机验证,再补 firewalld 443 口:
1 2 curl -s -o /dev/null -w '%{http_code}' https://yum.internal.msksbr.com/
1 2 sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.4.0/24" port protocol="tcp" port="443" accept' --zone=internalsudo firewall-cmd --reload
已有基础设施节点回补配置 根证书已在上一步全节点分发并信任,这里只需回补 NTP 和确认 DNS 解析 。
NTP——所有节点 chronyd 指向 infra-ntp(172.16.4.21):
1 2 3 4 5 6 sudo sed -i 's/^pool .*/# &/' /etc/chrony.confsudo sed -i 's/^server .*/# &/' /etc/chrony.confecho "server 172.16.4.21 iburst" | sudo tee -a /etc/chrony.confsudo systemctl restart chronydchronyc sources -v
infra-ntp 自己不需要改——它本身就是上游。
DNS 和 Docker registry-mirrors——各节点统一指向 infra 服务:
DNS 在部署阶段已全部指向 172.16.4.20,无需改动。Docker daemon 的 registry-mirrors 和 insecure-registries 在 TLS 升级时已移除旧配置——此时各节点 pull 镜像走 docker.internal.msksbr.com 打全标签即可。私有镜像推送到 5000,Hub 缓存走 5001。
验证 DNS 解析 + CA 信任链到位:
1 2 3 4 5 curl -s -o /dev/null -w '%{http_code}' https://docker.internal.msksbr.com:5000/v2/_catalog curl -s -o /dev/null -w '%{http_code}' https://yum.internal.msksbr.com/
至此六个基础设施节点全部互联互通:DNS 解析内网域名、NTP 时间同步、CA 证书互信、TLS 自动续签。infra-docker 和 infra-yum 的 HTTPS 链路全网信任。
模板机 after-infra 快照 回到模板机,接入已部署的所有基础设施,拍第二张快照。之后业务层、数据层、管理网节点都从 after-infra 起手。
1. DNS → infra-dns:
1 2 3 4 5 6 7 8 sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.100/24 \ ipv4.gateway 172.16.4.10 ipv4.dns "172.16.4.20" dig +short dns.internal.msksbr.com dig +short baidu.com
2. NTP → infra-ntp:
1 2 3 4 5 sudo sed -i 's/^pool .*/# &/' /etc/chrony.confsudo sed -i 's/^server .*/# &/' /etc/chrony.confecho "server 172.16.4.21 iburst" | sudo tee -a /etc/chrony.confsudo systemctl restart chronydchronyc sources -v
3. 信任 step-ca 根证书:
根证书已在之前的步骤中分发过一轮,若模板机当时未配置 IP,则现在补传。在物理机 上:
1 2 scp deploy@172.16.3.36:/opt/step-ca/certs/root_ca.crt . scp root_ca.crt deploy@172.16.4.100:/tmp/
模板机上:
1 2 sudo cp /tmp/root_ca.crt /etc/pki/ca-trust/source/anchors/sudo update-ca-trust
这一步很关键——后续所有走 HTTPS 的内网服务都需要系统信任内部 CA,否则 curl / dnf / docker 全部证书报错。后面克隆出来的业务/数据/管理网节点也会继承这个信任,无需再次配置。
4. Docker → infra-docker:
1 2 3 4 sudo mkdir -p /etc/docker/certs.d/docker.internal.msksbr.com:5001sudo cp /tmp/root_ca.crt /etc/docker/certs.d/docker.internal.msksbr.com:5001/ca.crtsudo vim /etc/docker/daemon.json
1 2 3 4 { "iptables" : true , "registry-mirrors" : [ "https://docker.internal.msksbr.com:5001" ] }
1 2 3 4 5 6 sudo systemctl restart dockerdocker pull nginx:alpine docker rmi nginx:alpine docker pull nginx:alpine
5. Yum → infra-yum:
1 2 sudo rm -f /etc/yum.repos.d/*.reposudo vim /etc/yum.repos.d/local.repo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 [openeuler-os] name =Local OpenEuler OSbaseurl =https://yum.internal.msksbr.com/openeuler-osenabled =1 gpgcheck =0 [openeuler-everything] name =Local OpenEuler Everythingbaseurl =https://yum.internal.msksbr.com/openeuler-everythingenabled =1 gpgcheck =0 [openeuler-epol] name =Local OpenEuler EPOLbaseurl =https://yum.internal.msksbr.com/openeuler-epolenabled =1 gpgcheck =0 [openeuler-update] name =Local OpenEuler Updatebaseurl =https://yum.internal.msksbr.com/openeuler-updateenabled =1 gpgcheck =0 [docker-ce] name =Local Docker CEbaseurl =https://yum.internal.msksbr.com/docker-ceenabled =1 gpgcheck =0 [step] name =Local Step CLIbaseurl =https://yum.internal.msksbr.com/stepenabled =1 gpgcheck =0
1 2 3 sudo dnf clean allsudo dnf makecachesudo dnf install -y htop
6. 清理、关机、拍快照:
1 2 3 4 5 6 7 8 sudo rm -f /etc/machine-idsudo touch /etc/machine-idsudo chmod 444 /etc/machine-idsudo rm -f /etc/ssh/ssh_host_*sudo sed -i '/^UUID/d' /etc/NetworkManager/system-connections/*.nmconnection 2>/dev/nullsudo poweroff
关机后在 VMware 里拍快照,命名为 after-infra 。后续克隆业务层、数据层、管理网节点都从这里起手,DNS / NTP / CA / Docker / Yum 全部就绪,且根证书已信任——新节点只需 step ca bootstrap + step ca certificate 就能自签 TLS 证书。
数据层 应用跑起来之前,先把数据库架好。图书管理系统用 MySQL 8.0,数据落盘后用 Percona XtraBackup 定期热备。
MySQL 8.0(data-db) 1. 从 after-infra 快照链接克隆 data-db,移除 eth-dmz/eth-pub(只保留 eth-infra + eth-mgmt + eth-app),额外挂载 5 块 50GB 数据盘。开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 12 13 for iface in eth-infra eth-mgmt eth-app; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.33/24 \ ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.39/24 sudo nmcli con add con-name "net-app" ifname eth-app type ethernet \ ipv4.method manual ipv4.addresses 172.16.2.22/24 sudo hostnamectl set-hostname data-db
数据层节点不设默认网关——DNS/NTP/Docker/Yum/CA 全在直连的 infra 子网内(172.16.4.0/24),不需要经代理出站。数据库能出外网还叫什么数据库。
2. 5 块数据盘组 RAID5(4 数据 + 1 热备),再上 LVM:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 lsblk sudo mdadm --create /dev/md0 --level=5 --raid-devices=4 --spare-devices=1 \ /dev/sdb /dev/sdc /dev/sdd /dev/sde /dev/sdf sudo mdadm --detail --scan | sudo tee -a /etc/mdadm.confsudo pvcreate /dev/md0sudo vgcreate vg_mysql /dev/md0sudo lvcreate -l 100%FREE -n lv_mysql vg_mysqlsudo mkfs.xfs /dev/vg_mysql/lv_mysqlsudo mkdir -p /var/lib/mysqlsudo mount /dev/vg_mysql/lv_mysql /var/lib/mysqlecho '/dev/vg_mysql/lv_mysql /var/lib/mysql xfs defaults 0 0' | sudo tee -a /etc/fstab
RAID5 允许坏一块盘不丢数据,热备盘自动顶上重建。LVM 之上方便后续 lvextend 在线扩容——数据涨了加盘、扩 VG、扩 LV、xfs_growfs,不停 MySQL。
3. 安装 step CLI + 签发 TLS 证书:
MySQL 8.0 原生支持 TLS——证书由 step-ca 签发,域名 db.internal.msksbr.com 在 DNS 里已经指向 172.16.2.22。
1 2 3 4 5 sudo dnf install -y step-cliFP=$(step certificate fingerprint /etc/pki/ca-trust/source/anchors/root_ca.crt) step ca bootstrap --ca-url https://ca.internal.msksbr.com:9000 --fingerprint "$FP "
provisioner 密码从物理机传过来:
1 2 scp ca-password deploy@172.16.3.39:/tmp/
签发证书:
1 2 3 4 5 6 7 8 9 10 11 12 sudo mkdir -p /opt/mysql/certssudo chown deploy:deploy /opt/mysql/certsstep ca certificate db.internal.msksbr.com \ /opt/mysql/certs/db.crt \ /opt/mysql/certs/db.key \ --provisioner admin --password-file /tmp/ca-password sudo chmod 600 /opt/mysql/certs/db.keysudo chmod 644 /opt/mysql/certs/db.crtsudo chown -R 999:999 /opt/mysql/certsrm -f /tmp/ca-password
证书在 data-db 本机签发,无需物理机中转。db.internal.msksbr.com 的 A 记录已指向 172.16.2.22,内网节点通过域名走 TLS 连接即可验证证书。
4. 创建目录和配置文件:
1 sudo mkdir -p /opt/mysql/config
MySQL 配置文件——开启 GTID 复制 + ROW binlog,为后续加只读副本做准备:
1 sudo vim /opt/mysql/config/my.cnf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [mysqld] server-id = 1 bind-address = 0.0 .0.0 port = 3306 character-set-server = utf8mb4collation-server = utf8mb4_unicode_cigtid_mode = ON enforce_gtid_consistency = ON log_bin = mysql-binbinlog_format = ROWbinlog_row_image = FULLbinlog_expire_logs_seconds = 604800 log_slave_updates = ON ssl_cert = /certs/db.crtssl_key = /certs/db.keyssl_ca = /certs/ca.crtrequire_secure_transport = ON innodb_buffer_pool_size = 2 Ginnodb_flush_log_at_trx_commit = 1 innodb_file_per_table = ON
require_secure_transport=ON 强制所有 TCP 连接走 TLS——连上了但没证书的一律拒绝。ssl_ca 指向根证书让 MySQL 能验证客户端证书链。gtid_mode=ON + binlog_format=ROW 是 GTID 复制的最低要求,后续加副本时无需改配置重起。binlog_expire_logs_seconds=604800 保留 7 天 binlog——够 XtraBackup 追增量。MySQL 8.0.46 已废弃 binlog_expire_logs_days。
5. docker-compose.yml:
1 sudo vim /opt/mysql/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 services: mysql: image: docker.internal.msksbr.com:5001/library/mysql:8.0 container_name: mysql restart: unless-stopped ports: - "172.16.2.22:3306:3306" environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/root_password MYSQL_DATABASE: bookmgr MYSQL_USER: bookmgr MYSQL_PASSWORD_FILE: /run/secrets/user_password volumes: - /opt/mysql/config/my.cnf:/etc/mysql/conf.d/my.cnf:ro - /var/lib/mysql:/var/lib/mysql - /opt/mysql/certs:/certs:ro secrets: - root_password - user_password networks: - mysql-net xtrabackup: image: docker.internal.msksbr.com:5001/percona/percona-xtrabackup:8.0 profiles: [backup ] network_mode: host user: "999:999" volumes: - /var/lib/mysql:/var/lib/mysql:ro - /opt/mysql/certs:/certs:ro - /opt/mysql/secrets:/run/secrets:ro secrets: root_password: file: /opt/mysql/secrets/root_password user_password: file: /opt/mysql/secrets/user_password networks: mysql-net: driver: bridge
生成密码文件:
1 2 3 4 sudo mkdir -p /opt/mysql/secretsopenssl rand -base64 24 | sudo tee /opt/mysql/secrets/root_password > /dev/null openssl rand -base64 24 | sudo tee /opt/mysql/secrets/user_password > /dev/null sudo chmod 600 /opt/mysql/secrets/*
MySQL 8.0 镜像支持 _FILE 后缀的环境变量,从 Docker secret 文件读密码——不暴露在 docker inspect 或环境变量中。
把根证书复制到 certs 目录,MySQL 需要它验证客户端证书链:
1 2 sudo cp /etc/pki/ca-trust/source/anchors/root_ca.crt /opt/mysql/certs/ca.crtsudo chmod 644 /opt/mysql/certs/ca.crt
6. 启动并导入数据:
1 2 3 cd /opt/mysqldocker compose up -d docker compose logs -f mysql
MYSQL_DATABASE + MYSQL_USER 环境变量已让 MySQL 自动建好 bookmgr 库和用户,接下来只差表。物理机从 git.msksbr.com/msksbr/bookMgr 拉取 SQL 文件,scp 到 data-db 的 /tmp/,然后导入:
1 2 3 4 ROOT_PW=$(sudo cat /opt/mysql/secrets/root_password) docker cp /tmp/bookmgr.schema.sql mysql:/tmp/ docker exec -i mysql mysql -uroot -p"$ROOT_PW " bookmgr < /tmp/bookmgr.schema.sql
不用 docker-entrypoint-initdb.d 那套——那个只在数据目录为空时跑一次,后面想加表还得进来 exec,还不如一开始就手动导入来得直接。作者用的是带测试条目的版本bookmgr.sql,读者可以只导 bookmgr.schema.sql。
MYSQL_USER 环境变量自动创建的用户不带 REQUIRE SSL,补上:
1 2 3 4 ROOT_PW=$(sudo cat /opt/mysql/secrets/root_password) docker compose exec mysql mysql -uroot -p"$ROOT_PW " -e " ALTER USER 'bookmgr'@'%' REQUIRE SSL; "
docker compose exec 走的是容器内的 Unix socket,不受 require_secure_transport 限制——本地管理操作不受影响。REQUIRE SSL 只限制 TCP 远程连接。
MYSQL_USER 自动创建的 bookmgr 用户默认拥有 bookmgr 库的全部权限,含 DDL(DROP/ALTER/CREATE)。应用只需要 CRUD,收回多余权限:
1 2 3 4 5 docker compose exec mysql mysql -uroot -p"$ROOT_PW " -e " REVOKE ALL PRIVILEGES ON bookmgr.* FROM 'bookmgr'@'%'; GRANT SELECT, INSERT, UPDATE, DELETE ON bookmgr.* TO 'bookmgr'@'%'; FLUSH PRIVILEGES; "
最小权限原则:应用账号只管增删改查,DDL 留给 root 通过 socket 本地操作。
验证:
1 2 3 4 5 6 ROOT_PW=$(sudo cat /opt/mysql/secrets/root_password) docker compose exec mysql mysql -uroot -p"$ROOT_PW " -e " SELECT @@gtid_mode, @@binlog_format, @@server_id; SHOW DATABASES; USE bookmgr; SHOW TABLES; "
7. 证书自动续签:
和 infra-docker 一样,systemd timer 每天续签并重启容器。
1 sudo vim /etc/systemd/system/mysql-cert-renew.service
1 2 3 4 5 6 7 8 9 [Unit] Description =Renew MySQL TLS certificateAfter =network-on line.targetWants =network-on line.target[Service] Type =on eshotExecStart =/usr/local/bin/step ca renew --force /opt/mysql/certs/db.crt /opt/mysql/certs/db.keyExecStartPost =/usr/bin/docker compose -f /opt/mysql/docker-compose.yml restart
1 sudo vim /etc/systemd/system/mysql-cert-renew.timer
1 2 3 4 5 6 7 8 9 10 11 [Unit] Description =Daily renewal of MySQL TLS certificateRequires =mysql-cert-renew.service[Timer] OnCalendar =dailyPersistent =true RandomizedDelaySec =5400 [Install] WantedBy =timers.target
1 2 sudo systemctl daemon-reloadsudo systemctl enable --now mysql-cert-renew.timer
RandomizedDelaySec=5400(90 分钟)和 docker/yum 的错开,CA 不会被同时打到。
验证 TLS:
1 2 3 4 ROOT_PW=$(sudo cat /opt/mysql/secrets/root_password) docker compose exec mysql mysql -uroot -p"$ROOT_PW " -e "SHOW VARIABLES LIKE '%ssl%';"
8. XtraBackup 热备——全量 + 增量,流式推到 data-backup:
XtraBackup 必须跑在 data-db 上(需要本地访问 MySQL datadir),备份结果通过 SSH 管道流到 data-backup 异地存储。
创建备份专用 OS 用户——backup,组 app:
1 2 sudo groupadd -g 2001 appsudo useradd -m -u 2001 -g app -G docker -s /bin/bash backup
backup 用户 uid/gid 与 data-backup 保持一致。加入 docker 组才能跑 docker compose。
SSH 密钥——data-db → data-backup:
1 2 3 sudo -u backup ssh-keygen -t ed25519 -f /home/backup/.ssh/id_ed25519 -N "" sudo cat /home/backup/.ssh/id_ed25519.pub
备份脚本:
1 2 3 sudo mkdir -p /opt/mysql/backupsudo vim /opt/mysql/backup/backup.shsudo chmod +x /opt/mysql/backup/backup.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #!/bin/bash set -eBACKUP_HOST="172.16.2.23" BACKUP_DIR="/var/lib/backup" LOG_FILE="/var/log/mysql-backup.log" COMPOSE_FILE="/opt/mysql/docker-compose.yml" TIMESTAMP=$(date +%Y%m%d_%H%M%S) DAY_OF_WEEK=$(date +%u) BACKUP_PW=$(cat /opt/mysql/secrets/backup_password) [ "$DAY_OF_WEEK " -eq 7 ] && TYPE="full" || TYPE="inc" if [ "$TYPE " = "inc" ]; then LATEST_FULL=$(ssh backup@"$BACKUP_HOST " "ls -d $BACKUP_DIR /full_* 2>/dev/null | tail -1" ) [ -z "$LATEST_FULL " ] && TYPE="full" fi DIR="${BACKUP_DIR} /${TYPE} _${TIMESTAMP} " ssh backup@"$BACKUP_HOST " "mkdir -p $DIR " docker compose -f "$COMPOSE_FILE " --profile backup run --rm xtrabackup \ xtrabackup \ --host=172.16.2.22 --port=3306 \ --user=backup --password="$BACKUP_PW " \ --ssl-mode=VERIFY_CA --ssl-ca=/certs/ca.crt \ --backup --stream=xbstream --target-dir=/tmp \ --datadir=/var/lib/mysql \ 2>> "$LOG_FILE " \ | ssh backup@"$BACKUP_HOST " \ "docker run --rm -i --user 2001:2001 \ -v /var/lib/backup:/var/lib/backup \ docker.internal.msksbr.com:5001/percona/percona-xtrabackup:8.0 \ xbstream -x -C $DIR "
xtrabackup 走 TCP 连接本机 MySQL 172.16.2.22:3306(network_mode: host 时默认走 socket 连不到容器),读 /var/lib/mysql 数据文件做物理备份,--stream=xbstream 输出到 stdout,SSH 管道推到 data-backup 后由对端容器里的 xbstream -x 解开落盘。profiles: [backup] 保证 docker compose up -d 时不会启动此服务——仅 cron 按需 invoke。
备份专用 MySQL 用户 + 密码:
1 2 3 4 5 6 7 8 9 10 11 12 BACKUP_PW=$(openssl rand -base64 24) echo "$BACKUP_PW " | sudo tee /opt/mysql/secrets/backup_password > /dev/nullsudo chmod 640 /opt/mysql/secrets/backup_passwordsudo chgrp app /opt/mysql/secrets/backup_passwordROOT_PW=$(sudo cat /opt/mysql/secrets/root_password) docker compose exec mysql mysql -uroot -p"$ROOT_PW " -e " CREATE USER 'backup'@'172.16.2.22' IDENTIFIED BY '$BACKUP_PW ' REQUIRE SSL; GRANT BACKUP_ADMIN, PROCESS, LOCK TABLES, SELECT, REPLICATION CLIENT ON *.* TO 'backup'@'172.16.2.22'; FLUSH PRIVILEGES; "
@'172.16.2.22' 而非 @'localhost'——MySQL 把 TCP 连接和 socket 视为不同来源,network_mode: host 走 TCP 时 xtrabackup 来源 IP 就是本机 172.16.2.22,@'localhost' 只认 socket。
Cron + 日志文件权限:
1 2 3 4 5 6 7 8 sudo touch /var/log/mysql-backup.logsudo chown backup:app /var/log/mysql-backup.logsudo tee /etc/cron.d/mysql-backup << 'EOF' 0 3 * * 7 backup /opt/mysql/backup/backup.sh 0 3 * * 1-6 backup /opt/mysql/backup/backup.sh EOF
手动跑一次验证:
1 sudo -i -u backup /opt/mysql/backup/backup.sh
1 2 ssh backup@172.16.2.23 "ls -la /var/lib/backup/full_*/"
备份目录有 xtrabackup_checkpoints 文件、日志无报错,全链路通。后续 cron 到点自动执行。
9. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 11 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --change-interface=eth-app --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.2.0/24" port protocol="tcp" port="3306" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
只有 app-backend(172.16.2.21)会连 3306,但放行整个 /24 也不会有其他节点访问——内网没有多余的服务。
data-backup 异地存储 data-backup 是纯存储节点——不跑任何备份任务。备份由 data-db 的 xtrabackup 容器执行,经 SSH 管道流式推到本机落盘。只需要 RAID 存储 + SSH 接收。
1. 从 after-infra 快照链接克隆 data-backup,移除 eth-dmz/eth-pub(只保留 eth-infra + eth-mgmt + eth-app),额外挂载 5 块 50GB 数据盘。开机,组 RAID5 + LVM:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 lsblk sudo mdadm --create /dev/md0 --level=5 --raid-devices=4 --spare-devices=1 \ /dev/sdb /dev/sdc /dev/sdd /dev/sde /dev/sdf sudo mdadm --detail --scan | sudo tee -a /etc/mdadm.confsudo pvcreate /dev/md0sudo vgcreate vg_backup /dev/md0sudo lvcreate -l 100%FREE -n lv_backup vg_backupsudo mkfs.xfs /dev/vg_backup/lv_backupsudo mkdir -p /var/lib/backupsudo mount /dev/vg_backup/lv_backup /var/lib/backupecho '/dev/vg_backup/lv_backup /var/lib/backup xfs defaults 0 0' | sudo tee -a /etc/fstab
配置 IP:
1 2 3 4 5 6 7 8 9 10 11 12 13 for iface in eth-infra eth-mgmt eth-app; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.34/24 \ ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.40/24 sudo nmcli con add con-name "net-app" ifname eth-app type ethernet \ ipv4.method manual ipv4.addresses 172.16.2.23/24 sudo hostnamectl set-hostname data-backup
预拉取接收端需要的镜像——data-db 的备份流通过 SSH 管道过来后,由 xbstream -x 解开落盘:
1 docker pull docker.internal.msksbr.com:5001/percona/percona-xtrabackup:8.0
2. 创建备份专用用户——backup,组 app:
1 2 3 4 5 6 7 8 sudo groupadd -g 2001 appsudo useradd -m -u 2001 -g app -G docker -s /bin/bash backupsudo mkdir -p /home/backup/.sshsudo chmod 700 /home/backup/.sshsudo chown -R backup:app /home/backupsudo chown -R backup:app /var/lib/backup
backup 用户给 uid/gid 2001,与应用层服务保持一致,后面 bookmgr 后端也会归入 app 组。/var/lib/backup 归 backup:app,备份流写到这个目录不需要 root。
模板机 sshd 只允许 deploy,加上 backup:
1 2 sudo sed -i 's/AllowUsers deploy/AllowUsers deploy backup/' /etc/ssh/sshd_config.d/99-hardening.confsudo systemctl reload sshd
3. 手动传 SSH 公钥:
data-db 上 backup 用户的公钥需要手动放到 data-backup 的 backup 用户 authorized_keys——backup 用户不设密码,ssh-copy-id 走不通。
在 data-db 上:
1 2 sudo cat /home/backup/.ssh/id_ed25519.pub
回到 data-backup :
1 2 3 4 5 sudo tee /home/backup/.ssh/authorized_keys << 'EOF' EOF sudo chmod 600 /home/backup/.ssh/authorized_keyssudo chown backup:app /home/backup/.ssh/authorized_keys
这就是我说的”只能手动传”——生产环境的服务账户不设密码、不开 PasswordAuthentication,公钥必须由管理员亲自放进去。
4. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 11 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --change-interface=eth-app --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.2.22" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
data-backup 只接受 SSH 入站——data-db 推送备份流、管理网运维。
应用层 app-backend(Spring Boot 后端) bookMgr 后端——Spring Boot 4.0 + Kotlin + MyBatis-Plus,JWT + Argon2 认证,等保三级审计日志。源码在 git.msksbr.com/msksbr/bookMgr ,deploy.sh 构建 Docker 镜像并推到 Gitea Registry。
0. 镜像准备——推到本地私有仓库:
app-backend 没有默认网关,拉不了 Gitea Registry。在能访问互联网的机器上(dev 机或 infra-docker)先把镜像从 Gitea 拖下来,再推到本地私有仓库:
1 2 3 4 5 docker pull git.msksbr.com/msksbr/bookmgr:v0.1f docker tag git.msksbr.com/msksbr/bookmgr:v0.1f \ docker.internal.msksbr.com:5000/msksbr/bookmgr:v0.1f docker push docker.internal.msksbr.com:5000/msksbr/bookmgr:v0.1f
本地 Registry 在 5000 端口(私有仓库),5001 是 Hub 代理缓存。私有镜像一律走 5000。
1. 从 after-infra 快照链接克隆 app-backend,移除 eth-pub/eth-dmz(保留 eth-infra + eth-mgmt + eth-app)。开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 12 13 for iface in eth-infra eth-mgmt eth-app; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.32/24 \ ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.38/24 sudo nmcli con add con-name "net-app" ifname eth-app type ethernet \ ipv4.method manual ipv4.addresses 172.16.2.21/24 sudo hostnamectl set-hostname app-backend
同 data-db,无默认网关——所有依赖(DNS/Yum/Docker/CA)全在直连的 infra 子网。
2. step CLI 签发 TLS 证书:
DNS 里已有 backend.internal.msksbr.com → 172.16.2.21(infra-dns 部署时加的),后端就用这个域名,不用新建。
验证 DNS:
1 2 dig @172.16.4.20 backend.internal.msksbr.com +short
在 app-backend 上安装 step CLI 并签发证书:
1 2 3 4 sudo dnf install -y step-cliFP=$(step certificate fingerprint /etc/pki/ca-trust/source/anchors/root_ca.crt) step ca bootstrap --ca-url https://ca.internal.msksbr.com:9000 --fingerprint "$FP "
provisioner 密码从物理机传过来:
1 2 scp ca-password deploy@172.16.3.38:/tmp/
签发:
1 2 3 4 5 6 7 8 9 10 11 sudo mkdir -p /opt/bookmgr/certssudo chown deploy:deploy /opt/bookmgr/certsstep ca certificate backend.internal.msksbr.com \ /opt/bookmgr/certs/backend.crt \ /opt/bookmgr/certs/backend.key \ --provisioner admin --password-file /tmp/ca-password sudo chmod 600 /opt/bookmgr/certs/backend.keysudo chmod 644 /opt/bookmgr/certs/backend.crtrm -f /tmp/ca-password
证书 CN 为 backend.internal.msksbr.com,WAF 通过这个域名走 HTTPS 连后端。
3. 目录结构 + 环境变量 + docker-compose.yml:
1 2 sudo mkdir -p /opt/bookmgr /var/log/bookmgrsudo chown -R deploy:deploy /opt/bookmgr
bookmgr 数据库用户的密码在 data-db 上,先从管理网传过来:
1 2 3 sudo cat /opt/mysql/secrets/user_password
回到 app-backend,生成 JWT 密钥,填入刚才复制的 DB 密码:
1 2 JWT_SECRET=$(openssl rand -base64 48) DB_PW='<从 data-db 复制的密码>'
JWT 密钥 48 字节 base64 → 64 字符,满足 HMAC-SHA256 最低 32 字节要求。
.env 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 sudo tee /opt/bookmgr/.env << EOF # 数据库 DB_DRIVER=com.mysql.cj.jdbc.Driver DB_TYPE=mysql DB_URL=172.16.2.22 DB_PORT=3306 DB_NAME=bookmgr DB_USER=bookmgr DB_PASSWORD=${DB_PW} # JWT JWT_SECRET=${JWT_SECRET} # 日志 LOG_PATH=/var/log/bookmgr LOG_EXPORT_AUDIT=true LOG_EXPORT_RUNTIME=true LOG_LEVEL=INFO # TLS(Spring Boot 3.1+ 原生 PEM 支持) SERVER_SSL_CERTIFICATE=/certs/backend.crt SERVER_SSL_CERTIFICATE_PRIVATE_KEY=/certs/backend.key SERVER_SSL_ENABLED=true SERVER_PORT=8443 EOF sudo chmod 600 /opt/bookmgr/.envsudo chown deploy:deploy /opt/bookmgr/.env
docker-compose.yml:
1 sudo vim /opt/bookmgr/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 services: bookmgr: image: docker.internal.msksbr.com:5000/msksbr/bookmgr:v0.1f container_name: bookmgr restart: unless-stopped ports: - "172.16.2.21:8443:8443" env_file: - /opt/bookmgr/.env volumes: - /opt/bookmgr/certs:/certs:ro - /var/log/bookmgr:/var/log/bookmgr networks: - app-net networks: app-net: driver: bridge
Spring Boot 4.0 原生支持 PEM 证书——SERVER_SSL_CERTIFICATE + SERVER_SSL_CERTIFICATE_PRIVATE_KEY 直接加载,不需要 keystore。端口 8443,WAF 走 HTTPS 连接。env_file 而非 environment——敏感值(JWT / DB 密码)不暴露在 docker inspect 中。
4. 拉镜像 + 启动:
1 2 3 4 cd /opt/bookmgrdocker compose pull docker compose up -d docker compose logs -f
验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 CA_CERT="/etc/pki/ca-trust/source/anchors/root_ca.crt" BASE="https://backend.internal.msksbr.com:8443" curl --cacert "$CA_CERT " -s -o /dev/null -w "HTTP %{http_code}" \ -X POST "$BASE /api/auth/logout" curl --cacert "$CA_CERT " -s "$BASE /api/books/getall" | python3 -m json.tool curl --cacert "$CA_CERT " -s "$BASE /api/books/search?query=kotlin" | python3 -m json.tool
logout 返回 200 说明路由通了;getall 返回 15 条图书、search 命中 “Kotlin实战”,中文不乱码——HTTPS → Spring Boot → MyBatis → MySQL(TLS 加密连接)全链路通。
5. TLS 证书自动续签:
1 sudo vim /etc/systemd/system/bookmgr-cert-renew.service
1 2 3 4 5 6 7 8 9 [Unit] Description =Renew bookmgr TLS certificateAfter =network-on line.targetWants =network-on line.target[Service] Type =on eshotExecStart =/usr/local/bin/step ca renew --force /opt/bookmgr/certs/backend.crt /opt/bookmgr/certs/backend.keyExecStartPost =/usr/bin/docker compose -f /opt/bookmgr/docker-compose.yml restart
1 sudo vim /etc/systemd/system/bookmgr-cert-renew.timer
1 2 3 4 5 6 7 8 9 10 11 [Unit] Description =Daily renewal of bookmgr TLS certificateRequires =bookmgr-cert-renew.service[Timer] OnCalendar =dailyPersistent =true RandomizedDelaySec =2700 [Install] WantedBy =timers.target
1 2 sudo systemctl daemon-reloadsudo systemctl enable --now bookmgr-cert-renew.timer
RandomizedDelaySec=2700(45 分钟)——MySQL 用了 5400,bookmgr 用 2700,继续错开。
6. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 11 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --change-interface=eth-app --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.2.10" port protocol="tcp" port="8443" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
8443 只放 WAF(172.16.2.10),其他节点一律不通。管理网走 SSH,不需要访问应用端口。
app-frontend(React 前端) bookMgr 前端——React 19 + TypeScript + Vite,shadcn/ui + Tailwind CSS,SPA 单页应用。源码在 git.msksbr.com/msksbr/bookMgr-client ,多阶段 Docker 构建:node:22-alpine 编译 → nginx:alpine 托管 dist/,默认监听 80,try_files $uri /index.html 做 SPA fallback。
0. 镜像准备——推到本地私有仓库:
1 2 3 4 5 docker pull git.msksbr.com/msksbr/bookmgr-client:latest docker tag git.msksbr.com/msksbr/bookmgr-client:latest \ docker.internal.msksbr.com:5000/msksbr/bookmgr-client:latest docker push docker.internal.msksbr.com:5000/msksbr/bookmgr-client:latest
1. 从 after-infra 快照链接克隆 app-frontend,移除 eth-pub/eth-dmz(保留 eth-infra + eth-mgmt + eth-app)。开机配置 IP:
1 2 3 4 5 6 7 8 9 10 11 12 13 for iface in eth-infra eth-mgmt eth-app; do sudo nmcli con delete "$iface " 2>/dev/null done sudo nmcli con add con-name "net-infra" ifname eth-infra type ethernet \ ipv4.method manual ipv4.addresses 172.16.4.31/24 \ ipv4.dns "172.16.4.20" sudo nmcli con add con-name "net-mgmt" ifname eth-mgmt type ethernet \ ipv4.method manual ipv4.addresses 172.16.3.37/24 sudo nmcli con add con-name "net-app" ifname eth-app type ethernet \ ipv4.method manual ipv4.addresses 172.16.2.20/24 sudo hostnamectl set-hostname app-frontend
2. step CLI + 签发 TLS 证书:
1 2 3 4 sudo dnf install -y step-cliFP=$(step certificate fingerprint /etc/pki/ca-trust/source/anchors/root_ca.crt) step ca bootstrap --ca-url https://ca.internal.msksbr.com:9000 --fingerprint "$FP "
provisioner 密码从管理网传:
1 2 scp /tmp/ca-password deploy@172.16.3.37:/tmp/
签发:
1 2 3 4 5 6 7 8 9 10 11 sudo mkdir -p /opt/frontend/certssudo chown deploy:deploy /opt/frontend/certsstep ca certificate frontend.internal.msksbr.com \ /opt/frontend/certs/frontend.crt \ /opt/frontend/certs/frontend.key \ --provisioner admin --password-file /tmp/ca-password sudo chmod 600 /opt/frontend/certs/frontend.keysudo chmod 644 /opt/frontend/certs/frontend.crtrm -f /tmp/ca-password
3. nginx SSL 配置 + docker-compose.yml:
原镜像的 nginx.conf 只配了 80 明文。自定义一份加上 TLS:
1 2 sudo mkdir -p /opt/frontendsudo vim /opt/frontend/nginx.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server { listen 443 ssl; server_name frontend.internal.msksbr.com; ssl_certificate /certs/frontend.crt; ssl_certificate_key /certs/frontend.key; root /usr/share/nginx/html; gzip on ; gzip_types text/css application/javascript application/json image/svg+xml; gzip_vary on ; location / { try_files $uri /index.html; } }
docker-compose.yml:
1 sudo vim /opt/frontend/docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 services: frontend: image: docker.internal.msksbr.com:5000/msksbr/bookmgr-client:latest container_name: frontend restart: unless-stopped ports: - "172.16.2.20:443:443" volumes: - /opt/frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro - /opt/frontend/certs:/certs:ro networks: - front-net networks: front-net: driver: bridge
4. 拉镜像 + 启动:
1 2 3 cd /opt/frontenddocker compose pull docker compose up -d
验证——直接走 443 标准端口,无需指定端口号:
1 2 3 curl --cacert /etc/pki/ca-trust/source/anchors/root_ca.crt \ https://frontend.internal.msksbr.com
5. TLS 证书自动续签:
1 sudo vim /etc/systemd/system/frontend-cert-renew.service
1 2 3 4 5 6 7 8 9 [Unit] Description =Renew frontend TLS certificateAfter =network-on line.targetWants =network-on line.target[Service] Type =on eshotExecStart =/usr/local/bin/step ca renew --force /opt/frontend/certs/frontend.crt /opt/frontend/certs/frontend.keyExecStartPost =/usr/bin/docker compose -f /opt/frontend/docker-compose.yml restart
1 sudo vim /etc/systemd/system/frontend-cert-renew.timer
1 2 3 4 5 6 7 8 9 10 11 [Unit] Description =Daily renewal of frontend TLS certificateRequires =frontend-cert-renew.service[Timer] OnCalendar =dailyPersistent =true RandomizedDelaySec =1800 [Install] WantedBy =timers.target
1 2 sudo systemctl daemon-reloadsudo systemctl enable --now frontend-cert-renew.timer
RandomizedDelaySec=1800(30 分钟)——MySQL 5400、bookmgr 2700、frontend 1800,继续错开。
6. 锁门——firewalld:
1 2 3 4 5 6 7 8 9 10 11 sudo firewall-cmd --permanent --change-interface=eth-infra --zone=internalsudo firewall-cmd --permanent --change-interface=eth-mgmt --zone=internalsudo firewall-cmd --permanent --change-interface=eth-app --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.2.10" port protocol="tcp" port="443" accept' --zone=internalsudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.3.0/24" port protocol="tcp" port="22" accept' --zone=internalsudo firewall-cmd --reload
443 只放 WAF(172.16.2.10)。前端不需要对外暴露——WAF 是唯一入口。