1. 先把概念讲清楚:用户、用户组与附加组各司其职
上个月我在一台新装好的 Rocky Linux 9 上给项目组搭建共享环境,原计划半小时搞定,结果在“用户——用户组——附加组”这三层概念上来回绕了好几圈。很多人平时敲useradd、chown、chmod都很溜,但被问到“主组和附加组有什么区别”“为什么加了附加组还要重登才生效”时,容易卡壳。其实这三者就像是门禁系统的三层设计:用户是你的身份卡片,用户组是你所在部门的默认权限,附加组则是你被额外授权的项目区域权限。本文就用这台测试机上的 10 个实战例子,把这三层关系彻底拆开讲清楚。
1.1 用户:Linux 里的“身份凭证”,不是你以为的“人”
在 Linux 里,用户不是物理意义上的人,而是一个权限主体。内核并不关心你叫什么名字,它只认一个整数:UID(User ID)。我们常用的root、zhangsan只是给这个整数起的“别名”,方便人在键盘前识别。操作系统判断“某个进程能不能访问这个文件”,归根到底看的是进程的 UID、GID 以及附加组列表,而不是看用户名是否匹配。
普通文件也好,目录也好,每个文件都有两个所有权属性:文件属主(owner)和文件属组(group)。属主是一个 UID,属组是一个 GID。你在文件系统里看到的drwxr-xr-x root root,第二列root其实是 GID 对应的组名。所以说清楚用户,必须先接受一个底层事实:Linux 的一切权限判断,都是数值运算,不是字符串匹配。这一条会在后面解释“为什么改了附加组没生效”的时候反复用到。
1.2 用户组:默认权限的“打包单位”
用户组(group)是一组用户的集合,以 GID 为唯一标识。它的核心作用有两个:一是给一组用户赋予相同的文件访问权限,二是作为新创建文件的默认属组。
每个用户都必须有一个主组(primary group)。用户创建文件时,文件的属组默认就是该用户的主组。比如用户zhangsan主组是devops,那他touch一个新文件,ls -l看到的属组就是devops。这个默认行为可以通过目录的 setgid 位改写,后面例子 6 会专门演示。
1.3 附加组:给用户增加权限边界的“后门”
附加组(supplementary groups)是用户除主组之外额外加入的组。一个用户可以同时属于多个组,其中一个是主组,其余都是附加组。附加组的意义在于按项目场景动态扩展权限,而不用把用户的主组改来改去。
举例来说,zhangsan的主组是devops,但他临时参与ops项目的值班,需要访问/project/share目录,运维管理员只要把他加入ops附加组,他就能拥有该目录赋予ops组的权限。流程上是:usermod -aG ops zhangsan,然后让zhangsan重新登录一次。如果一开始就把zhangsan的主组改成ops,那他以后建文件默认属组都会变成ops,反而不利于按部门隔离。
1.4 主组和附加组,别再用“等级”去理解
网上很多文章喜欢把组说成“上级”,把用户说成“下级”,这种等级化描述容易误导。更准确的理解是:
- 主组是用户“默认归属”的组,决定新文件的默认属组;
- 附加组是用户“额外被授权”的组,决定用户能额外访问哪些组权限的资源。
两者没有“高级低级”之分,只是在权限判定时地位不同。进程访问文件时,内核会依次检查进程的 UID 是否等于文件属主 UID,再检查进程的 GID 或附加组列表里是否有文件属组 GID,最后看该组的权限位。这里的关键点:附加组列表和主组一样,都属于进程凭证,只是修改后的生效时机不同,这个在例子 4 和例子 8 里会进一步展开。
2. 账号体系背后的三张表和四条命令
理解了概念后,必须知道这些信息落在哪里。Linux 账号体系的核心是三个文本文件,Linux 之所以强大,是因为这些文件是纯文本、可解释、可备份的。但这也意味着一旦手滑改错格式,用户就可能登录不了系统。
2.1/etc/passwd里的 7 个冒号段
/etc/passwd文本格式如下:
zhangsan:x:1002:1003::/home/zhangsan:/bin/bash这段共 7 列,按顺序分别是:
| 列 | 含义 |
|---|---|
| zhangsan | 用户名 |
| x | 密码占位符,真正的密码哈希在/etc/shadow |
| 1002 | UID |
| 1003 | 主组 GID |
| - | 用户描述信息(GECOS),可为空 |
| /home/zhangsan | 用户家目录 |
| /bin/bash | 登录 Shell |
注意第二列在现代 Linux 里固定是x,如果这里直接放一段!!或*,通常表示该用户禁止密码登录。不要手动改这一列,要改密码就走passwd。
2.2/etc/group里的 4 个冒号段,成员字段就是附加组入口
/etc/group的格式相对简单:
ops:x:1005:zhangsan,lisi四列分别是:组名、密码占位符、GID、组成员列表。成员列表可以理解为“额外的用户集合”,这里的用户其实都是附加组用户。如果一个用户的主组是ops,他不会出现在这个成员字段里,而是出现在/etc/passwd的第四列。
这个细节会带来一个很常见的困惑:groups zhangsan显示zhangsan : devops ops,但你在/etc/group的devops行里却看不到zhangsan,因为devops是他主组。看到成员字段里没有自己,先别急着怀疑系统出错了。
2.3/etc/shadow:密码与密码策略的敏感文件
/etc/shadow保存密码哈希和密码过期策略,只有 root 和有特殊权限的进程能读。它的第 2 列是加密后的密码字符串,常见开头有$6$(SHA-512)、$y$(yescrypt)、!(锁定)等。如果看到!!或者!开头,说明这个用户没有可用密码,通常是不能登录的。
日常排查“用户无法登录”时,要先看这一列,而不是急着改权限。比如刚用useradd创建的用户在passwd之前,shadow 字段就是!!,表现为“输对密码也进不去”,因为根本没有有效密码。
2.4 常用命令及使用边界
用户和组管理的主要命令有四条:
useradd:创建用户,配置主组、附加组、家目录、Shell;usermod:修改已有用户的属性,比如改主组、加附加组、改家目录;groupadd/groupdel:创建/删除用户组;passwd/chpasswd/chage:设置密码和管理密码策略。
另外还有两个“查询类”命令:id和groups。id的输出最完整,会列出 UID、GID 和 groups 列表;groups则只输出用户所属的组名列表。这两者都能看到附加组,但输出格式不一样,后面例子 8 会专门演示其中的坑。
3. 实战拆解:从零搭建一个用户与组管理方案(10 个例子)
下面这 10 个例子不是孤立的命令堆砌,而是一条完整业务线。我在 Rocky Linux 9 上模拟了一个场景:项目组要新来 3 个研发、2 个运维,需要建共享目录/project/share,研发要能读写项目文件,运维要在共享目录里拥有管理权限。整个过程从确认环境开始,到最终删号收尾,全部覆盖。
提示:以下命令默认在 root 权限下执行,除非特别说明,否则不要在每台机器上盲目照抄;请根据实际 UID/GID 范围调整。
3.1 例子 1:先摸清这台机器的用户 ID 分配范围
创建账号前,先看系统的 UID/GID 默认分配规则,否则盲目useradd后可能和已有用户冲突。
grep -E '^UID_MIN|^UID_MAX|^SYS_UID_MIN|^SYS_UID_MAX|^GID_MIN|^GID_MAX' /etc/login.defs我这边输出是:
UID_MIN 1000 UID_MAX 60000 SYS_UID_MIN 201 SYS_UID_MAX 999 GID_MIN 1000 GID_MAX 60000 SYS_GID_MIN 201 SYS_GID_MAX 999这说明系统普通用户的 UID 从 1000 开始,给系统服务预留的 UID 是 201~999。查看目的有两点:第一,给业务用户指定 UID 时不要落在系统区间;第二,理解不同发行版的标准不同,CentOS 系用 1000,Debian 系早期也有从 1000 开始,但有些新版桌面版从 1000 开始却保留 1000 给首个用户,需要实测。
知识点提取:用户 UID 分配范围由/etc/login.defs控制,useradd默认按照这个范围递增分配。创建业务用户时,建议显式指定一个高段 UID,比如20220起,方便后期从众多账号中区分业务账号和系统账号。
3.2 例子 2:创建三个用户组,并约定 GID 编号
按照项目规划,需要devops(研发组)、ops(运维组)、guest(访客组)三组。为了未来权限审计清晰,GID 也不随机分配:
groupadd -g 12001 devops groupadd -g 12002 ops groupadd -g 12003 guest验证:
getent group devops ops guest输出:
devops:x:12001: ops:x:12002: guest:x:12003:这里我手动指定了 GID。groupadd -g的-g选项就是指定 GID,后面跟一个未使用的数字。生产环境中,GID 规划比 UID 规划更重要,因为组通常要配合目录权限,如果组 ID 在跨服务器同步时不统一,NFS 等场景下容易权限错乱。
知识点提取:组也分系统组和普通组,系统组一般用groupadd -r创建,GID 落在系统范围内,用于服务运行,不建议把业务用户加入系统组。业务组建议从 10000 以上开始分配,并在团队 Wiki 里维护一张“GID 分配表”,避免两台服务器各建各的导致同一个组两个 ID。
3.3 例子 3:创建研发用户,指定主组和附加组
创建第一个研发用户zhangsan,要求主组是devops,附加组带上ops,家目录和 Shell 也有明确要求:
useradd -u 20221 -m -d /home/zhangsan -s /bin/bash -g devops -G ops zhangsan检查用户信息:
id zhangsan输出:
uid=20221(zhangsan) gid=12001(devops) groups=12001(devops),12002(ops)这里一条命令就把三件事做了:
-u 20221指定 UID;-g devops指定主组;-G ops指定附加组。
我故意把 UID 设成 20221 是为了和系统用户明显拉开距离,方便后续安全策略(如按 UID 段过滤)使用。注意-G后面可以接多个附加组,用逗号分隔,比如-G ops,guest。
知识点提取:useradd的-g指定主组,-G指定附加组。如果不指定-g,useradd会默认创建与用户名同名的组作为主组,这个默认行为由/etc/login.defs里的USERGROUPS_ENAB yes控制。很多人刚学 Linux 时看到“怎么创建了一个用户,会自动多出一个同名组”,其实就是这个原因。
3.4 例子 4:给已有用户追加附加组,记得用-aG
过了两天,运维组反馈希望zhangsan也参与发布工作,那就把他加入ops附加组。此时操作是:
usermod -aG ops zhangsan注意这个-a,是--append的缩写,表示“追加”。如果漏掉-a,只写usermod -G ops zhangsan,会把zhangsan原有的附加组列表整体替换成ops,原来属于devops主组的附加组关系被清空。虽然主组不受影响,但他在其他组的权限会瞬间消失。
我特意做过一次破坏性实验:
usermod -G ops zhangsan id zhangsan输出:
uid=20221(zhangsan) gid=12001(devops) groups=12001(devops),12002(ops)因为zhangsan原本只有ops一个附加组,这次没看出什么问题。但如果用户原本有ops,dba两个附加组,这条命令执行后就只剩ops了。生产环境改附加组的黄金法则是:凡是usermod -G,务必带上-a,除非你明确要覆盖整个附加组列表。
知识点提取:usermod -aG是追加,usermod -G是覆盖。两个命令在字母上看只差一个a,后果天差地别。另外注意:修改完附加组后,已登录的会话不会自动拿到新组权限,必须退出重新登录,或者用newgrp临时切换,这一点排在“附加组不生效”问题排查的第一位。
3.5 例子 5:修改用户主组后,为什么文件属组不变
项目调整后,zhangsan从研发组调入运维组,需要把主组改成ops:
usermod -g ops zhangsan执行后查看:
id zhangsan输出:
uid=20221(zhangsan) gid=12002(ops) groups=12002(ops),12001(devops)主组已经变成ops,原来的devops自动变成附加组。但这里有个坑:zhangsan家目录以及他之前创建的文件,属性组不会自动跟着变。
看一下:
ls -ld /home/zhangsan ls -l /home/zhangsan/test.txt大概率还是zhangsan devops,因为usermod只改了账号在/etc/passwd里的主组 GID,不会逐个扫描文件去改属组。若业务要求文件属组也迁移,必须额外执行:
chown -R zhangsan:ops /home/zhangsan知识点提取:修改主组和修改文件属组是两件事。命令层面的usermod -g只是修改了“默认组”,凡是用户以后新建的文件,默认属组才会是ops。历史文件需要手动chown。操作顺序上,先chown再让用户切回目录干活,否则他有可能会在旧权限环境下创建一些归属混乱的文件。
3.6 例子 6:创建共享目录,让附加组权限真正生效
现在建立/project/share目录,目标是:属于devops和ops的用户都能进入并创建文件,且所有新文件自动继承ops组。这样zhangsan虽然主组已改为ops,但依然能访问。命令如下:
mkdir -p /project/share chown root:ops /project/share chmod 2775 /project/sharechown root:ops把目录属组设为ops;chmod 2775是两个权限位的组合:2是 setgid 位,775是 rwxr-xr-x。
setgid 位的作用是:在该目录下新建的文件或子目录,其属组自动继承目录的属组,而不是创建者主组。如果没有 setgid,zhangsan在这里touch一个文件,文件的属组应该是他的主组ops;但如果他的主组后来变成devops,新文件属组又会变成devops,很容易造成混乱。
用zhangsan验证访问:
su - zhangsan -c "touch /project/share/test.txt && ls -l /project/share/test.txt"输出中属性组是ops,说明 setgid 生效了。再让另一个用户lisi也测试,只要能写就证明附加组授权生效。
知识点提取:共享目录权限三板斧:chown root:组名、chmod 2775、成员加入附加组。如果你要给多个组共享,可以创建多个目录,分别 chown 给不同组,再把用户分别加入对应附加组。比直接把所有用户加入同一个组更安全。
3.7 例子 7:临时切换主组,newgrp与sg的用法
有时候用户需要“临时以某个组身份”创建文件,但不想退出重新登录。比如zhangsan当前主组是ops,临时要以devops的身份创建一个放在共享区里的文件:
su - zhangsan newgrp devops touch temp_devops.txt ls -l temp_devops.txt输出中temp_devops.txt的属组就是devops。newgrp会启动一个新的子 Shell,并把当前主组切换成指定的组。切换条件是你必须属于这个组,也就是它要么是主组,要么在附加组列表里。退出这个子 Shell 后,主组恢复原样。
sg命令和newgrp类似,但它适合一次性执行命令而不进入交互式 Shell:
sg devops -c "touch temp_sg.txt"知识点提取:newgrp/sg是“不退出登录就临时变更主组”的最便捷方式,常用于解决“我想在这个目录下生成一个属于某组的文件”的需求。注意这两个命令依赖一个前提:用户必须已经是该组成员,否则会提示密码或拒绝。不要去改/etc/group里的主组字段来绕过,那是翻车率最高的操作之一。
3.8 例子 8:用id和groups正确读取成员关系
查用户组关系最容易踩的坑在groups输出上:
groups zhangsan输出:
zhangsan : ops devops这个列表的第一个ops是主组,devops是附加组。但不少文档会把输出的第一个组当成“第一个附加组”,其实它不是。要更清晰地看 UID/GID 和组关系,用id:
id zhangsan输出:
uid=20221(zhangsan) gid=12002(ops) groups=12002(ops),12001(devops)这里gid=后面的是主组,groups=后面跟着的是完整组列表,第一个仍是主组,之后才是附加组。如果你只想看附加组,可以用:
id -nG zhangsan输出中没办法一眼区分哪个是主组,因为-nG会把所有组名列出。要精确区分,就配合id -gn获取主组名:
id -gn zhangsan id -nG zhangsan知识点提取:判断附加组不要凭直觉数第几个,要结合id的完整输出:gid即主组,groups列表里去掉主组后剩下的全是附加组。如果/etc/group的成员字段里看不到某用户,但groups能看到,那是因为这个组恰好是它的主组,并不是系统出错了。
3.9 例子 9:批量创建用户,脚本里最容易忽略的密码策略
批量添加用户是最常见的自动化需求。现在要创建 5 个用户:alice、bob、carol、dave、eve,前 3 个加入devops附加组,全部加入ops附加组。脚本如下:
for user in alice bob carol dave eve; do useradd -m -s /bin/bash -G ops "$user" echo "$user:Init@123" | chpasswd chage -d 0 "$user" done usermod -aG devops alice usermod -aG devops bob usermod -aG devops carol分解一下:
- 第一行
useradd -m -s /bin/bash -G ops表示创建用户、创建家目录、Shell 设为 bash,附加组加入ops; - 第二行
echo ... | chpasswd批量设置初始密码; - 第三行
chage -d 0强制用户下次登录后必须修改密码。
脚本看起来短,坑其实不少。最大的坑是:如果不写chage -d 0,用户拿着初始密码可能一直不换,安全审计一查一个准。第二坑是:加了-G ops后,再执行usermod -G devops alice(没有-a),会把ops附加组覆盖掉,所以我在脚本里对需要多个附加组的用户用了-aG devops追加。
知识点提取:批量创建用户三个关键点:
- 密码不落库,用
chpasswd以 stdin 方式批量设置; - 强制首次登录修改密码,必须
chage -d 0; - 对已有用户的附加组进行追加时,牢记
-aG,避免覆盖。
脚本写完后建议先bash -n useradd.sh做语法检查,再在测试机跑一遍。
3.10 例子 10:删除用户时,家目录和组的正确清理
有员工离职,需要删除账号eve。生产环境里的删除操作必须谨慎,先确认她不再拥有任何共享目录的属主文件:
find /project -user eve -ls如果确认没有需要保留的文件,直接删除并清理家目录:
userdel -r eve-r会同时删除家目录和邮件池。如果忘记-r,家目录会残留,通常是很占空间的隐患。接着看eve加入的组是否需要清理成员关系:
gpasswd -d eve ops不过用户都删了,这个命令其实会报错“user doesn't exist”,正确的顺序是:先把用户从所有附加组中移除,再删除用户。如果已经删了,组里的成员列表已经自动被系统去掉了吗?不一定。实际上userdel会尝试从/etc/group的成员列表里移除该用户,但有些情况下(比如手动编辑过组文件)会留下不再存在的用户名,导致解析器发出警告。
知识点提取:删除用户流程建议是:
- 先检查该用户是否拥有共享文件(
find / -user 用户名); - 再用
gpasswd -d或直接调用userdel前先编辑组?不行,组操作只建议命令; - 最后
userdel -r 用户名,并保留用户 UID/GID 以防重建时被新用户占用。
这一步做完,整个用户生命周期就完整闭环了:创建、配置、授权、迁移、删除。
4. 最容易翻车的三个场景与完整排查链路
理论知识再熟,实际运维中一样会踩坑。这里把我自己碰过的三个高发问题整理成完整排查链路,照着顺序走,比漫无目的地改权限高效得多。
4.1 场景 A:加完附加组,还是访问不了共享目录
某同事反馈,明明已经执行了usermod -aG ops zhangsan,但zhangsan访问/project/share仍然Permission denied。
排查链路:
第一步:确认组成员关系是否真的加上了
id zhangsan如果输出里没有12002(ops),说明命令没执行成功,回去看第 3 步。
第二步:确认用户当前会话是否重新加载了组身份
这是最容易被忽略的一步。用户在那个报错的终端里如果是在执行usermod之前就已登录,那么他的进程组列表还是旧的。让他执行exit后重新登录,或直接newgrp ops临时切换,再测试。
第三步:逐级检查目录权限
namei -l /project/sharenamei会输出路径上的每一级目录权限。如果/project或/的权限没有给足 others 或相关组的x权限,用户根本无法“穿过去”。常见情况是/project权限是 750,属组是 root,那么非 root 用户就无法进入。需要给/project设置 755 或把共享目录挂到路径更浅的位置。
第四步:检查文件系统挂载选项
mount | grep /project如果挂载选项里有nosuid还好,若是noexec会导致执行脚本受限,但一般不影响读写。更关键的是有些远程文件系统(如 NFS)有root_squash等行为,会影响属主显示,这些放到远程文件系统专项讨论。
知识点提取:“加了组但没权限”按 90% 概率集中在两个原因:会话未刷新、目录中间路径缺 x 权限。不要一上来就改 777,先用id和namei定位问题。
4.2 场景 B:主组改完,文件属组一团糟
有一次我把用户的-g从devops改成ops,旧文件属组依然停留在devops,导致共享备份脚本按组匹配时把该用户文件漏了。
这里的原因是usermod -g不负责重新分配文件属组。正确的排查和处理方式:
find /home/zhangsan -group devops -exec chgrp ops {} \;先把该用户家目录中所有属组为旧组的文件批量改成新组,再检查共享目录:
find /project -user zhangsan -exec chgrp ops {} \;如果文件特别多,建议在业务低峰期执行,并在执行前确认不会影响正在运行的应用。另外,如果你想让用户主组和设备上的默认组保持一致,还要调整/etc/default/useradd或/etc/login.defs里的GROUP变量。
知识点提取:主组变更后,文件属组不会自动跟随。所以改usermod -g后,强制更新目标目录的文件属组是标准动作。这一步在操作计划里就要写好,别等用户反馈“新文件是 ops,老文件还是 devops”才补,补的次数多了容易漏。
4.3 场景 C:手动编辑/etc/group导致用户直接“消失”
我见过有人直接用vi /etc/group在成员列表末尾加用户名,结果保存后,系统里某些服务读取组信息时直接报错,用户登录后也看不到那个组。
这类问题通常出在格式细节上:
- 成员之间必须用英文逗号分隔,不能有空格;
- 成员列表末尾不要留逗号;
- 组名、GID、成员行不能有额外冒号;
- 如果你新增了一行,GID 不能与其他行重复。
一旦格式被破坏,getent group会跳过异常行,用户在groups输出里也就看不到该组。强烈建议不要手动编辑/etc/group,添加组成员用usermod -aG或gpasswd -a;删除组成员用gpasswd -d。
万一你真手改了,赶紧恢复:
cp /etc/group /etc/group.bak getent group | grep xxx如果连getent都无法输出,可以把/etc/group.bak恢复回来,再检查是否与/etc/passwd、/etc/shadow里的主组 GID 对应。恢复用的备份建议提前用 cron 每天拷贝一次,这是防止“手滑”的最后防线。
5. 知识点提取一张表:命令、作用、坑位速查
把上面 10 个例子里反复出现的命令整理成一张速查表,适合贴在便签上或在排障时快速回忆。
| 命令 | 作用 | 关键选项/易错点 |
|---|---|---|
useradd | 新建用户 | -u指定 UID;-g指定主组;-G指定附加组;-m创建家目录 |
usermod | 修改用户属性 | -aG追加附加组;-G覆盖附加组;-g修改主组,注意文件属组不会自动变 |
userdel | 删除用户 | -r同时删除家目录/邮件池;删除前先检查用户文件 |
groupadd | 新建组 | -g指定 GID;业务组建议从 10000+ 开始分配 |
gpasswd | 管理组成员 | -a添加成员;-d删除成员;不要手动编辑/etc/group |
id | 查看完整身份信息 | id显示 uid/gid/groups;id -gn只看主组名;id -nG列出所有组名 |
groups | 查看组列表 | 输出第一个是主组,不是附加组 |
newgrp | 临时切换主组 | 需要已是组成员;sg适合一次性命令 |
chown | 修改属主/属组 | chown user:group file同时改两者;chown :group file只改属组 |
chmod 2xxx | 设置 setgid/setuid/sticky | 2775的2表示 setgid,使新文件自动继承目录属组 |
chage | 管理密码老化策略 | chage -d 0 user强制用户下次登录改密 |
chpasswd | 批量设置密码 | 通过 stdin 传user:password,适合脚本 |
这张表对比下来,能看出 Linux 用户管理的一条核心原则:所有“变更”都应该通过专用命令完成,而不是直接编辑配置文件。配置文件只是最终落地的结果,命令才是经过了校验和封装的正式入口。
如果你在做自动化运维,建议在脚本里加上set -e,并对每一步操作的结果做校验,比如id user确认追加成功后再继续下一步。这样一套下来,用户、用户组、附加组这套体系就不会再是“命令会敲,报错不会查”的状态了。