最近帮一家小公司处理线上事故,后端服务半夜突然连不上数据库,登录服务器一看,问题根源不是数据库崩了,而是运维同事临时创建的部署账号家目录权限设成了 777,被人顺手改了 .bashrc,全局环境变量被替换,服务起不来。这种问题在真实环境里太常见了。Linux 账号和权限管理,听起来是每个用过 Linux 的人都会两下子的基本功,但真正上了生产服务器,因为账号建得不规范、权限给得太随意引发的故障,比内核崩溃出现的频率高得多。
这篇文章不打算讲教科书式的概念罗列,而是把我实际踩过的坑、排查过的故障、以及现在搭建服务器时固化成习惯的那套做法完整写出来。无论你是刚入门的运维、写代码但对服务器半生不熟的开发,还是自己买了云主机折腾个人站点的爱好者,照着这套思路去检查你的服务器,多少能提前排掉几个雷。
1. 账号管理不只是 useradd:从用户表结构到生命周期管理
1.1 建号之前先看懂三张表
很多人用 Linux 几年,useradd一敲就完事,但从来没打开过/etc/passwd。我强烈建议你先执行一次cat /etc/passwd,把文件结构看明白,因为后续所有账号问题,最后都要回到这三个文件里找答案。
/etc/passwd每一行是一个用户账号,冒号分隔了 7 个字段:
root:x:0:0:root:/root:/bin/bash deploy:x:1001:1001:Deploy User:/home/deploy:/usr/sbin/nologin依次是:用户名、密码占位符、UID、GID、注释信息、家目录、登录 Shell。
注意第二个字段现在几乎都是x,真正的密码哈希在/etc/shadow里。/etc/passwd是对所有用户可读的,如果把密码哈希放这里,等于把钥匙挂在门上。这也是 Linux 把密码拆出去的核心原因。
/etc/group管理用户组,每一行是组名、组密码占位符、GID、组成员列表。先弄清楚一个概念:用户创建时默认会有一个同名私有组,这个组的 GID 会写进/etc/passwd的第四个字段,叫主组(primary group)。而/etc/group里第四个字段列出来的组,是用户的附加组(supplementary group)。主组决定新文件的默认属组,附加组决定你能额外访问哪些资源,这两个东西混在一起是最常见的权限事故源头。
1.2 useradd、usermod、userdel 的组合用法
生产环境建号,我一般不用裸的useradd,因为不同发行版对默认值的处理不同,Debian/Ubuntu 上你敲useradd,可能连家目录都不给你建。现在我用这类命令组合:
useradd -m -d /home/deploy -s /bin/bash -G www-data -c "Deploy User" -e 2026-12-31 deploy-m:创建家目录,-d指定家目录路径。-s指定登录 Shell。如果是跑服务的账号,我通常给/usr/sbin/nologin,禁止交互登录。-G:加入附加组,比如让部署账号加入www-data组以便读写 Web 目录。-e:账号过期时间,配合内部规范,防止账号永久有效。
账号信息建错的时候用usermod改。这里有个我吃过亏的点:usermod -G如果不加-a,会把用户从原来的所有附加组里踢出去,只保留你指定的新组。比如一个用户原来在docker组,你敲usermod -G www-data zhangsan,他就失去 docker 权限了,而且你不会立刻发现。正确写法是:
usermod -aG www-data zhangsanuserdel同理。裸的userdel deploy只删账号不删家目录,服务器上会留一堆 UID 归属不清的孤儿文件。我一般用userdel -r deploy连家目录和邮件池一起删。但注意一点:如果一个 UID 曾经拥有过某些文件,删号之后这些文件的属主会显示成一串数字,将来新用户如果被系统分配了同一个 UID,他就会变成这些文件的新属主。这属于高风险的 UID 复用问题,后面细说。
1.3 僵尸账号、UID复用和过期清理
有段时间我接手一批旧服务器,发现/etc/passwd里有十多个三年前的离职员工账号,Shell 还是/bin/bash,家目录还在。这种僵尸账号是最容易被忽略的入口。
我的处理习惯是:
# 锁定账号,禁止登录 usermod -L zhangsan # 过期账号 chage -E 2025-01-01 zhangsanpasswd -l zhangsan也能锁定,但chage -E可以直接设定过期日期,比手动管理状态更可控。对于确定要删除的账号,动手之前先扫描它名下的文件:
find / -user zhangsan -ls 2>/dev/null如果文件很多,先chown -R给接手的人,再删号,避免留下 UID 孤儿。UID 复用的问题我在一次迁移中碰到过:旧系统里一个内部工具以 UID 1005 运行,后来这个 UID 被分配给了一个新员工账号,结果新员工发现自己能读那个工具的历史缓存文件,里面还有别人的临时数据。后来我定了一条规矩:内部系统账号固定使用特定 UID 段(比如 1000-1999 给人,2000+ 给服务账号),并登记在案,禁止随意复用。
2. Linux 权限模型拆解:普通权限、ACL 与隐藏位
2.1 为什么 rwx 只是第一层筛子
聊权限不能只聊那些rwxr-xr-x的字母,你得先理解文件和目录的权限语义完全不同。
文件的r是能读内容,w是能改内容,x是能当成程序执行。目录的r是能列出目录里的文件名,w是能在目录里新建或删除文件,x是能进入目录。这里最容易被忽略的是:如果目录没有x权限,哪怕你有r,你也进不去,列出名字有什么用?反过来,很多服务报Permission denied,不是文件本身没权限,而是路径上某个目录的x权限没给够。
举个例子,Nginx 要读/var/www/html/index.html,它必须对/var、/var/www、/var/www/html三级目录都有x权限。很多人只chmod 755了最里层的目录,外层是系统默认的 755 没问题,但如果你自己建了个/data/app/logs,中间某层目录权限是 700,服务进程就会莫名其妙读不到文件。排查这类问题一定要用下面的方式逐层看:
namei -l /data/app/logs/app.lognamei会列出路径上每一层的权限,比一层层ls -ld快得多。
2.2 SUID、SGID、粘滞位:三个最容易被误解的位
普通 rwx 之外,还有三个特殊权限位,分别是 SUID(4)、SGID(2)和粘滞位(1)。它们的数字位会体现在 chmod 的第一位,比如4755、2770、1777。
SUID 的作用是:用户执行这个程序时,进程的有效身份是程序属主,而不是执行者本人。最常见的例子是/usr/bin/passwd,普通用户修改自己的密码时,需要写/etc/shadow,这个文件只有 root 能写,靠的正是 passwd 程序上的 SUID 位。但 SUID 是双刃剑,任何一个带 SUID 的程序如果有漏洞,就可能被人利用来读取 root 才该访问的文件。所以我在巡检时经常会扫一遍全系统的 SUID 文件:
find / -perm -4000 -type f 2>/dev/null有人可能会问,SGID 用在目录上是什么意思。我给协作目录设置 SGID 后,目录里新建的文件和目录会继承目录的属组,而不是创建者自己的主组。这对共享目录来说非常实用。比如团队共享目录/data/shared属组是devteam,你给它加上 SGID:
chgrp devteam /data/shared chmod 2770 /data/shared之后不管谁在这个目录里建文件,属组都会自动是devteam,不用每次手工chgrp。
粘滞位更简单,/tmp就是drwxrwxrwt。粘滞位的作用是:在公共目录里,你可以创建文件,也能改自己的文件,但你不能删别人创建的文件。没有粘滞位,一个777的公共目录里,任何用户都可以删掉其他人的文件。判断目录是否设置了粘滞位,看权限尾部的t就行。
2.3 umask:默认权限是怎么来的
每次新建文件,系统不会直接给你666或777,而是用默认值减去 umask。当前 shell 的 umask 用umask命令查看,一般默认是022。
- 新建文件:
666 - 022 = 644,即rw-r--r--。 - 新建目录:
777 - 022 = 755,即rwxr-xr-x。
这里有个细节:文件默认本来就不应该给执行权限,所以是666,目录因为需要x来进入,所以是777。减法的结果是文件 644、目录 755,这也解释了为什么你touch出来的脚本总要先chmod +x才能跑。
如果目录是多人协作场景,umask 建议设成002,这样新文件是664、新目录是775,组内成员可写。修改用户默认 umask,可以写在/etc/profile、/etc/bash.bashrc或/etc/login.defs里。但注意,systemd 管理的服务进程默认 umask 是022,跟 shell 里的设置无关。我曾经遇到开发在容器里改了 umask 以为生效了,结果服务以 systemd 方式启动后还是生成 644 文件,导致协作目录里别人没法改配置。最后是在 service 文件里加了:
[Service] UMask=0002这才符合预期。
3. chmod/chown 实战:高频误区和完整排查链路
3.1 符号模式与数字模式:什么时候用哪个
chmod有两种写法,一种是chmod u+x file,一种是chmod 755 file。我的经验是:交互式调整时用符号模式,能清楚表达意图;批量配置和脚本里用数字模式,结果确定、不易被当前 umask 干扰。
举几个常用的:
chmod u+x script.sh # 给属主加执行权限 chmod g-w config.ini # 去掉属组的写权限 chmod -R 755 /data/app # 递归设置chown的格式是属主:属组,比如:
chown -R deploy:devteam /data/app这里的坑是,很多人只chown了属主,忘了:组名,结果文件属组还是 root,应用账号能读不能写。写完chown建议立刻ls -ld确认一下。
3.2 一次 Permission denied 的完整排查链路
遇到应用报Permission denied,不要上来就chmod 777。777是解决问题的捷径,也是把服务器拱手让人的捷径。我现在的排查顺序是固定的:
第一步,确认服务进程到底是谁在跑:
ps aux | grep nginx以 nginx 为例,worker 进程属主是www-data还是nginx,不同发行版不一样。先搞清楚身份,再谈权限。
第二步,用id看这个用户属于哪些组:
id www-data第三步,逐层检查目标路径权限。重点不是看最后一层,而是每一层目录:
namei -l /var/www/html/index.html第四步,检查 ACL 和隐藏属性。普通 rwx 显示为+时,说明存在 ACL:
getfacl /var/www/html lsattr /var/www/htmllsattr检查i(不可修改)属性。有一次我排查了一个多小时,chmod、chown都试遍了还是报权限错误,最后发现文件被加了chattr +i,任何写入都被拒绝。那个时刻真的会让人怀疑人生。
第五步,如果系统开启了 SELinux 或 AppArmor,还需要确认安全上下文:
getenforce ls -Z /var/www/html/index.html有时候chmod是对的,但 SELinux 的 type 标签不对,一样给你拦下来。这类问题在dmesg或/var/log/audit/audit.log里通常有avc: denied的明确记录。不建议直接setenforce 0关掉 SELinux,那等于把最后一道门也拆了,正确做法是调整文件上下文或者写对应的策略模块。
3.3 批量变更、误操作恢复和权限基线
批量操作最容易出事。chown -R和chmod -R跑在大目录上,一旦路径写错,可能直接把系统目录的属主改坏。我处理过一次同事在/根目录上执行chown -R deploy:deploy /的现场,命令还没跑完,他就发现sudo已经不可用了,因为/etc/sudoers的属主变成了 deploy。最后只能重启进单用户模式修复。
所以我现在的做法是:动工之前用getfacl把权限备份下来:
getfacl -R /data/app > /tmp/app-perms.acl真出了事,恢复也只是反着执行一次:
setfacl --restore=/tmp/app-perms.acl这套东西对普通权限也有效,因为它会把属主、属组、ACL 一起记录。另外我强烈建议给服务器建立一套“权限基线”,哪些关键目录必须是 root 拥有、哪些脚本必须无写权限,写进内部文档,巡检时直接比对。权限管理最怕的不是改了错权限,而是改了之后没人知道基线是什么。
4. sudo 提权:最小化配置、日志审计与后门排查
4.1 sudoers 语法和最小授权原则
多用户服务器需要提权,但直接交出 root 密码是最坏的做法。root 密码一旦多人知晓,根本查不出是谁执行了什么命令。sudo的价值在于:既能提权,又能记录,还能限制命令范围。
所有 sudo 配置都通过visudo来改,它会检查语法错误,避免你写错行导致 sudo 整个不可用。配置文件主文件是/etc/sudoers,但我习惯把业务配置拆到/etc/sudoers.d/下面,主文件只做 include,方便管理和备份。
一条最基本的规则长这样:
deploy ALL=(ALL) /usr/bin/systemctl restart nginx含义是:deploy用户在所有主机上可以以 root 身份执行 systemctl restart nginx 这一个命令。注意,如果用户还能执行/bin/bash、/usr/bin/vim这类命令,那白名单就形同虚设。vim 里可以:!bash直接弹 shell,find 也可以-exec执行任意命令。给 sudo 白名单一定要想清楚:这个命令本身有没有脱离参数约束的能力。
另一个常见需求是让某个组的人都能执行运维命令:
%ops ALL=(ALL) /usr/bin/systemctl4.2 sudo 日志审计与提权痕迹排查
启用 sudo 之后,我接手服务器第一件事就是检查日志是否完整。Debian/Ubuntu 看/var/log/auth.log,RHEL/CentOS 看/var/log/secure。日志里会记录每次 sudo 的时间、用户、执行命令。
我还建议在 sudoers 里加一行,把 sudo 命令统一记录到专属文件:
Defaults logfile=/var/log/sudo.log Defaults log_input, log_output第二步sudo.log会把用户敲进去的字符和命令输出也记录下来,对事后审计非常有帮助。开这个功能之后要注意磁盘占用,日志增长会比普通记录快很多,建议配合 logrotate 轮转。
如果有人试图绕过 sudo,也常在日志里留下痕迹。比如多次输错密码、反复尝试/etc/sudoers内容,在auth.log里都会出现大量authentication failure。我巡检时关注几个点:
/etc/sudoers和/etc/sudoers.d/里有没有出现诡异的 NOPASSWD。/root/.ssh/authorized_keys有没有新增的不认识公钥。/etc/passwd里有没有 UID 0 的非 root 用户。
判断 UID 0 账号用一条命令:
awk -F: '$3 == 0 {print $1}' /etc/passwd正常情况下只会输出root。如果多出来了别的账号,基本可以断定被留了后门。
4.3 NOPASSWD 与命令白名单的取舍
很多自动化脚本为了方便,给服务账号配了NOPASSWD,也就是 sudo 不需要密码。我能理解自动化场景下的需求,但建议限制范围,不要用这种写法:
deploy ALL=(ALL) NOPASSWD: ALL这相当于给 deploy 用户发了一张 root 终身会员卡。脚本需要免密,也应该只免密那几条固定命令,比如:
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart app关于 sudo 的密码缓存时间,默认是 15 分钟,意思是 15 分钟内同一终端再执行 sudo 不用重复输密码。时间短了影响效率,时间长了风险变高。我在安全要求高的服务器上会把timestamp_timeout调成 5:
Defaults timestamp_timeout=5对于内部堡垒机场景,我甚至建议用 0,每次都必须输密码。多一层确认,多一分审计价值。
5. 远程登录、密钥与密码策略:账号权限的另一半
5.1 SSH 公钥登录与 authorized_keys 的权限坑
账号权限管理在远程场景下,最容易被忽略也最致命的就是 SSH 配置。我见过很多人创建了用户、设置了密码,然后把密码通过微信发给对方。这种做法一旦被截获或者聊天记录泄露,服务器就等于拱手送人。生产环境我全面切换到密钥登录。
生成密钥对:
ssh-keygen -t ed25519 -C "deploy@company"然后把公钥追加到目标用户~/.ssh/authorized_keys。这里权限必须严格遵守:
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chown -R username:username ~/.sshSSH 服务端有StrictModes保护,如果authorized_keys或.ssh目录的权限过于宽松,sshd 会直接忽略这个公钥文件,导致明明密钥放对了却一直提示认证失败。我排查过太多次这种问题,基本都是因为公钥文件是从别处拷贝过来,属性不对。
同时建议在/etc/ssh/sshd_config里设置:
PermitRootLogin no PasswordAuthentication no禁止 root 直接 SSH 登录,禁止密码认证,改用密钥。普通用户需要提权时走 sudo,这样每一步操作都有记录。
5.2 PAM 密码策略与账号锁定
对于必须使用密码登录的内部系统,密码策略不能靠自觉。Linux 的 PAM 模块可以在/etc/security/pwquality.conf里配置密码复杂度,比如最小长度、字符种类。配合/etc/pam.d/common-password或/etc/pam.d/system-auth引入:
password requisite pam_pwquality.so retry=3密码输入错误可以配置账号锁定。我现在常用的方案是pam_faillock,连续失败 5 次锁定 15 分钟:
auth required pam_faillock.so preauth audit silent deny=5 unlock_time=900 auth required pam_faillock.so authfail audit deny=5 unlock_time=900注意不同发行版 PAM 配置文件的 organization 不同,Ubuntu 和 CentOS 写法有差异,改之前一定要先备份对应文件,并且保持当前会话不退出,用新窗口验证语法没问题再收工。直接在 PAM 上把自己锁在外面,是非常经典的翻车事件。
5.3 多用户环境的权限规划
我最后想强调的是,账号和权限不能“按人头拍脑袋”。我现在给服务器规划用户时,会把账号分成几类:
| 账号类型 | 示例 | 登录方式 | 权限策略 |
|---|---|---|---|
| 人工运维账号 | zhangsan | SSH 密钥 | sudo 白名单 |
| 服务运行账号 | app-web | nologin | 仅服务所需文件权限 |
| 数据同步账号 | rsync-bot | nologin | 仅同步目录读写 |
| 审计备份账号 | backup | 密钥+限制命令 | 定期轮换密钥 |
服务运行账号一律nologin,不给 Shell,不给密码,也不加入过大的组。多个服务共用一个账号虽然方便,但出了问题无法区分责任,排查隔离也难。能用组来控制的权限,就不要单独给用户放行;能把权限缩小到目录级别,就不要给整个文件系统的写权限。
6. 一次线上故障复盘:权限管理到底能坏到什么样
6.1 故障现象:服务起不来,日志全是 Permission denied
去年有一回,公司一个 Java 后端服务在发版后频繁报错,日志里不断出现:
java.io.IOException: Permission denied at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method)服务账号是appuser,运行目录是/opt/backend,日志目录是/opt/backend/logs。第一反应是日志目录没写权限,但ls -ld一看是drwxr-xr-x,属主 root,属组 root。appuser 既不是属主也不在 root 组,当然写不进去。
6.2 排查过程:从 id、ls -ld 到日志定位
我这边完整的排查顺序是:
ps aux | grep java id appuser namei -l /opt/backend/logs/app.log ls -ld /opt/backend /opt/backend/logs getfacl /opt/backend/logs排查发现/opt/backend是 755,/opt/backend/logs也是 755,appuser 根本没有任何写权限。按常规操作,我把日志目录属主改掉:
chown -R appuser:appuser /opt/backend/logs chmod -R 750 /opt/backend/logs本以为重启服务就恢复了,结果照样Permission denied。这次报错变成了无法创建临时文件。用strace跟踪进程后发现,服务启动时会往/opt/backend/tmp写文件,而这个目录属主还是 root。这就是只看目标文件、不看全局导致的疏漏。
6.3 修复与加固:把账号权限做成制度化清单
最后修复很简单,把整个运行时目录按角色分配给 appuser:
chown -R root:root /opt/backend chown -R appuser:appuser /opt/backend/logs /opt/backend/tmp chmod 750 /opt/backend/logs但真正让我印象深刻的不是命令本身,而是复盘时的一个问题:为什么每次发版部署,都要手动去调一次权限?根本原因是部署脚本用 root 解压了压缩包,解压出来的文件属主全是 root,导致服务账号永远在“等 someone 来 chown”。
从那以后我定了一个规矩:应用代码包在打包阶段就固定好属主和权限,部署脚本里把chown -R appuser:appuser作为固定步骤写入,而不是等故障出现再补。权限管理如果每次都是被动救火,那问题永远解决不完。把账号规划、目录归属、sudo 白名单、密钥策略这些全部固化成模板,新服务器上线直接套用,才算是真正把权限这件事从“技能”变成了“制度”。
我个人在实际操作中的体会是:Linux 账号和权限管理,越是看起来基础的东西,越值得花时间建立规范性流程。一次chmod 777能换来一时的省事,但等服务器真的被人拿到权限,代价就不是一个命令能挽回的了。