1. 为什么“用户与组”在 Linux 系统里是地基而不是摆设
1.1 内核眼里只有数字,没有名字
我刚入行时接手过一台老服务器,某开发者反复提交工单说“服务起不来”,我登上去一看,日志里全是 Permission denied。排查了半天,最后发现是项目目录的属主被前任运维用chown -R改成了某个离职半个月的账号,而新账号压根不在那个组里。这个场景我后来见过太多次——用户与组管理看着基础,但几乎没有哪次线上故障能在绕过它的情况下真正解决。
Linux 内核本身并不认识zhangsan、lisi这类名字。它在 process、inode、socket 上只存两个整数:UID(用户 ID)和 GID(组 ID)。当某个进程尝试打开文件时,内核拿的是发起进程的 UID/GID 去和文件 inode 里的 UID/GID 比对,名字只是/etc/passwd、/etc/group这两个文本文件给人类准备的“翻译层”。所以“用户管理”的本质,是在维护一张从名字到数字的映射表,并保证映射关系稳定一致。
理解了这一点,很多诡异现象就有了解释。比如你从备份里恢复了一个目录,看到ls -l显示属主是一串数字而不是名字,那是因为原系统里那个 UID 对应的用户在这台机器上根本不存在。这类“孤儿文件”日常不显眼,等到权限出问题才让人抓狂。所以做用户与组管理,第一原则永远是:让名字和数字的对应关系尽量清晰、可预期、可审计。
1.2 三档权限的判定顺序,和你以为的不太一样
Linux 文件权限只有三档三位:owner、group、other。但“用户同时属于文件属主组,那到底算哪一档”这个问题,很多人栽过跟头。
内核的判定逻辑是顺序匹配:先看进程的有效 UID 是否等于文件的属主 UID,如果相等,直接按 owner 位的权限处理,后面两层全部跳过;如果不相等,再看进程是否属于文件的属主组(主组或附加组都算),如果属于,按 group 位处理;最后都不沾,才落到 other 位。
举个例子,某个项目目录drwxr-x---,属主是dev01,属组是devteam,而dev02既是devteam组成员,又恰好被设置成了该目录的属主(这种情况太常见了,因为chown -R dev02会把组也捎带改掉)。那么dev02的实际权限以 owner 位为准,也就是rwx,group 位里设置的r-x反而没生效。反过来说,如果你想用 group 位来精确控制某个人的读写权限,就得确保这个人不是文件属主,否则你的组权限形同虚设。
这也是为什么我强烈建议在日常管理中把“属主”和“属组”分开思考:属主一般只写一个管理账号,真正的协作控制尽量通过组完成。一个用户通过附加组可以同时属于前端组、后端组、运维组,从而实现“多对多”的项目协作。没有组这套机制,文件权限模型就只能停留在单用户独占,而组的存在才让多用户共享、最小权限成为可能。
2. 记账本到防火墙:/etc/passwd、/etc/shadow、/etc/group 逐字节拆解
2.1 三个文件的责任边界
用户管理对应的核心数据库是四个文件,但日常最常被讨论的是三个:/etc/passwd、/etc/shadow、/etc/group,还有一个/etc/gshadow容易被忽略。
/etc/passwd每行 7 个字段,用冒号分隔:
- 用户名
- 密码占位符(现代 Linux 一律是
x,表示真哈希已迁到 shadow) - UID
- 主组 GID
- GECOS 注释字段(通常是姓名、电话等,也可以塞一句话描述,比如“某项目管理员”)
- 家目录
- 登录 shell
/etc/shadow每行 9 个字段,是真正的密码保险箱:
- 用户名
- 密码哈希(
!或*表示锁定或不可登录) - 最后一次修改密码的日期(从 1970-01-01 起算的天数)
- 两次修改密码的最小间隔天数
- 密码最长有效天数
- 过期前警告天数
- 宽限期天数
- 账号过期日
- 保留字段
/etc/group每行 4 个字段:
- 组名
- 组密码占位符
- GID
- 附加成员列表(主组成员不在这个列表里,而是由
/etc/passwd的第 4 字段决定)
这三个文件的分工很有意思:passwd是“记账本”,记录身份和归属;shadow是“防火墙”,用最低权限保护密码哈希;group是“通讯录”,记录群组关系和附加成员。设计成这样,本质上是为了安全隔离——老的 Unix 系统把哈希直接放进passwd,任何能读/etc/passwd的人都能拿哈希跑字典攻击。所以现代系统把哈希挪到只有 root 可读的shadow,passwd里的x只是一个占位符。
2.2 别用手工编辑,也别乱改 GID
有经验的运维基本不会直接vi /etc/passwd,因为并发编辑很容易把文件写坏。标准做法是用vipw编辑passwd,用vigr编辑group,它们会自动加锁、结束后还会提示是否同步更新 shadow 文件。如果文件已经损坏,可以用pwck和grpck做一致性检查,它会逐行扫出“有用户但没家目录”“有 GID 但组不存在”之类的问题。
新手最容易犯的错是:为了图快,手动把某用户的 UID 改掉。如果只改passwd不改文件属主,这个用户所有旧文件的 owner 数字就变成了一个无名 UID。更稳妥的办法是直接改文件属主,或者用usermod -u配合find ... -exec chown批量同步。我个人的习惯是:命令行工具能解决的事绝不动手改文件,改文件前一定先备份,改完一定跑一遍 pwck。
/etc/gshadow则存放组密码和组管理员列表,一般纯命令行模式用不到,但如果你接触的是较老的发行版或企业内部统一认证场景,还是要知道它的存在。组密码这种机制用途很窄,多数人一辈子用不上,但知道“组也是可以有密码的”这个事实,能帮你避免某些文档误导。
3. 从零拉一个能用的账号:useradd 选项、默认值和两种账号套路
3.1 先搞清楚发行版默认值,再动手
useradd在不同发行版上的默认行为差异很大。Debian 系通常会自动创建家目录并复制/etc/skel,而 RHEL 系则默认不创建家目录,必须加-m。所以第一件事永远是执行useradd -D看看当前发行版的默认配置,配合/etc/login.defs里的CREATE_HOME、UMASK、PASS_MAX_DAYS等参数一起确认。
一个典型的“人类用户”创建命令长这样:
useradd -m -d /home/某开发 -s /bin/bash -c "某项目开发工程师" -G sudo,devteam 某开发-m创建家目录,-d指定家目录路径,-s指定登录 shell,-c写注释,-G挂附加组。创建后马上执行passwd 某开发设置初始密码,并让用户下次登录时自行修改。
如果创建完发现ls -ld /home/某开发的权限不对劲,多半是login.defs里的UMASK被设置成了077,导致家目录连属主自己以外的任何人都进不去。单独改目录权限很容易,但要注意它背后对应的安全策略,别把组协作目录的 umask 也一起顺手改成宽松值。
3.2 服务账号和人类账号要分开养
给程序跑的服务账号(比如nginx、redis、自研中间件)不该用普通用户的套路创建。标准做法是:
useradd -r -M -s /usr/sbin/nologin 某服务账号-r表示系统账号,UID 会落在系统保留范围(通常是 0-999),-M明确不创建家目录,shell 用nologin保证没人能直接登录进去跑命令。服务账号没有密码、没有家目录、没有交互 shell,这是刻意为之——攻击者即使拿下该账号,也无法获得可交互的登录环境。
很多部署脚本图省事,拿一个带密码的普通账号跑后台服务,这在安全上是给自己留后门。我建议的准则是:凡是人类不需要登录的进程,一律用nologin系统账号运行,密钥文件、配置目录的属主也指向它。这样即使某服务被攻破,能造成的破坏范围也会被限制在该账号的权限边界内。
3.3 /etc/skel 的“一次性”陷阱
/etc/skel目录里的文件会在创建新用户时自动复制到家目录,用来初始化.bashrc、.profile、环境变量模板等。它有个关键特性:只对新用户生效。你改了/etc/skel,老用户的家目录不会自动更新。所以最好的做法是把它当作发布基线来管理,一切模板变更走脚本批量同步,而不是指望已存在用户自己“感知”。
另外一个坑是/etc/skel里的文件如果带上了过于宽松的权限(比如某个配置文件写成0666),那每个新用户都会继承这个风险点。我一般会把skel里的敏感文件权限强行收敛到0600,再配合/etc/login.defs里的UMASK 027,从源头上让新用户的文件对“其他用户”不可读。
4. 修改、锁定、过期、删除:账号全生命周期这样管理才不埋雷
4.1 usermod:改一处,牵一发而动全身
usermod是最常用的账号修改工具,但它的选项里有几个容易踩坑的地方。
usermod -aG docker 某开发-aG中的-a表示 append,也就是“追加到附加组”。如果漏写了-a,命令会把该用户原有的附加组全部替换成docker,导致用户瞬间失去其他组权限。这个错误在自动化脚本里尤其致命,因为它往往不会立刻报错,而是等到用户在某个协作目录操作时才暴露。
改家目录也有讲究:
usermod -d /data/某开发 -m 某开发-d只修改/etc/passwd里的家目录路径,-m才会把原家目录内容移动过去。只用-d会导致用户登录后落在一个空目录里,旧文件还留在原处。我见过不止一次因为忘了加-m导致用户数据“看起来全丢了”的工单,其实文件都还在,只是路径映射断了。
修改用户名和 UID 同样要谨慎:
usermod -l 新名字 旧名字 usermod -u 新UID 新名字改名不改 UID 通常安全,因为文件属主数字没变。但改 UID 会让用户所有文件的属主数字发生变化,必须配合find /home/用户名 -exec chown 新UID:组 {} +同步修复。我强烈建议:除非有明确的安全审计要求,否则别动 UID,一个稳定不变的 UID 比花哨的名字重要得多。
4.2 passwd 和 chage:密码策略是账号安全的半条命
passwd不只是改密码,它还能完成账号锁定和解锁:
passwd -l 某用户 # 锁定 passwd -u 某用户 # 解锁-l的原理是在 shadow 哈希前加!,让任何密码都无法匹配。但要注意,锁定不等于删除,也不等于账号过期。锁定的账号如果存在 SSH 公钥认证,仍可能被登录——因为公钥认证根本不校验密码哈希。这是一个很多人忽略的漏洞:你锁了密码,但没锁公钥。
密码过期策略用chage管理更清晰:
chage -M 90 -W 7 -I 15 某开发-M 90表示密码最长使用 90 天,-W 7表示到期前 7 天开始警告,-I 15表示过期后还有 15 天宽限期。查看策略用chage -l 某开发,它会列出一张人类可读的表格,比直接翻 shadow 文件直观得多。
4.3 userdel:一个已经登录的用户,删起来没那么简单
userdel -r 某用户会删除用户并连带移除家目录,但我不建议在用户仍有进程运行时执行。正确姿势是先检查:
pgrep -u 某用户 find / -xdev -user 某用户 2>/dev/null如果用户的某个服务还在跑,直接删除会导致该进程变成无主进程,文件也没有属主,后续审计像一团乱麻。规范流程应该是:先用usermod -L锁定,再用pkill回收进程,确认没有任何引用后再userdel -r。
删完用户后还有一个善后动作:用find / -nouser -o -nogroup扫描全局,找到所有“无主文件”。这些文件如果放任不管,就是权限体系里的盲区。我在接手维护的每一台服务器上都会跑一遍这条命令,结果几乎每次都能扫出一堆历史遗留。
5. 组的真正价值:从 groupadd 到 SGID 共享目录的一次完整落地
5.1 主组和附加组的角色分工
创建组很简单:
groupadd 某项目组 groupadd -g 2010 devteam但要把组用好,必须先分清“主组”和“附加组”。主组是/etc/passwd第 4 字段写的 GID,用户新建文件时默认继承的组就是它;附加组写在/etc/group的成员列表里,作用是把用户“拉进”更多协作圈。id 某用户输出中第一个括号里的就是主组,后面跟着的全是附加组。
一个常见误区是:大家习惯把所有用户的主组都设成同名的个人组(比如某开发:某开发),然后在共享目录里用额外的组来做授权。这个模式没问题,但要避免“把共享目录属组改成个人组”这种操作——因为个人组往往只有那一个用户,改了等于没改。
5.2 落地一个真正的共享项目目录
假设前端组webteam需要在一个目录里协作,完整流程应该是:
mkdir -p /data/某项目 chown root:webteam /data/某项目 chmod 2775 /data/某项目重点在2775。首位2是 SGID 位,它保证:任何在目录里新建的文件,组会自动继承为webteam,不会再变成创建者自己的个人组。没有 SGID 位的话,A 用户创建的文件属组是A,B 用户去改就报 Permission denied,协作直接破裂。这个问题我在真实项目里至少帮人排查过五次,每次都是同一个原因:目录上少了 SGID 位。
末尾的5是r-x,表示组内成员能读能进但默认不能改别人的文件,配合每个文件自身的归属实现精确控制。再加一层 ACL 也常见,但传统权限模型能说清楚的场景,我尽量不引入额外复杂度。
5.3 新加组为什么没生效
一个高频问题是:管理员刚把用户加进webteam,用户立刻去访问共享目录,结果还是 Permission denied。
原因通常是用户当前的登录会话是在加组之前建立的。Shell 启动时已经缓存了当时的组集合,后续即使/etc/group变了,当前会话也不会自动刷新。最简单的解决办法是让用户重新登录。如果不想中断会话,可以执行newgrp webteam创建一个以新组为主组的子 shell,操作完exit退出即可。这个细节看似小,但在线上“加组后没权限”的工单里占了相当比例。
6. 用户组与文件权限的联动:chown、chmod、umask 实操要点
6.1 冒号后面不写组名,后果和你以为的不一样
chown的标准写法是chown 用户:组 文件,但有一个容易误伤的细节:如果你只写chown 用户: 文件(冒号后面留空),GNU chown 会把文件属组改成该用户的主组;如果你完全不写冒号,写成chown 用户 文件,则只改属主,属组保持不变。
这条规则我专门吃过亏:原本只想把某目录交给dev01管理,命令写成了chown -R dev01: /data/某项目,结果dev01的主组变成了目标目录的属组,其他原本在该组内的同事一下子全部失去了推代码的权限。事后复盘,规范的写法应该是明确写全两个字段:
chown -R dev01:devteam /data/某项目批量操作时记得先ls -ln确认当前的 UID/GID 数字,再决定要不要加-R。-R遇到软链接时默认不会追踪,真正要改链接本身需要-h参数,这也是个容易漏掉的细节。
6.2 umask:新建文件的“默认地盘”由它划定
umask决定新文件和新目录的初始权限。文件默认基数是666,目录是777,用基数减去 umask 得到实际权限。所以umask 022时,新文件是644,新目录是755;umask 027时,新文件是640,新目录是750。
对多用户服务器,我强烈建议把默认 umask 设成027而不是022,让用户新建的普通文件对“其他用户”完全不可读。对有协作需求的目录,则单独在目录内改为umask 002,配合 SGID 保护新文件属组。用一句话概括:全局从严,局部放权,而不是反着来。
6.3 特殊权限位与用户组的关系
setuid、setgid、sticky这三类特殊权限,都和用户组模型强绑定。setuid会让可执行程序以属主身份运行,典型如passwd命令;setgid让程序以属组身份运行,目录上则体现为继承属组;sticky位则出现在目录上,比如/tmp,它规定“谁创建的文件只有谁才能删”,避免任何用户都能删别人的临时文件。
这三者都是刀尖上的工具。线上出现奇怪问题时,我排查流程里总是包含一句ls -l看特殊权限位:比如某个目录权限显示为drwxrwxrwt,那基本可以判断是谁手滑给普通目录加了 sticky,导致组内成员无法删除不属于自己创建的文件。反过来,如果共享目录需要成员之间互相清理文件,就不该保留 sticky 位。
7. 用户组管理里最常见的翻车现场和排查路线图
7.1 先看数字,再看名字
排查用户组问题,我有一套固定的动作:
id 某用户 getent passwd 某用户 getent group 某组 ls -ln 目标文件id看用户的所有组归属,getent看系统最终生效的数据库,ls -ln看文件真实的 UID/GID 数字。这里有个关键习惯:用-n选项强制显示数字,避免被名字误导。因为当文件属主数字在系统里没有对应名字时,ls -l只会显示一大串数字,反而比平时更显眼。
我遇到过一个案例:某用户一直说“文件明明是我的,但没权限”。ls -ln一看,文件 UID 是1003,用户 UID 是1004,两个数字差一位。原因是有管理员用userdel删了旧账号后又新建,系统自动分配了下一个 UID,结果“看起来同名,实际换了身份证号”。这种问题,靠眼睛看名字永远发现不了。
7.2 用户删了,文件还在,怎么办
这是用户组管理里最典型的“历史债”:某账号被删,但他曾经创建的文件散布在服务器各处,属主数字变成一个无主 ID,属组数字也可能对应一个不存在的组。
处理流程分三步:
- 先扫描:
find / -xdev -nouser -o -nogroup 2>/dev/null,把所有无主文件和无主组文件列出来。 - 再确认:这些文件是否需要保留,需要保留的话归哪个新账号管。
- 最后修复:
find / -xdev -nouser -exec chown 新用户:新组 {} +。
如果项目里有“删除用户后必须保留数据供审计”的要求,这一步尤其重要。无主文件一旦放任不管,后续任何权限复核都过不了。
7.3 危险配置要主动扫,别等事发再追责
有两个高危点,我会在新接手的每台服务器上第一时间排查:
第一,是否存在 UID 为 0 的非 root 账号。UID 0 意味着 root 权限。检查命令是:
awk -F: '$3 == 0 {print $1}' /etc/passwd正常情况只应该输出root一行,如果冒出别的用户名,基本可以判定是严重的安全事故或后门。
第二,sudo 组里的成员名单。很多团队习惯把所有人一股脑塞进sudo组,图的是“方便”。但从用户组管理的角度,这是典型的权限过度膨胀。我建议尽量收敛 sudo 成员,给需要提权的用户配置白名单式的sudoers片段,而不是用组做粗粒度放开。原因很简单:sudo 组的成员等于拥有整个服务器的内存和磁盘,这种“平等”对安全维护来说是灾难。
账号过期清理这块,我自己的节奏是每季度跑一次:
awk -F: '{if ($7 == "/usr/sbin/nologin" || $7 == "/bin/false") print $1}' /etc/passwd把服务账号和人类登录账号分开列出来,跟业务方确认哪些该留、哪些该锁。整个用户组管理的日常,其实就藏在这些不起眼的例行检查里——它不产生任何炫目的交付物,但它决定着一台服务器在遭受攻击或发生误操作时,能挺多久、能救多少。
最后分享一个小习惯:我在任何机器上做完用户或组的变更后,一定会顺手跑一遍pwck && grpck,再用id 受影响用户确认结果。这套组合拳看起来啰嗦,但能挡住九成以上的手滑操作。伺服器管理从来不是靠一次大动作,而是靠这些反复确认的小动作堆积出来的稳定性。