用户与服务管理
# 第 9 章 用户与服务管理
# 目录介绍
# 9.1 用户管理
# 9.1.1 useradd / usermod / userdel——用户生命周期
#!/bin/bash
# ===== useradd —— 创建用户 =====
# 基本创建(使用系统默认值)
useradd alice
# 完整创建(所有参数显式指定)
useradd -m \ # 创建家目录 (/home/bob)
-s /bin/bash \ # 登录 Shell
-c "Bob Smith" \ # 注释(全名)
-u 1500 \ # 指定 UID
-g developers \ # 主组
-G docker,wheel \ # 附加组
-d /home/bob \ # 指定家目录路径
-e 2025-12-31 \ # 账户过期日期
bob
# 创建系统用户(不创建家目录,UID < 1000)
useradd -r -s /usr/sbin/nologin appuser
# ===== 查看默认值 =====
useradd -D # 显示创建用户的默认值
# 输出:GROUP=100 HOME=/home INACTIVE=-1 EXPIRE= SHELL=/bin/bash
# 默认配置文件:/etc/default/useradd 和 /etc/login.defs
# ===== usermod —— 修改用户 =====
usermod -l newname oldname # 修改用户名
usermod -u 2000 alice # 修改 UID
usermod -g staff alice # 修改主组
usermod -aG docker alice # 追加附加组(-a = append)
usermod -aG sudo,wheel alice # 追加多个组
usermod -s /bin/zsh alice # 修改 Shell
usermod -L alice # 锁定用户
usermod -U alice # 解锁用户
usermod -e 2026-12-31 alice # 设置账户过期日
# ⚠️ usermod -G 不加 -a 会覆盖所有附加组
usermod -G docker alice # ❌ 覆盖——alice 只剩 docker 组
usermod -aG docker alice # ✅ 追加——保留原有组 + docker
# ===== userdel —— 删除用户 =====
userdel alice # 只删除用户,保留家目录
userdel -r alice # 删除用户 + 家目录 + 邮件池
userdel -f alice # 强制删除(即使用户已登录)
# ===== 直接编辑 /etc/passwd 字段含义 =====
# root:x:0:0:root:/root:/bin/bash
# ① ② ③ ④ ⑤ ⑥ ⑦
# ① 用户名 ② 密码占位符(x=存在shadow中) ③ UID ④ GID
# ⑤ 注释 ⑥ 家目录 ⑦ 登录 Shell
# ===== 查看用户信息 =====
id alice # UID/GID/所属组
finger alice # 用户详细信息
groups alice # 用户所属的组
getent passwd alice # 查询 passwd 数据库条目
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
46
47
48
49
50
51
52
53
54
55
56
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
46
47
48
49
50
51
52
53
54
55
56
# 9.1.2 passwd / chage——密码与账户策略
#!/bin/bash
# ===== passwd —— 修改密码 =====
passwd alice # root 修改他人密码(不需旧密码)
passwd # 普通用户修改自己密码(需旧密码)
passwd -l alice # 锁定密码(等价于 usermod -L)
passwd -u alice # 解锁密码
passwd -S alice # 查看密码状态
# 输出:alice P 2025-06-01 0 90 7 -1
# 状态:P=密码有效 L=锁定 NP=无密码
# 上次修改 最小天数 最大天数 警告天数 过期宽限天数
passwd -e alice # 强制用户下次登录修改密码
passwd -n 7 alice # 设置最短密码修改间隔(天)
passwd -x 90 alice # 设置密码最长有效期(天)
passwd -w 7 alice # 密码过期前 7 天开始警告
passwd -i 14 alice # 密码过期后 14 天宽限期(过期还能登录)
# ===== chage —— 账户老化策略 =====
# 查看策略
chage -l alice
# 输出:
# 最近一次密码修改:2025-06-01
# 密码过期时间:2025-08-30
# 密码失效时间:从不
# 账户过期时间:从不
# 两次修改密码最小间隔:7
# 两次修改密码最大间隔:90
# 密码过期前警告:7
# 设置策略
chage -M 90 -m 7 -W 7 alice # 同上 passwd 参数
chage -E 2025-12-31 alice # 账户过期日期
chage -I 14 alice # 密码过期宽限期
chage -d 0 alice # 强制下次登录改密码
# ===== 密码复杂度策略(/etc/security/pwquality.conf)=====
# 或在 /etc/pam.d/common-password(Debian)/ /etc/pam.d/system-auth(RHEL)
# password requisite pam_pwquality.so retry=3 \
# minlen=12 difok=3 dcredit=-1 ucredit=-1 lcredit=-1 ocredit=-1
# minlen=12 : 最少 12 个字符
# difok=3 : 新密码至少 3 个字符不同于旧密码
# dcredit=-1 : 至少 1 个数字
# ucredit=-1 : 至少 1 个大写字母
# lcredit=-1 : 至少 1 个小写字母
# ocredit=-1 : 至少 1 个特殊字符
# ===== 检查弱密码用户 =====
check_weak_passwords() {
echo "检查空密码用户:"
awk -F: '($2 == "" || $2 == "!") && $1 != "root" {print $1}' /etc/shadow
echo ""
echo "检查 UID=0 的非 root 用户:"
awk -F: '($3 == 0 && $1 != "root") {print $1}' /etc/passwd
echo ""
echo "检查可登录的用户(非 nologin 和 false):"
awk -F: '$7 !~ /(nologin|false)$/ {printf "%-15s %s\n", $1, $7}' /etc/passwd
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 9.1.3 批量创建用户——从 CSV 导入
#!/bin/bash
# ===== 方式 1:从 CSV 文件批量创建 =====
# 格式:用户名,密码,全名,组1:组2
# alice,Pass1234,Alice Wang,developers:docker
# bob,Pass5678,Bob Li,developers
# carol,Pass9012,Carol Zhang,testers
cat > users.csv << 'CSV'
alice,Pass1234,Alice Wang,developers:docker
bob,Pass5678,Bob Li,developers
carol,Pass9012,Carol Zhang,testers
CSV
batch_create_users() {
local csv_file="$1"
local default_shell="${2:-/bin/bash}"
while IFS=',' read -r username password fullname groups_str; do
# 跳过空行和注释行
[[ -z "$username" || "$username" == \#* ]] && continue
echo "创建用户: $username"
# 确保附加组存在
if [[ -n "$groups_str" ]]; then
IFS=':' read -ra groups <<< "$groups_str"
for grp in "${groups[@]}"; do
getent group "$grp" > /dev/null || groupadd "$grp"
done
fi
# 创建用户
if id "$username" &>/dev/null; then
echo " 用户 $username 已存在,跳过"
else
useradd -m -s "$default_shell" -c "$fullname" "$username"
fi
# 添加附加组
if [[ -n "$groups_str" ]]; then
usermod -aG "$(echo "$groups_str" | tr ':' ',')" "$username"
fi
# 设置密码
echo "${username}:${password}" | chpasswd
# 强制首次登录改密码
chage -d 0 "$username"
echo " ✅ $username 创建完成,组: $groups_str"
done < "$csv_file"
}
# batch_create_users users.csv
# ===== 方式 2:用 newusers 批量导入 =====
# newusers 读取 /etc/passwd 格式的文本文件
cat > users_passwd_fmt.txt << 'TXT'
alice:x:1501:1501:Alice Wang:/home/alice:/bin/bash
bob:x:1502:1502:Bob Li:/home/bob:/bin/bash
TXT
newusers users_passwd_fmt.txt
# ===== 方式 3:循环+变量批量创建 =====
for i in $(seq 1 10); do
username="dev${i}"
useradd -m -s /bin/bash -g developers "$username"
echo "${username}:Password${i}" | chpasswd
done
# ===== 批量删除测试用户 =====
batch_delete_users() {
local prefix="$1" # 用户名前缀(如 dev)
for user in $(awk -F: -v pre="$prefix" '$1 ~ "^"pre {print $1}' /etc/passwd); do
echo "删除用户: $user"
userdel -r "$user"
done
}
# batch_delete_users "dev"
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 9.1.4 用户组管理——groupadd / gpasswd
#!/bin/bash
# ===== 创建组 =====
groupadd developers
groupadd -g 2000 designers # 指定 GID
# ===== 修改组 =====
groupmod -n newname oldname # 重命名组
groupmod -g 3000 developers # 修改 GID
# ===== 删除组 =====
groupdel developers # 删除组(不能是任何用户的主组)
# ===== 组成员管理 =====
# 添加用户到组
gpasswd -a alice developers # 添加
gpasswd -d alice developers # 移除
gpasswd -M alice,bob developers # 设置组员(覆盖)
gpasswd -A alice developers # 设置组管理员
# ===== 查看组成员 =====
members developers # 列出组内所有用户(需安装 members 包)
getent group developers # 查看组条目
grep "^developers:" /etc/group # 直接查看 /etc/group
# ===== /etc/group 格式 =====
# developers:x:2000:alice,bob,carol
# ① ② ③ ④
# ① 组名 ② 组密码(通常为空) ③ GID ④ 成员列表(逗号分隔)
# ===== 实战:项目目录权限设置 =====
# 场景:多个开发者共享 /opt/project 目录
setup_project_dir() {
local dir="/opt/project"
local group="developers"
groupadd -f "$group" # -f = 已存在不报错
mkdir -p "$dir"
chgrp -R "$group" "$dir" # 改变目录组
chmod 2775 "$dir" # 2775 = setgid + rwxrwxr-x
# setgid 位(2) = 在目录下创建的文件自动继承目录的组
echo "项目目录 $dir 已设置,所有 developers 组成员可读写"
}
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
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
# 9.1.5 sudo 配置——最小权限原则
#!/bin/bash
# ===== sudo 相关文件 =====
# /etc/sudoers 主配置文件(用 visudo 编辑)
# /etc/sudoers.d/ 分片配置目录(推荐)
# ===== visudo —— 安全编辑 sudoers =====
# 必须用 visudo 编辑——它会语法检查,防止锁死 sudo
visudo # 编辑 /etc/sudoers
visudo -f /etc/sudoers.d/myapp # 编辑分片文件
# ===== sudoers 语法速查 =====
# 格式:用户 主机=(运行身份) 命令
cat > /etc/sudoers.d/myapp << 'SUDOERS'
# 1. 单用户全权限(危险!)
# alice ALL=(ALL:ALL) ALL
# 2. 单用户无密码执行特定命令(推荐)
alice ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
alice ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx
alice ALL=(ALL) NOPASSWD: /usr/bin/systemctl status nginx
# 3. 组权限
%developers ALL=(ALL) /usr/bin/systemctl restart myapp
%developers ALL=(ALL) /bin/journalctl -u myapp
# 4. 禁止特定命令
alice ALL=(ALL) ALL, !/usr/bin/su, !/usr/bin/passwd root
# 5. 免密码执行所有命令(运维用户)
deploy ALL=(ALL) NOPASSWD: ALL
# 6. 只能以特定用户身份运行
webapp ALL=(www-data) NOPASSWD: /usr/bin/systemctl restart php-fpm
SUDOERS
# ===== 查看 sudo 权限 =====
sudo -l # 当前用户能执行哪些 sudo 命令
sudo -l -U alice # root 查看 alice 的 sudo 权限
# ===== sudo 日志 =====
# sudo 操作记录在 syslog 或专门的 sudo 日志中
grep sudo /var/log/auth.log # Debian/Ubuntu
grep sudo /var/log/secure # RHEL/CentOS
journalctl -u sudo # systemd 系统
# ===== 实战:创建受限运维用户 =====
create_op_user() {
local username="op"
useradd -m -s /bin/bash "$username"
mkdir -p /home/$username/.ssh
chmod 700 /home/$username/.ssh
# 只允许重启和查看日志
cat > "/etc/sudoers.d/$username" << SUDOERS
$username ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl status nginx, \
/usr/bin/systemctl restart php-fpm, \
/bin/journalctl -u nginx, \
/bin/journalctl -u php-fpm, \
/usr/bin/df -h, /usr/bin/free -h, /usr/bin/uptime
SUDOERS
echo "运维用户 $username 创建完成"
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 9.1.6 SSH 密钥管理——免密登录与批量分发
#!/bin/bash
# ===== 生成 SSH 密钥对 =====
ssh-keygen -t ed25519 -C "alice@server01" # Ed25519(推荐,现代算法)
ssh-keygen -t rsa -b 4096 -C "bob@server01" # RSA 4096(兼容性好)
# -t = 算法 -b = 密钥长度 -C = 注释 -f = 指定文件名
# 密钥存储位置
# 私钥(绝不可外泄): ~/.ssh/id_ed25519
# 公钥(可以分发): ~/.ssh/id_ed25519.pub
# ===== 免密登录(手动版)=====
# 方式 1:ssh-copy-id(推荐)
ssh-copy-id user@remote-server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@remote
ssh-copy-id -p 2222 user@remote # 指定端口
# 方式 2:手动复制(前一种失败时用)
cat ~/.ssh/id_ed25519.pub | ssh user@remote \
"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
# ===== 批量分发公钥脚本 =====
cat > batch_ssh_copy.sh << 'SCRIPT'
#!/bin/bash
PUB_KEY="$HOME/.ssh/id_ed25519.pub"
SERVERS=("server01" "server02" "server03" "server04")
USER="deploy"
if [[ ! -f "$PUB_KEY" ]]; then
echo "生成密钥..."
ssh-keygen -t ed25519 -N "" -f "${PUB_KEY%.pub}"
fi
for server in "${SERVERS[@]}"; do
echo "分发到 $server ..."
ssh-copy-id -i "$PUB_KEY" "${USER}@${server}" || {
echo "ssh-copy-id 失败,尝试手动复制..."
cat "$PUB_KEY" | ssh "${USER}@${server}" \
"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
}
done
echo "分发完成"
SCRIPT
# ===== SSH 安全加固 =====
# 编辑 /etc/ssh/sshd_config
cat > /etc/ssh/sshd_config.d/security.conf << 'SSHD'
# 禁止 root 直接登录
PermitRootLogin no
# 禁止密码登录(只用密钥)
PasswordAuthentication no
# 禁止空密码
PermitEmptyPasswords no
# 限制登录尝试
MaxAuthTries 3
# 限制登录用户
AllowUsers alice bob deploy
# AllowGroups developers
# 修改默认端口(可选,减少扫描)
# Port 2222
# 空闲超时
ClientAliveInterval 300
ClientAliveCountMax 2
SSHD
systemctl reload sshd
# ===== 撤销某人的 SSH 公钥 =====
revoke_ssh_key() {
local username="$1"
sed -i "/${username}@/d" /home/*/ssh/authorized_keys 2>/dev/null
# 或精确撤销:从 known_hosts 和 authorized_keys 同时清理
echo "已撤销 ${username} 的公钥"
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 9.1.7 登录审计与安全——谁在什么时候做了什么
#!/bin/bash
# ===== 查看谁在线 =====
who # 当前登录的用户
w # 更详细(在做什么)
last # 历史登录记录
last -n 20 # 最近 20 次
last alice # alice 的登录历史
lastb # 失败登录尝试(需要读取 /var/log/btmp)
lastb -n 20 # 最近 20 次失败
# ===== 登录失败排查 =====
# 查看暴力破解
grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10
# 查看被攻击 IP
grep "Failed password" /var/log/auth.log | grep -oP '\d+\.\d+\.\d+\.\d+' | sort | uniq -c | sort -rn | head -10
# ===== 命令审计(记录所有用户执行的命令)=====
# 方式 1:在 /etc/profile 最后添加
# export PROMPT_COMMAND='history -a; logger -p local1.notice "[AUDIT] $(whoami) [$(who am i | awk "{print \$1}")]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//")"'
# 用户命令会发送到 syslog (local1.notice)
# 方式 2:使用 auditd 监控关键文件
# auditctl -w /etc/passwd -p wa -k passwd_changes # 监控 passwd 写操作
# auditctl -w /etc/shadow -p wa -k shadow_changes # 监控 shadow 写操作
# auditctl -w /etc/sudoers -p wa -k sudoers_changes # 监控 sudoers 变更
# ===== 登录安全脚本:检测异常登录 =====
cat > login_monitor.sh << 'SCRIPT'
#!/bin/bash
# 放入 /etc/profile.d/ 或 SSH PAM 中执行
THRESHOLD=5 # 5 分钟内失败 5 次 = 异常
# 检查最近失败登录(仅 root 权限可读 /var/log/btmp)
if [[ -f /var/log/btmp ]]; then
recent_fails=$(lastb -s -5min 2>/dev/null | wc -l)
if (( recent_fails > THRESHOLD )); then
echo "[$(date)] 异常:最近 5 分钟有 ${recent_fails} 次失败登录"
# 提取攻击来源 IP
lastb -s -5min 2>/dev/null | awk 'NF>0 {print $3}' | sort | uniq -c | sort -rn \
| while read count ip; do
if [[ "$count" -ge 3 ]]; then
echo " 攻击者 IP: $ip ($count 次)"
# 可选:加入 iptables 黑名单
# iptables -A INPUT -s $ip -j DROP
fi
done
fi
fi
# 检查 root 直接登录(如果禁用了 root 登录则告警)
root_logins=$(last root | grep -v "never logged in" | wc -l)
if (( root_logins > 0 )); then
echo "[$(date)] 警告:root 用户最近有登录记录"
fi
SCRIPT
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
46
47
48
49
50
51
52
53
54
55
56
57
58
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
46
47
48
49
50
51
52
53
54
55
56
57
58
# 9.1.8 锁定用户与强制踢出
#!/bin/bash
# ===== 锁定账户 =====
usermod -L alice # 锁定——在 /etc/shadow 密码前加 !
passwd -l alice # 同上
# 解锁:
usermod -U alice
passwd -u alice
# ===== 设置账户过期 =====
usermod -e 1970-01-01 alice # 设置过期日到过去 = 立即失效
# ===== 修改 Shell 禁止登录 =====
usermod -s /usr/sbin/nologin alice # 禁止交互登录
usermod -s /bin/false alice # 更彻底——连非交互都禁止
# ===== 强制踢出用户 =====
# 查看登录会话
who -u # 显示 PID
# alice pts/0 2025-06-10 14:30 . 12345
# 给用户发消息
write alice pts/0 <<< "系统维护,5 分钟后将断开连接"
# 踢出指定用户
pkill -KILL -u alice # 杀死 alice 所有进程
kill -9 12345 # 杀死指定 PID 的会话
# 踢出所有除 root 外的用户
kick_all_users() {
local exclude_user="${1:-root}"
who | awk -v ex="$exclude_user" '$1 != ex {print $1, $2, $5}' \
| while read user tty pid; do
echo "踢出用户 $user (TTY:$tty PID:$pid)"
kill -9 "$pid" 2>/dev/null || true
done
}
# ===== 限制用户资源(/etc/security/limits.conf)=====
cat > /etc/security/limits.d/restrict.conf << 'LIMITS'
# 限制 developer 组的资源
@developers hard nproc 100 # 最大进程数
@developers hard nofile 4096 # 最大打开文件数
@developers hard maxlogins 3 # 最大同时登录数
@developers hard cpu 60 # CPU 时间限制(分钟)
LIMITS
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
46
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
46
# 9.2 服务管理
# 9.2.1 systemctl——现代 Linux 服务管理核心
#!/bin/bash
# ===== 服务状态操作 =====
systemctl status nginx # 查看状态(最常用)
systemctl start nginx # 启动
systemctl stop nginx # 停止
systemctl restart nginx # 重启
systemctl reload nginx # 重载配置(不中断服务)
systemctl enable nginx # 开机自启
systemctl disable nginx # 取消自启
systemctl is-active nginx # 检查是否运行
systemctl is-enabled nginx # 检查是否开机自启
systemctl is-failed nginx # 检查是否启动失败
# ===== 查看列表 =====
systemctl list-units --type=service # 所有已加载的服务
systemctl list-units --type=service --state=running # 正在运行的服务
systemctl list-units --type=service --state=failed # 启动失败的服务
systemctl list-unit-files --type=service # 所有已安装的服务(含未加载的)
# ===== 日志查看 =====
journalctl -u nginx # 查看服务日志
journalctl -u nginx -f # 实时跟踪日志(类似 tail -f)
journalctl -u nginx --since "10 minutes ago" # 最近 10 分钟
journalctl -u nginx --since "2025-06-10" # 指定日期
journalctl -u nginx -n 50 # 最近 50 行
journalctl -u nginx -p err # 只看错误级别日志
# ===== 服务依赖 =====
systemctl list-dependencies nginx # 查看服务依赖树
systemctl list-dependencies nginx --reverse # 查看哪些服务依赖 nginx
# ===== 屏蔽服务(比 disable 更强)=====
systemctl mask nginx # 屏蔽——禁止手动启动和开机自启
systemctl unmask nginx # 解除屏蔽
# ===== 失败服务自动重启(在 Unit 文件中配置)=====
# Restart=on-failure 异常退出自动重启
# RestartSec=10s 重启间隔
# StartLimitBurst=5 允许的重启次数
# StartLimitInterval=60s 计数周期
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
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
# 9.2.2 自定义 systemd Unit 文件
#!/bin/bash
# ===== 基础服务模板 =====
# 位置:/etc/systemd/system/myapp.service
cat > /etc/systemd/system/myapp.service << 'UNIT'
[Unit]
Description=My Application Service
Documentation=https://example.com/docs
After=network.target
Wants=redis.service
[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python /opt/myapp/main.py
ExecStartPre=/opt/myapp/check_config.sh # 启动前检查
ExecStartPost=/opt/myapp/notify_started.sh # 启动后通知
ExecStop=/bin/kill -TERM $MAINPID
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
StartLimitBurst=5
StartLimitInterval=60s
# 资源限制
LimitNOFILE=65535
LimitNPROC=4096
MemoryMax=1G
MemoryHigh=800M # 软限制——超了会 throttle
CPUQuota=200%
# 环境变量
Environment="APP_ENV=production"
Environment="PORT=8080"
EnvironmentFile=/etc/myapp/env
# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# 安全加固
ProtectSystem=strict # 只读系统目录
ProtectHome=true # 禁止访问 /home
NoNewPrivileges=true # 禁止提权
PrivateTmp=true # 独立 /tmp
[Install]
WantedBy=multi-user.target
UNIT
# 重载并启动
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
# ===== systemd Unit 类型对比 =====
# Type=simple :默认,ExecStart 启动的主进程即主服务
# Type=forking :老旧守护进程模式(启动后 fork 到后台),需配合 PIDFile
# Type=oneshot :一次性任务(类似 init.d 脚本),完成后退出
# 配合 RemainAfterExit=yes 表示"执行完也算 running"
# Type=notify :服务启动后发 sd_notify() 通知 systemd
# Type=idle :等其他 job 完成再启动
# ===== 定时器 Unit(替代 cron)=====
# 位置:/etc/systemd/system/cleanup.timer
# 位置:/etc/systemd/system/cleanup.service
cat > /etc/systemd/system/cleanup.service << 'TSVC'
[Unit]
Description=Daily cleanup job
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh
TSVC
cat > /etc/systemd/system/cleanup.timer << 'TIMER'
[Unit]
Description=Run cleanup daily at 3AM
[Timer]
OnCalendar=daily
OnCalendar=*-*-* 03:00:00
Persistent=true # 错过了补执行
RandomizedDelaySec=600 # 随机延迟 10 分钟(避免所有机器同时执行)
[Install]
WantedBy=timers.target
TIMER
systemctl enable cleanup.timer
systemctl start cleanup.timer
systemctl list-timers
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 9.2.3 service / chkconfig——兼容传统 SysV Init
#!/bin/bash
# ===== service 命令(SysV Init 时代)=====
# systemd 系统已重定向到 systemctl,但 service 仍可使用
service nginx start
service nginx stop
service nginx restart
service nginx status
service nginx reload
service --status-all # 列出所有服务状态
# ===== chkconfig(管理开机自启)=====
chkconfig --list # 列出所有服务自启状态
chkconfig nginx on # 开机自启
chkconfig nginx off # 禁止自启
chkconfig --add myapp # 添加服务(从 /etc/init.d/)
# ===== Systemd 兼容命令映射 =====
# service nginx start → systemctl start nginx
# chkconfig nginx on → systemctl enable nginx
# service nginx status → systemctl status nginx
# service --status-all → systemctl list-units --type=service
# ===== 旧式 init.d 脚本模板 =====
# 仅在无 systemd 的系统上使用(如旧版 CentOS 6、Alpine Linux)
# 位置:/etc/init.d/myapp
cat > /etc/init.d/myapp << 'INITSCRIPT'
#!/bin/bash
# chkconfig: 2345 80 20
# description: My Application Service
# 2345 = 在 runlevel 2,3,4,5 开启
# 80 = 启动优先级(S80)
# 20 = 关闭优先级(K20)
PIDFILE="/var/run/myapp.pid"
LOGFILE="/var/log/myapp.log"
CMD="/usr/bin/python /opt/myapp/main.py"
start() {
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
echo "myapp 已在运行"
return 1
fi
echo "启动 myapp..."
nohup $CMD >> "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
echo "启动完成,PID: $(cat "$PIDFILE")"
}
stop() {
if [[ ! -f "$PIDFILE" ]]; then
echo "myapp 未运行"
return 1
fi
local pid=$(cat "$PIDFILE")
echo "停止 myapp (PID: $pid)..."
kill -TERM "$pid"
rm -f "$PIDFILE"
echo "已停止"
}
case "$1" in
start) start ;;
stop) stop ;;
restart) stop; sleep 1; start ;;
status)
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
echo "myapp 运行中 (PID: $(cat "$PIDFILE"))"
else
echo "myapp 已停止"
fi
;;
*) echo "用法: $0 {start|stop|restart|status}" ;;
esac
INITSCRIPT
chmod +x /etc/init.d/myapp
chkconfig --add myapp
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 9.2.4 服务健康检查——不仅是端口存活
#!/bin/bash
# ===== 层次 1:端口是否监听 =====
check_port() {
local port="$1"
if ss -tlnp | grep -q ":$port "; then
echo "✅ 端口 $port 在监听"
return 0
else
echo "❌ 端口 $port 未监听"
return 1
fi
}
# ===== 层次 2:服务是否有响应(HTTP)=====
check_http_response() {
local url="$1"
local timeout="${2:-5}"
local code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout "$timeout" "$url")
if [[ "$code" == "200" ]]; then
echo "✅ HTTP $code: $url"
return 0
else
echo "❌ HTTP $code: $url"
return 1
fi
}
# ===== 层次 3:业务功能检查(端到端)=====
check_api_health() {
local endpoint="$1"
# 检查业务 API 是否返回预期数据
response=$(curl -s "$endpoint" 2>/dev/null)
if echo "$response" | jq -e '.status == "ok"' > /dev/null 2>&1; then
echo "✅ API 正常: $endpoint"
return 0
else
echo "❌ API 异常: $endpoint (响应: $response)"
return 1
fi
}
# ===== 综合健康检查 =====
cat > service_health_check.sh << 'SCRIPT'
#!/bin/bash
# 可放入 cron 每 1 分钟执行一次
HOST="localhost"
# 定义检查项:名称|检查方式|地址|失败动作
CHECKS=(
"nginx|http|http://${HOST}:80/health|restart_nginx"
"app_api|api|http://${HOST}:8080/api/health|restart_app"
"redis|tcp|${HOST}:6379|restart_redis"
"mysql|tcp|${HOST}:3306|alert_only"
)
restart_nginx() { systemctl restart nginx; }
restart_app() { systemctl restart myapp; }
restart_redis() { systemctl restart redis; }
alert_only() { echo "[FATAL] $(date) $1 不可达" >> /var/log/health_alert.log; }
for check in "${CHECKS[@]}"; do
IFS='|' read -r name type target action <<< "$check"
ok=false
case "$type" in
http)
curl -sf --connect-timeout 3 "http://${target}" >/dev/null 2>&1 && ok=true
;;
tcp)
timeout 2 bash -c "echo > /dev/tcp/${target%:*}/${target#*:}" 2>/dev/null && ok=true
;;
api)
curl -sf "$target" 2>/dev/null | jq -e '.status=="ok"' >/dev/null && ok=true
;;
process)
pgrep -x "$target" >/dev/null && ok=true
;;
esac
if $ok; then
echo "[$(date)] ✅ $name 正常"
else
echo "[$(date)] ❌ $name 异常,执行恢复动作: $action"
eval "$action $name"
fi
done
SCRIPT
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 9.2.5 优雅重启——零停机更新策略
#!/bin/bash
# ===== 策略 1:reload(重载配置,不中断连接)=====
# Nginx
nginx -t && systemctl reload nginx # 先测试配置,再重载
# PHP-FPM
systemctl reload php-fpm # 等待旧 worker 处理完再退出
# ===== 策略 2:pre-fork 预热(重启前启动新进程)=====
# 场景:更新 Node.js 应用(不支持 reload)
graceful_restart_node() {
local app_dir="$1"
local port="$2"
local new_port="$((port + 1))"
# 1. 在新端口启动新进程
cd "$app_dir"
PORT="$new_port" nohup node server.js > /tmp/app_new.log 2>&1 &
new_pid=$!
# 2. 等待新进程就绪
for i in $(seq 1 30); do
if curl -sf "http://localhost:$new_port/health" >/dev/null 2>&1; then
echo "新进程就绪 (PID: $new_pid)"
break
fi
sleep 1
done
# 3. 切换:用 iptables 把流量转到新端口
# 或通知 Nginx upstream 切换到新端口
iptables -t nat -D PREROUTING -p tcp --dport "$port" -j REDIRECT --to-port "$port"
iptables -t nat -A PREROUTING -p tcp --dport "$port" -j REDIRECT --to-port "$new_port"
# 4. 等待旧端口连接耗尽
sleep 5
old_pid=$(lsof -ti :$port)
if [[ -n "$old_pid" ]]; then
kill -TERM "$old_pid"
fi
echo "优雅重启完成(旧端口 $port → 新端口 $new_port)"
}
# ===== 策略 3:Nginx upstream 平滑切换 =====
cat > nginx_rolling_restart.sh << 'SCRIPT'
#!/bin/bash
# 每次重启一个 upstream server,逐个完成
# 需 Nginx upstream 配置 health_check
UPSTREAM_SERVERS=("192.168.1.10" "192.168.1.11" "192.168.1.12")
for server in "${UPSTREAM_SERVERS[@]}"; do
echo "从 upstream 移除: $server"
# 修改 Nginx 配置标记该 server down
ssh "$server" "systemctl restart myapp"
# 等待服务恢复
echo "等待服务恢复..."
for i in $(seq 1 30); do
if ssh "$server" "curl -sf http://localhost:8080/health" >/dev/null 2>&1; then
echo "$server 已恢复"
break
fi
sleep 2
done
# 恢复 upstream
echo "$server 重新加入 upstream"
sleep 10 # 给一些时间让连接分发稳定
done
echo "所有 server 优雅重启完成"
SCRIPT
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 9.2.6 灰度发布脚本——分批滚动更新
#!/bin/bash
# ===== 灰度发布策略 =====
# 10% → 观察 5 分钟 → 50% → 观察 5 分钟 → 100%
# 每个阶段检查错误率,异常则自动回滚
cat > canary_deploy.sh << 'SCRIPT'
#!/bin/bash
# 灰度发布入口脚本
APP="myapp"
NEW_VERSION="$1"
OLD_VERSION=$(cat /opt/${APP}/current_version 2>/dev/null || echo "unknown")
if [[ -z "$NEW_VERSION" ]]; then
echo "用法: $0 <新版本号>"
exit 1
fi
DEPLOY_DIR="/opt/${APP}"
HEALTH_URL="http://localhost:8080/health"
METRICS_URL="http://localhost:8080/metrics"
ROLLBACK_SCRIPT="${DEPLOY_DIR}/rollback.sh"
# ---- 阶段定义 ----
declare -A STAGES=(
["canary"]="10%"
["half"]="50%"
["full"]="100%"
)
# ---- 工具函数 ----
log() { echo "[$(date '+%H:%M:%S')] $*"; }
check_error_rate() {
local max_rate="${1:-5}" # 最大允许错误率(%)
# 从 metrics 端点获取 1 分钟错误率
local err_rate=$(curl -sf "$METRICS_URL" 2>/dev/null | jq -r '.error_rate_1m // 0')
if (( $(echo "$err_rate > $max_rate" | bc -l) )); then
return 1 # 错误率过高
fi
return 0
}
health_wait() {
local timeout="${1:-30}"
for i in $(seq 1 "$timeout"); do
if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
rollback() {
log "💀 执行回滚!"
if [[ -x "$ROLLBACK_SCRIPT" ]]; then
bash "$ROLLBACK_SCRIPT" "$OLD_VERSION"
else
log "没有回滚脚本,手动回滚到版本 $OLD_VERSION"
fi
exit 1
}
# ---- 灰度流程 ----
log "===== 灰度发布 $APP v${NEW_VERSION} (旧版: v${OLD_VERSION}) ====="
for stage in canary half full; do
percentage="${STAGES[$stage]}"
log "当前阶段: $stage (${percentage})"
# 部署新版本到该阶段节点
# 这里假设用 ansible 或 ssh 分发(简化为本地)
bash "${DEPLOY_DIR}/deploy_stage.sh" "$stage" "$NEW_VERSION"
# 等待服务健康
if ! health_wait 30; then
log "阶段 ${stage} 健康检查失败"
rollback
fi
# 观察期
log "观察期 5 分钟..."
sleep 300
# 检查错误率
if ! check_error_rate 3; then
log "阶段 ${stage} 错误率过高"
rollback
fi
log "阶段 ${stage} 通过 ✅"
done
log "===== 灰度发布完成: v${OLD_VERSION} → v${NEW_VERSION} ====="
echo "$NEW_VERSION" > "${DEPLOY_DIR}/current_version"
SCRIPT
# ===== 配合的 Nginx upstream 权重配置 =====
# 灰度发布需要 Nginx 支持动态调整 upstream 权重
cat > /etc/nginx/conf.d/upstream_weight.conf << 'NGINX'
upstream app_backend {
# canary 阶段:新版本权重 10%
# server 192.168.1.10:8080 weight=9; # 旧版
# server 192.168.1.10:8081 weight=1; # 新版(canary)
# half 阶段:50%
# server 192.168.1.10:8080 weight=1;
# server 192.168.1.10:8081 weight=1;
# full 阶段:100%
server 192.168.1.10:8080 weight=1; # 新版
# server 192.168.1.10:8081 down; # 旧版下线
keepalive 32;
}
NGINX
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# 9.3 新手陷阱与思考题
# 陷阱 Top 5
陷阱 1:usermod -G 忘记 -a 导致丢失所有附加组
# ❌ 覆盖附加组——用户可能失去 sudo/docker 权限
usermod -G docker alice # alice 只剩 docker 组
# ✅ 追加组
usermod -aG docker alice # 保留原有组 + docker
# 修复:查看用户当前组
id alice # 看缺少什么组
usermod -aG sudo,wheel alice # 补回来
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
陷阱 2:SSH 安全加固后自己也被锁在外面
# ❌ 改了 /etc/ssh/sshd_config 后没测试就直接重启 sshd
# 如果配置有误——再也连不上了!
# ✅ 在另一个终端保持 SSH 连接,测试新配置
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
# 修改配置...
sshd -t # 先测试语法
systemctl reload sshd # 用 reload 而不是 restart
# 在新终端测试能否登录
ssh localhost # 确认能登录后再关旧终端
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
陷阱 3:userdel 不删家目录留下安全隐患
# ❌ 只删除用户,家目录残留(含 SSH 密钥等敏感信息)
userdel alice
# ✅ 彻底删除
userdel -r alice
# 或事后手动清理
find /home -maxdepth 1 -nouser -exec rm -rf {} \;
1
2
3
4
5
6
7
2
3
4
5
6
7
陷阱 4:systemctl mask 后忘记 unmask 导致服务启动不了
# ❌ mask 后再 start 无任何提示但就是启动不了
systemctl mask nginx
systemctl start nginx # 静默失败
# 检查:
systemctl status nginx
# 输出:Loaded: masked (Reason: Unit nginx.service is masked.)
# ✅ 排查:
systemctl list-unit-files | grep masked
systemctl unmask nginx
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
陷阱 5:健康检查只检查端口,服务实际上是假死
# ❌ 端口在监听但不响应——假死
ss -tlnp | grep 8080 # 端口监听 ✓
curl http://localhost:8080/ # 但实际不响应 ✗
# ✅ 分层检查
# 1. 端口
# 2. HTTP 响应码
# 3. 业务数据正确性
# 4. 响应时间阈值
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 综合思考题
用户安全审计:写脚本列出系统上所有 UID=0 的用户、无密码用户、可登录但 90 天未修改密码的用户、家目录权限为 777 的用户。
批量迁移:将一批用户从旧服务器迁移到新服务器(包括用户条目、密码 hash、组关系、家目录数据),写出迁移脚本。
SSH 入侵防护:用 iptables + 脚本实现:对同一 IP 60 秒内 SSH 失败超过 5 次,自动封禁该 IP 1 小时,并记录到日志。
自定义健康检查服务:写一个 systemd service + timer,每 30 秒对关键 API 做健康检查,失败时自动重启对应服务,且连续失败 3 次时发送告警。
零停机部署完整方案:设计一个完整的零停机部署流程,包含:
- 健康检查端点
- Nginx upstream 动态切换
- 旧进程优雅退出(等待活跃连接完成)
- 自动回滚机制
上次更新: 2026/06/17, 12:47:39