本文初次写作时,能申请到的 ECS 大部分使用 CentOS 7.4。由于 CentOS 转向 Stream 模式发布,大部分开发者转向了 Debian 系发行版或其他与 RHEL 兼容的发行版(做的较好的有 AlmaLinux)。在政务云这边,2023 年新采购机器只能在 Ubuntu 18.04 LTS(本文更新时刚好进入付费支持(Expanded Security Maintenance)阶段)和龙蜥(Anolis OS 8)之间二选一。笔者的建议是:如果你不是一位 Linux 专家,那么在后者上遇到的问题至少可以翻翻 Red Hat 系统管理员手册,解决起来应该会容易一些。
Anolis OS 8 号称提供了 CentOS 8 的完全兼容,并且也提供了原地升级的解决方案。对于 50% 跑在 JVM 上,50% 跑在 Container 里的 Java Web Developer 来说,只要外围的工具套件没变,大概是不会有什么问题的。
因此本文将以龙蜥和 RH 系发行版为主介绍,对于使用 Debian 系发行版的开发者,具体命令可能会有所不同。因此在阅读本文时应当以领会精神为主,不宜照搬。
此外如果在堡垒机后有一台控制主机,可以将本文的某些步骤编写成 Ansible Playbook,但那是另一个主题了。
登录与用户设置
云服务商改为使用 VPN + 密码的方式登录,现已无法使用密钥。
首次使用修改堡垒机密码,新密码就目前而言应当使用内部发布的 pg
(Password Generator)生成,也可以使用在线密码生成网站 random.org 等提供随机字符串。
使用密钥登录堡垒机,用 ssh-keygen -t ed25519
生成 SSH 密钥对,方法参见 4.3 服务器上的 Git - 生成 SSH 公钥 或 # 配置 SSH 公钥 - Coding.net。默认的生成位置通常为 ~/.ssh/
目录,公钥后缀为 .pub
,使用 cat
命令输出所有内容并复制。堡垒机目前似乎不支持 Ed25519 密钥,使用 RSA(不加参数)。同一个项目下的机器需要互联时,推荐使用 Ed25519。
在堡垒机网页控制台添加公钥后,可通过 ssh -i [path to private key] [username]@[ip] -p [port]
直接登录堡垒机 UserShell,其中 SSH 端口可在堡垒机网页控制台查询,一般为 60022 / 30022。
设置非 root 用户并使用该用户部署项目
有关创建用户的其他参数,可参照 How to Create Users in Linux (useradd Command)
# 创建用户 yufan
# -m 表示为该用户创建 home 目录
# -G wheel 表示添加到 wheel 用户组
useradd -m -G wheel yufan
# 202305 补充
# 在一次网络攻防演练中,日常运维账户被攻破,所幸该账户不在 wheel 用户组中
# 因此最好不要把其他账户添加到 wheel 用户组中
useradd -m yufan
# 为用户修改密码(以及首次设置密码)
passwd yufan
# 把用户从 wheel 组中删除
gpasswd -d yufan wheel
# 删除用户
userdel yufan
使用 su
和 su -
切换到其他用户可能会由于某些环境变量未加载产生问题,如果碰到了,从堡垒机直接登录目标用户即可。
推荐使用 cloud-user
这样的用户名。
设置主机间的 SSH 登录
将中控主机的公钥写入其他主机的 authorized_keys
文件,如果你之前按照说明创建了 cloud-user
用户,路径应当是 /home/cloud-user/.ssh/authorized_keys
。
使用 ssh cloud-user@$HOST
测试登录其他主机,如果弹出密码输入框,则说明密钥机制出了问题。ssh cloud-user@$HOST -v
可以打印详情,对应地,在目标主机上设置 /etc/ssh/sshd_config
:
# Logging
SyslogFacility AUTH
LogLevel DEBUG
然后通过 tail /var/log/secure
查看 SSH 服务端日志,发现输出了:
Sep 13 16:36:11 inst57 sshd[14101]: Authentication refused: bad ownership or modes for file /home/cloud-user/.ssh/authorized_keys
一般来说是由于 SSH 服务端在 /etc/ssh/sshd_config
默认设置了 StrictModes = yes
,当 $HOME/.ssh
以及其中文件的权限不符合要求时将会禁用密钥登陆。解决方法是禁止其他用户读写该目录和其中的文件,使用:
chmod 700 $HOME/.ssh
chmod 600 $HOME/.ssh/authorized_keys
这个问题在 AlmaLinux 9.2 上暂时没有复现。
更改 root 用户密码并禁用 root 用户远程登录
同一批采购的机器设置的 root 密码都是相同的,并且这些机器默认相互间可以通过 SSH 连接,注意通过 passwd
修改密码。新密码就目前而言应当使用内部发布的目前已公开发布的 Password Generator - GitHub 生成,详细资料可见本站的 Simple password generator 。root 用户口令不需要你能倒背如流,不要试图使用「某个词组拼音首字母 + 特殊符号 + 数字序列」的方案。
编辑 /etc/ssh/sshd_config
设置 PermitRootLogin no
,然后使用 systemctl restart sshd
重启服务。进行这个操作前确保你有别的用户可以远程登录。
网络配置
修改本机的 hostname
默认情况下 shell 提示符显示 [root@localhost ~]#
或者 [root@230f94j5hsd8 ~]#
这样的信息,如果有多台 ECS 需要管理,容易造成混淆。可以为不同的 ECS 重新设定 hostname,添加到 /etc/hosts
中以便通过 hostname 远程连接到其他机器。
建议的命名规则是:对 10.28.168.59
这台机器,设置 hostnamectl set-hostname inst59.cloud
。
可能重新登录后 shell prompt 才会生效。
修改 DNS 设置
ECS 原本的 DNS 设置可能导致一些 URL 无法被解析。文件 /etc/resolv.conf
是由各发行版的网络配置工具生成的,本来是不建议直接修改的,但是云服务商可能做了一种防呆设计,导致机器重启后就会把 DNS 重置成某个特定地址(很遗憾,不能用)。
如果没有这个麻烦,可以使用 nmcli
工具。
# 查看所有网络端口
$ nmcli connection
NAME UUID TYPE DEVICE
System eth0 5fb**************5f3e03 ethernet eth0
cni-podman0 58a**************136428 bridge cni-podman0
# 展示需要修改的那个端口的配置
# ipv4.dns 项目为 - 表示自动获取
$ nmcli con show 5fb**************5f3e03
...
ipv4.dns: -
...
# 手动指定 DNS 配置
$ nmcli con mod 5fb**************5f3e03 ipv4.dns "202.96.113.34
114.114.114.114"
# 重启 NetworkManager.service
$ systemctl restart NetworkManager.service
DNS 已经被修改:
$ cat /etc/resolv.conf
# Generated by NetworkManager
search cloud
nameserver 202.96.113.34
nameserver 114.114.114.114
nameserver 10.28.118.1
# NOTE: the libc resolver may not support more than 3 nameservers.
# The nameservers listed below may not be recognized.
nameserver 10.28.118.2
如果使用了容器,需要重启容器以应用网络变更。
如果上面提到的那个麻烦存在,那么就需要一些手段了。对于容器应用,可以在创建容器时指定 DNS --dns=114.114.114.114
。
另一个一劳永逸的方法是编写一个 systemd Unit /etc/systemd/system/dns-setup.service
,这是一个 oneshot
类型的服务,在网络配置上线后执行特定的脚本覆写 /etc/resolv.conf
:
[Unit]
Description=Configure DNS setup messed up by vendor after system start
After=network-online.target
[Service]
ExecStart=/usr/bin/dns-setup.sh
RemainAfterExit=true
Type=oneshot
[Install]
WantedBy=multi-user.target
注册这个 Service:
systemctl daemon-reload
systemctl enable --now dns-setup.service
覆写脚本 /usr/bin/dns-setup.sh
可以这么写:
#!/bin/sh
cat > /etc/resolv.conf << EOF
# Generated by /usr/bin/dns-setup.sh
# Managed by /etc/systemd/system/dns-setup.service
search cloud
nameserver 202.96.113.34
nameserver 114.114.114.114
EOF
使用 reboot -n
看看效果。
检查防火墙配置
防火墙默认是关闭的,因为访问控制是由平台处理的,不同项目间的 ECS 访问已经做了隔离。如果同一个项目内的 ECS 不可相互信任,则需要开启本机防火墙。
# 查看防火墙状态
systemctl status firewalld
# 启用防火墙
systemctl enable --now firewalld
# 示例:开放 8080 端口
firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload
# 查看状态
iptables-save | grep 8080
桥接网络模式下对 rootful 容器的访问不受防火墙控制。Docker 用户需要修改 iptables
配置,Podman 用户应当使用 rootless 模式。
使用 cockpit 的用户需要注意 9090 端口在新增 zone 中可能默认设置为放行,该行为是不可接受的,至少增加 IP 白名单限制。
获取服务器的对外 IP
需要确定服务器在访问互联网的时候展现的是哪个 IP,可以借助 ipinfo.io
。
$ curl http://ipinfo.io | jq
{
"ip": "220.191.236.129",
"city": "Taizhou",
"region": "Zhejiang",
"country": "CN",
"loc": "28.6627,121.4331",
"org": "AS4134 CHINANET-BACKBONE",
"timezone": "Asia/Shanghai",
"readme": "https://ipinfo.io/missingauth"
}
使用包管理器安装和更新软件包
在系统的支持周期内使用包管理器管理软件可以解决大部分版本过旧带来的安全问题。此外通过包管理器安装的软件,其配置、日志等内容一般都在该在的位置。
包管理器 dnf 应该是在 RHEL 8 引入的,对于大部分人来说和 alias dnf='yum'
没什么区别(当然实际上是 alias yum='dnf'
)。
更新包的时候 mirrors.cloud.aliyuncs.com
可能会出 403:
Status code: 403 for http://mirrors.cloud.aliyuncs.com/anolis/8/AppStream/x86_64/os/repodata/repomd.xml (IP: 13.35.125.18)
要换成 mirrors.openanolis.cn
的源,修改 /etc/yum.repos.d
下的 .repo
文件即可。
[AppStream]
name=AnolisOS-$releasever - AppStream
baseurl=http://mirrors.openanolis.cn/anolis/$releasever/AppStream/$basearch/os
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-ANOLIS
gpgcheck=1
非高可用服务设置自动更新
对可用性要求不是那么苛刻的应用(例如内部服务),可以设置包管理器自动更新软件包。先确认 dnf-automatic
已安装,然后编辑 /etc/dnf/automatic.conf
。
如下配置将会自动安装安全更新,你也可以通过设置 upgrade_type = default
要求更新全部软件包。
upgrade_type = security
apply_updates = yes
使用 systemctl
启动相关的 Timer:
systemctl enable --now dnf-automatic.timer
要禁止更新某些特定的软件包,在 /etc/dnf/dnf.conf
中配置 exclude=package1 package2 package3*
。
安装 EDR 服务器监控软件
参考最新的通知公告。
cockpit
cockpit 是一个 RHEL 8 引入的基于 Web 的控制台,可以在上面查看机器负载,管理用户、软件包更新、网络、容器、磁盘 …… cockpit 在 ECS 中可能没有默认安装。
# 额外增加对容器和磁盘的管理模块
dnf install cockpit cockpit-podman cockpit-storaged
# 不要维持服务常开,可以通过 start / stop 在需要时启用
# systemctl enable --now cockpit.socket
systemctl start cockpit.socket
可以用 cockpit 设置自动安装安全补丁,但是系统会自动重启,做好服务自启动,以及关注前面提到的网络配置问题。
cockpit 默认会把端口添加到防火墙规则中,因此需要特别注意防火墙和 IP 白名单的问题。
禁止 root 用户使用(登录)cockpit
检查 /etc/pam.d/cockpit
文件可以看到这样一段配置,按理 root 用户是无法登录 cockpit 的。
# List of users to deny access to Cockpit, by default root is included.
auth required pam_listfile.so item=user sense=deny file=/etc/cockpit/disallowed-users onerr=succeed
然而 Anolis、AlmaLinux 9、RockyLinux 似乎都没有创建这个文件,这个机制就失效了。手动创建文件,内容如下:
# /etc/cockpit/disallowed-users
# List of users which are not allowed to login to Cockpit
root
访问没有公网映射的 cockpit
危险操作,请勿在家模仿
cockpit 是一项 web 服务,也就是说我们需要从互联网发起 HTTP(S) 请求到这个端口上去,对于没有公网映射的机器,我们也许需要通过其他机器转发流量。
现在假设主机 A (10.28.168.59:202.96.113.34) 已经使用了 cockpit 控制台并做好了防护策略。为主机 A 生成 SSH 密钥对并把公钥配置到主机 B (10.28.168.57) 的 authorized_keys
中,然后直接在主机 A 的 cockpit 中 add new host 即可。
不过为了防止主机 A boom 了导致 B 一起 boom 的情况发生,我们也可以设置由 A 上的 NGINX 负责转发流量到 B 的 cockpit。
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream cp57 {
server 10.28.168.57:9090;
}
server {
listen 8443 ssl;
listen [::]:8443 ssl;
server_name <site-name>;
allow <ip-of-my-place>;
deny all;
location / {
proxy_pass https://cp57;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
然后在主机 B 上新增 cockpit 配置 /etc/cockpit/cockpit.conf
:
[WebService]
Origins = https://<site-name>:8443 wss://<site-name>:8443
ProtocolHeader = X-Forwarded-Proto
ForwardedForHeader = X-Forwarded-For
RequireHost = false
AllowUnencrypted = true
ClientCertAuthentication = false
然后重启该重启的服务就 ok 了。
防火墙这块可以直接建立 DMZ zone,IP 段可以用一些 在线子网掩码计算器 来算,尽量把其他地址排除出去。
然后把 cockpit service 从其他 zone 中删除。
挂载数据盘
政务云 ECS 的数据盘通常是没给初始化的,使用 fdisk -l
可以查看:
# fdisk -l
Disk /dev/vda: 53.7 GB, 53687091200 bytes, 104857600 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x0009e68a
Device Boot Start End Blocks Id System
/dev/vda1 * 2048 83884031 41940992 83 Linux
Disk /dev/vdb: 536.9 GB, 536870912000 bytes, 1048576000 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
在本文的早期版本中,需要敲一堆分区、格式化指令,还要编辑 /etc/fstab
添加一条记录以使系统启动时自动挂载设备,现在可以在 cockpit 上配置。一般来说我会选择把数据盘挂载到 /opt
目录。
最后可以看看磁盘空间状况:
# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 3.8G 0 3.8G 0% /dev
tmpfs 3.8G 0 3.8G 0% /dev/shm
tmpfs 3.8G 468K 3.8G 1% /run
tmpfs 3.8G 0 3.8G 0% /sys/fs/cgroup
/dev/vda1 59G 3.2G 53G 6% /
/dev/vdb 500G 3.6G 497G 1% /opt
tmpfs 770M 0 770M 0% /run/user/0
本地化设置
使用 /usr/bin/tzselect
交互式地配置系统的时区,或者使用 timedatectl set-timezone 'Asia/Shanghai'
直接配置,或者往 .profile
里直接写 TZ='Asia/Shanghai'; export TZ
。
# timedatectl status
Local time: Wed 2023-05-31 17:07:30 CST
Universal time: Wed 2023-05-31 09:07:30 UTC
RTC time: Wed 2023-05-31 09:07:30
Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: no
NTP service: active
RTC in local TZ: no
与 NTP 服务同步时间可以使用 ntpdate ntp1.aliyun.com
。RHEL 8 中移除了 ntpdate
这个程序,使用 systemctl status chronyd
查看 NTP 同步服务状态。
有的同学可能会发现如此设置后时间并没有同步,查看 chronyc tracking
会一般会返回 Reference ID : 00000000 ()...Leap status : Not synchronised
。可以试试编辑 /etc/chrony.conf
文件,删除 cloud.aliyuncs.com
和 tbsite.net
域名下的 NTP 服务器,一般能解决问题。也可以在 pool.ntp.org
项目中选择一个 NTP 池。
# /etc/chrony.conf from AlmaLinux 9.2
pool 2.almalinux.pool.ntp.org iburst
有时候执行 man <command>
会出现这样的报错:-bash: warning: setlocale: LC_CTYPE: cannot change locale (zh_CN.UTF-8): No such file or directory
,这可能是在哪里设置了 export LANG="zh_CN.UTF-8";export LC_CTYPE="zh_CN.UTF-8"
这样的信息而系统中没有中文语言区域信息。使用 localectl list-locales
查看系统当前支持哪些区域信息:
# localectl list-locales
C.utf8
en_AG
en_AU
en_AU.utf8
……
通过 dnf install glibc-langpack-zh
安装中文区域信息并设置 localectl set-locale LANG=zh_CN.utf8
:
# localectl status
System Locale: LANG=zh_CN.utf8
VC Keymap: us
X11 Layout: us
不过这样一些程序的输出可能会变成中文,就不太方便在 Stack Overflow 搜索了。考虑到这个,建议维持原样,使用 localectl set-locale LANG=en_US.utf8
。
tree 命令可能仍会打印字符的 Unicode,使用 -N
参数,或配置 alias tree='tree -N'
。
如果服务端需要使用中文字体,例如在服务端渲染包含汉字的图片,则需要安装中文字体。Google Noto 字体项目的目标是为所有现代设备开发一款涵盖所有语言的和谐、优质的字体系列。通过 dnf install langpacks-core-font-zh_CN google-noto-sans-cjk-ttc-fonts google-noto-serif-cjk-ttc-fonts -y
添加 Google Noto CJK 字体支持,这套字体包含了思源黑体和思源宋体,换句话说,覆盖了有衬线字体和无衬线字体。fc-list
可以查看系统中安装的字体。
# fc-list :lang=zh
/usr/share/fonts/google-noto-cjk/NotoSansCJK-DemiLight.ttc: Noto Sans CJK TC,Noto Sans CJK TC DemiLight:style=DemiLight,Regular
...
/usr/share/fonts/google-noto-cjk/NotoSerifCJK-Bold.ttc: Noto Serif CJK TC:style=Bold
...
/usr/share/fonts/google-noto-cjk/NotoSansCJK-Bold.ttc: Noto Sans Mono CJK SC:style=Bold
...
NGINX 配置
参考本站的 NGINX 配置最佳实践。
容器化部署应用
RHEL 8 中的默认容器管理工具换成了 Podman,对于大部分人来说可以直接 alias docker='podman'
然后继续用「docker」。不过需要注意的是 Podman 是可以在 rootless 环境下运行的,这个时候一旦非 root 用户注销,他创建的容器就会和其他用户进程一样被咔掉(指收到 SIGTERM
信号)。
可以使用 loginctl enable-linger [USER]
派生用户管理器,在用户登出后继续保持运行,再通过编写 unit file 将其注册为用户服务(User Service)并交给 systemd 管理。
Podman 的优点是 rootless 模式(不过现在 Docker 也有了)以及使用门槛极低的 podman-secret(确实比环境变量好上一些)。
JDK
以安装 Eclipse Temurin™ JDK 为例,由于众所周知的网络问题,使用 清华大学开源软件镜像站 源。
添加 /etc/yum.repos.d/adoptium.repo
文件,内容如下,其中 baseurl
的具体路径可能需视不同发型版而定,不过可以在浏览器中打开该目录查看确认。
[Adoptium]
name=Adoptium
baseurl=https://mirrors.tuna.tsinghua.edu.cn/Adoptium/rpm/rhel8-x86_64/
enabled=1
gpgcheck=1
gpgkey=https://packages.adoptium.net/artifactory/api/gpg/key/public
使用包管理器安装:
dnf makecache
dnf install temurin-17-jdk -y
其他无法使用包管理器安装的 JDK 建议手动放在 /usr/lib/jvm
目录下。以 GraalVM 为例,推荐目录结构如下:
$ tree -L 2 /usr/lib/jvm
/usr/lib/jvm
└── graalvm-jdk-21.0.2+13.1
└── bin
...
Python 3
系统中默认安装 Python 3.6.8,移除 Python 2。不过 3.6 也是一个比较早期的版本了,有一些比较新潮的工具貌似是不支持的。可以通过包管理器 dnf install python39
安装 python 3.9.7。对应的,使用 pip 时应当调用 pip-3.9
。
Python 的镜像源指向了一个叫做 yum.tbsite.ne
的莫名其妙的网站,可以手动换成清华的源。
> pip-3.9 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
> pip-3.9 config set install.trusted-host pypi.tuna.tsinghua.edu.cn
> pip-3.9 config list -v
For variant 'global', will try loading '/etc/xdg/pip/pip.conf'
For variant 'global', will try loading '/etc/pip.conf'
For variant 'user', will try loading '/root/.pip/pip.conf'
For variant 'user', will try loading '/root/.config/pip/pip.conf'
For variant 'site', will try loading '/usr/pip.conf'
global.index-url='https://pypi.tuna.tsinghua.edu.cn/simple'
install.trusted-host='pypi.tuna.tsinghua.edu.cn'
也可以在使用 pip
工具时指定使用清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple
。
一些命令行工具
dnf install tree
,tree
可以以树形结构展示目录下的文件。
# tree -L 2 /opt
/opt
├── biz
│ └── vcrc
└── containers
├── external_redmine
└── redmine
如果你在服务器上部署了一些中间件,尽量通过 ECS 上安装的客户端访问它们,而不是在防火墙上打洞。
tmux
是一款终端中的「窗口管理器」,也可以在你断开 SSH 连接时维持 session。
mycli 是一款基于命令行的 MySQL client,支持自动补全,基本可以满足除了 dump 之外的其他需求。要导入 SQL 文件初始化表和数据,使用 source
命令。
有一些包管理器的仓库中也许能搜索到 mycli,不过这不是由 mycli 的开发者维护的,推荐通过 pip 安装以获得最新的版本:
pip3 install mycli
旧版本的 Python(例如 3.6)可能会缺失一些 Module,可以使用 pip3 install mycli==1.20.0 pymysql==0.9.2
指定早期版本。
Redis 客户端方面可使用基于命令行的 Redis client —— iredis,通过 pip 安装:
pip3 install iredis
或者在没有 Python 环境的情况下使用如下命令安装:
wget https://github.com/laixintao/iredis/releases/latest/download/iredis.tar.gz && tar -xzf iredis.tar.gz && ./iredis