如果你以为Ubuntu用户管理就是“useradd建个号、passwd设个密、userdel删个人”,那只能说还没被生产环境教训过。作为运维,我几乎每周都要和账号打交道,而且越深入越发现,这套看似简单的操作背后,藏着不少默认行为、权限模型和审计细节。这篇文章不打算把man手册复读一遍,而是把我实际排查和处理的案例提炼成一套能直接落地的用户管理思路,覆盖创建、分组、授权、批量操作和下线清理。无论你是刚摸Linux的小白,还是管着几十台服务器的老手,应该都能在这里找到一些可抄的作业。
1. 先搞清楚Ubuntu用户管理的几个“底层事实”
1.1 为什么我一直建议优先用adduser而不是useradd
在Ubuntu上新建用户,其实有两条路:useradd是System V风格的底层命令,adduser则是对它的Perl封装。很多新手喜欢直接useradd,因为网上教程常写这个,结果建完用户之后发现连家目录都没有,更别提正常登录,然后就开始怀疑系统坏了。
这不是系统的问题,而是工具语义不同。useradd默认是“最小化创建”,它只往/etc/passwd和/etc/group里加记录,不创建家目录、不设置密码、不复制骨架文件。而adduser会做一整套初始化:生成家目录、复制/etc/skel下的.bashrc和.profile等文件、交互式设置密码、自动分配UID和主组。所以在交互式环境里,我始终推荐用adduser;只有在写脚本批量创建时,才用useradd,而且脚本里必须显式带上所有需要的参数,比如-m、-s、-d。
另外还有一点很容易被忽略:useradd创建的账号,如果不手动passwd,那它的密码字段在/etc/shadow里是空的。空密码在ssh层面和锁定状态的表现还不一样,用户用密码登录会直接被拒,但root可以在本机su过去,容易造成“这个账号好像能用”的误判。
1.2 核心配置文件:/etc/passwd、/etc/shadow、/etc/group
用户管理的底层全部落在三个文本文件上。理解它们,很多问题就能直接“看得出来”,而不是靠猜。
/etc/passwd每行七个字段,用冒号分隔。简单拆一下:
username:x:1000:1000:Full Name:/home/username:/bin/bash用户名后面的x表示密码哈希不在这个文件里,而是转存到/etc/shadow;第二个数字是UID,第三个数字是主组GID;中间的是注释字段,通常写全名;后面是家目录和登录Shell。如果登录Shell是/usr/sbin/nologin,那这个账号就不能用于交互式登录,但系统服务依然可以以这个身份运行,这是刻意设计的安全隔离。
/etc/shadow保存的是加密后的密码哈希和账户有效期策略,权限默认是640,只有root和shadow组能读。一旦权限放开,任意用户能读shadow,就等于拿到了暴力破解密码的素材,所以这是安全检查里最常被盯的点。
/etc/group更简单,每行是组名、组密码占位符、GID、组成员列表。注意组成员列表里填的是“附加组成员”,主组成员是不显示在最后一段的,这导致很多人误以为“没在该组”时,其实用户已经通过主组获得该组权限。
1.3 UID/GID分配逻辑:为什么普通用户从1000开始
Ubuntu预留了UID 0到999给系统账号,普通用户的UID从1000开始。这样做是为了把“人用的账号”和“服务跑的账号”区分开。像www-data、sshd、nobody这类账号,不需要密码登录,又需要以低权限运行,于是占用了低段UID。
这个分配策略在单机上看不出来问题,一旦涉及共享存储、NFS或容器环境,UID错乱就是大坑。因为文件系统上存的是数字UID,不是用户名,跨机器时如果两台机器上同一个UID代表的用户不同,文件属主就会乱。我处理过一个案例:某服务器上UID 1001本来属于一个内部服务账号,后来有人手工建用户时没有指定UID,系统顺手就给了1001,结果该服务启动后读取旧文件时全部报权限错误,排查了半天才发现是UID撞车。所以,凡是可能需要跨机器共享数据的用户,创建前最好统一规划UID段,或者干脆在/etc/login.defs里把UID_MIN改成从1100开始,给系统账号留出更多余量。
2. 创建用户和家目录:别再用默认参数了
2.1 一条adduser命令背后自动完成的事
在很多教程里,adduser username看起来像魔术。实际它依次完成了这些操作:创建UID和主组、创建/home/username、从/etc/skel复制骨架文件、设置用户密码、创建用户邮箱文件。这也解释了为什么用adduser建出来的账号,立刻就能用完整的环境登录,而useradd -musername只是“造了个壳”。
但“自动”不等于“一定适合你”。比如adduser默认家目录权限是755,意味着同一台机器上的其他用户都能进你的家目录看文件。如果机器是多用户共享的,你不想让别人随意浏览,就需要修改/etc/adduser.conf里的DIR_MODE,把它改成750或更严格。又比如adduser复制骨架文件使用的是/etc/skel,如果公司有统一标准,比如所有用户都自带某个环境变量或bash别名,就应该统一改/etc/skel,而不是等用户建好后再逐个补。
2.2 自定义家目录、Shell和UID的实操
有些用户需要把家目录放在数据盘,或者需要使用非默认Shell,这时候adduser的默认全流程就不够了。下面是常见的手工创建姿势:
# 创建用户,指定家目录、Shell、UID和主组 sudo useradd -m -d /data/deploy -s /bin/bash -u 1055 -g developer deploy # 初始化密码 sudo passwd deploy # 如果不需要密码登录,仅用于服务运行,可以锁定密码 sudo passwd -l deploy注意几个关键细节:
- -m表示创建家目录,但不会复制/etc/skel,需要的话手动拷贝一份。
- -d要配合-m使用,否则家目录只记录在/etc/passwd里,实际并不存在。
- 主组-g后面填组名或GID均可,但如果是自定义组,得先用groupadd把组建好。
- Shell用绝对路径,常见的是/bin/bash或/usr/sbin/nologin。
对于服务账号,我习惯创建后立刻passwd -l锁定密码,然后给运行目录单独chown。这样它只能通过启动脚本切过去,没人能远程以这个账号登录,安全冗余高很多。
2.3 新建用户SSH登录不上的常见原因与排查
这类问题我平均每月都能遇到一次。新建了账号,密码也设了,SSH却一直报Permission denied。归纳起来,原因集中在四个地方。
第一,密码没真正生效。有些发行版在useradd -p后面传的是明文密码,但系统存的是哈希,根本不会自动加密,结果密码永远对不上。所以要么用passwd设置,要么用openssl passwd -6生成哈希再传。
第二,家目录权限或属主不对。sshd启用了StrictModes时,如果.ssh目录或家目录属主不是用户自己,或者权限过宽,服务端会直接拒绝认证。比如某同事给另一个用户建家目录时直接cp -r了目录,导致属主还是root,新用户怎么都登陆不了。
第三,Shell被设成了nologin或不存在。检查一下/etc/passwd最后一段,如果Shell路径拼错了,登录会直接失败。
第四,sshd本身关闭了密码认证。这种情况本质不是用户问题,而是服务器的安全策略。可以在本机手动试一下su - user,如果能切过去,就说明问题在ssh配置而不是用户配置。
排查时按顺序看:/etc/passwd里的Shell、/etc/shadow里密码字段是否正常、家目录属主和权限、sshd -T 里认证方式。这样可以少走很多弯路。
2.4 完整示例:创建一个仅用于运行应用的受限用户
假设要给一个Web应用创建专用账号,要求:有家目录、能执行应用命令但不需要交互登录、UID固定为1055。
sudo groupadd --gid 1055 appgroup sudo useradd -m -u 1055 -g appgroup -s /usr/sbin/nologin appuser sudo mkdir -p /opt/myapp sudo chown -R appuser:appgroup /opt/myapp sudo chmod 750 /opt/myapp这个账号没有交互Shell,密码也可以不设置。因为应用启动脚本可以通过systemd的User=appuser切到该身份运行,根本不需要密码。给目录设置750,保证了组内成员能读和执行,但其他人没有权限。如果你发现某些应用因为家目录不能写而启动失败,可以给家目录加一个子目录并只授权那个子目录,而不是简单粗暴地chmod 777。
3. 用户组与sudo授权:最小权限不是口号
3.1 主组与附加组,别把关系搞混
每个用户有且仅有一个主组,但同时可以加入多个附加组。主组决定该用户新建文件的默认属组,附加组则代表“额外访问能力”。比如把用户加入docker组,他就能操作Docker daemon,而不需要sudo root权限。这种分层的思路,可以让你在审计时清楚看到某人有哪些权限来源。
修改附加组时,最经典的一个失误是:usermod -G docker user 会把该用户从原有的附加组成员列表里全部移除,只保留docker。如果你的用户本来在sudo组里,这一下就把管理员权限弄丢了。正确姿势必须是 usermod -aG docker user,其中-a表示append,G表示附加组。我见过不止一个同事因为少了-a,导致用户既失去sudo权限又影响后续操作,最后只能从root重置。
3.2 sudo授权精细控制的实例:别一上来就给ALL
sudo组里的用户默认拥有全部命令执行权限,这对多用户生产机来说太危险了。更稳妥的做法是单独在/etc/sudoers.d/下建一个文件,把可执行命令限制在一小撮白名单里。
比如想让deploy用户只能重启和查看nginx服务,可以这样写:
sudo visudo -f /etc/sudoers.d/deploy文件内容:
deploy ALL=(ALL) /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx, /usr/bin/systemctl reload nginx这里有几个关键点:
- 命令路径必须写绝对路径。否则攻击者可以做一个同名脚本放在PATH前面,利用sudo执行它。
- 逗号分割多个命令,末尾不要留多余空格。
- 如果只想省去输密码,可以加NOPASSWD:,但只建议在隔离的测试环境用,生产机还是保留密码校验。
- 用visudo打开文件,语法检查通过后才会写入,这样能避免编辑错误导致sudo直接跪掉。
3.3 创建专用组并限定共享目录访问的实例
多团队共用一台机器时,更容易出权限纠纷。我的常规做法是建专用组,利用目录的组权限做隔离。
假如有项目A和项目B两个团队,需要分别读写各自的共享目录。先建两个组:
sudo groupadd project_a sudo groupadd project_b然后创建目录,设置属组和setgid位:
sudo mkdir -p /data/project_a /data/project_b sudo chown root:project_a /data/project_a sudo chown root:project_b /data/project_b sudo chmod 2770 /data/project_a /data/project_b这里的2是setgid位。它带来的效果是:目录下新建的文件和子目录,会自动继承目录的属组,而不是创建者自己的主组。否则光是组权限还不能保证新文件属于project_a,两个团队之间很容易互相覆盖。
再把用户加入对应组:
sudo usermod -aG project_a alice sudo usermod -aG project_b bob这样简单几条命令,就让不同团队拥有了隔离的读写区,并且都能在各自目录里正常协作。
3.4 误给sudo权限后的急救方法
权限给大了,最常见的救急动作是编辑/etc/sudoers。但有些人图方便直接chmod改文件权限去编辑,改错语法后又放不回去,导致任何用户执行sudo都会报错,包括root。所以再次强调:改sudo配置必须用visudo,它会保存临时文件并做语法校验,至少能防止配置文件彻底坏掉。
万一真误删了sudo组里用户的全部权限,或者更严重,把UID改成0制造了“影子root级账号”,请立即恢复到正常状态。若已无法通过sudo进入root,就在重启时进入恢复模式,以单用户方式登录后修复。需要强调的是,这类操作属于应急手段,日常最好的策略还是控制sudo权限的变更路径,并且定期审计/etc/passwd和/etc/sudoers的修改记录。
4. 批量用户管理:从单机到多机的效率工具
4.1 newusers批量导入用户,更适合初始化场景
当一次要创建几十个账号时,一条条adduser太慢。newusers可以从标准输入读取一个固定格式文本,每行对应一个用户:
用户名:密码:UID:GID:注释:家目录:Shell例如:
echo "dev01:tempPass123:1001:1001:Dev One:/home/dev01:/bin/bash" | sudo newusers这里注意:密码字段可以是明文,newusers会用系统默认算法把它转成哈希。但明文密码会留在shell历史里,这是安全隐患。我通常是先用stty -echo关闭回显,或者把内容写入一个权限600的临时文件再重定向给newusers,执行完立刻删除临时文件。
还有一个细节:newusers创建用户时,如果家目录不存在,它不会自动创建家目录,需要结合mkdir和chown处理。所以这个工具更适合在已经确定目录结构一致的批量初始化场景下使用。
4.2 chpasswd批量修改密码与密码过期策略
批量重置密码,chpasswd是更好的选择。它和newusers一样从标准输入读取“用户名:密码”:
sudo chpasswd < /path/to/passwords.txt在准备这个文本前,先用umask 077来创建,保证只有root能读,执行完立刻删除。如果你的密码里有特殊符号,注意引号包裹。
强制一批用户下次登录必须改密码,可以配合chage:
while read user; do sudo chage -d 0 "$user"; done < users.txtchage -d 0的作用是把最近一次修改密码的日期设成1970年1月1日,这样系统会认为密码过期,用户下次登录时必须先设置新密码。这个动作在临时密码方案里非常常用。
4.3 批量锁定/删除时的脚本防误伤
批量操作最大的风险是误伤系统账号。系统账号的UID都在1000以下,所以脚本第一件事就应该按UID过滤。一个通用做法:
awk -F: '$3 >= 1000 {print $1}' /etc/passwd只对这个输出里的用户名做循环,能极大降低误伤概率。删除用户时,要格外小心userdel -r这个-r参数。它会连家目录和邮件池一起删,但如果家目录里有软链接指向其他公共目录,-r不会去追软链接,但如果你把家目录本身tar出来再恢复到别处,可能把软链接复制成普通文件,同样会造成数据混乱。
我习惯在删除前先做三件事:归档家目录、导出crontab、记录用户主要文件列表。确认无价值后再执行userdel -r。
4.4 多机用户一致性维护
没有LDAP或AD的团队,让多台机器上的用户保持一致是件很烦的事。复制/etc/passwd和/etc/shadow是最直白的方案,但shadow里的哈希如果每台机器不同,就会造成同一账号在不同机器密码不一致,后续麻烦更多。
我更推荐的方式是把用户配置固化成脚本,然后用自动化工具分发执行。比如在Ansible里用user模块,istate=present,指定name、uid、groups、shell,这样无论执行多少次,结果都一样。关键是所有机器上的UID必须一致,否则跨机同步文件时权限会错乱。
如果只是三五台机器,也可以把adduser命令包装到一个bash脚本里,在每台机器执行一遍。只是要记得脚本里显式指定UID,并统一密码策略。等规模再大,再考虑上集中身份认证服务,但那就是另一篇文章的话题了。
5. 用户生命周期管理:离职下线该做什么
5.1 先锁定还是先删除?锁定是更稳妥的开始
很多人的第一反应是userdel -r,但正确的流程应该是先锁定账号,观察一段时间,确认没有影响后再删除。原因是直接删除会让属于该用户的文件瞬间变成UID孤儿,而且如果用户还有运行中进程,删除后进程仍然会跑在旧UID下,后续管理变得很麻烦。
锁定一个账号,至少要做两件事:
sudo passwd -l username sudo usermod --expiredate 1 username第一行在密码哈希前加!,阻止密码认证。注意,这并不能阻止SSH公钥登录。第二行把账号的过期日期设为1970年1月1日,也就是账号立刻永久过期,不管你用什么认证方式,都不能再登录。这才是真正“冻结”账号。
等审计完数据、杀掉残留进程之后,再来决定是彻底删除,还是保持冻结存档。
5.2 下线前的数据备份步骤
给下线用户做备份,不能只想家目录。很多数据散布在crontab、服务配置和临时文件里。
至少备份这些:
# 家目录 sudo tar -czf /backup/username_home.tar.gz /home/username # 定时任务 sudo crontab -u username -l > /backup/username_crontab.txt # 如果存在systemd用户服务 sudo systemctl status user@$(id -u username).service备份家目录时,注意加上--exclude缓存目录,比如.browser-cache、.cache、.local/share/Trash等,否则备份体积会莫名其妙大很多。还要检查一下用户是否挂了NFS挂载点,如果有挂载在/home/username/remote下,可以直接跳过,否则等于把别人机器的数据也打包进来了。
5.3 删除后的UID孤儿文件排查
别人的人走了,但文件都还在。文件系统只记录数字UID,用户删除后,这个数字没有对应关系,于是显示成数字ID。查找方法:
find / -xdev \( -nouser -o -nogroup \) -ls 2>/dev/null-xdev避免进入其他挂载点,能大幅减少无谓扫描。对于孤儿文件,通常两种处理方式:如果还有价值,chown给接手的用户;如果确认是废数据,直接删除。使用chown时推荐带-h参数,避免只改链接本身还是链接目标的问题:
sudo chown -h newowner:newgroup /path/to/orphan在删除用户前提前执行一次这个查找,其实更好,能让你知道他的文件分布在哪里,避免删除后才发现遗漏。
5.4 审计和日志保留
合规要求下,账号下线不只是删人。登录记录、sudo记录和操作日志都要留底。Ubuntu里主要看点有:
- /var/log/auth.log,记录登录成功/失败、sudo执行、用户切换。
- wtmp/btmp,用last和lastb查看正常和失败的登录记录。
- 如果给sudoers配置了日志,/var/log/sudo.log或syslog里也会留下命令级记录。
删除用户前,先执行last username把自己需要的记录保存到文件,再删除,这些历史登录信息不会因为账号删除而消失,但保存到本地文件更方便日后举证和审计追溯。
6. 常见问题排查:登录失败和权限异常
6.1 “user is not in the sudoers file”的几种可能
这条报错几乎每个运维都见过。它的直接意思是,当前用户没有被授权执行sudo命令。最常见的情况是用户不在sudo组里,用id username看附加组列表,如果没有sudo,就执行:
sudo usermod -aG sudo username还有一种情况,用户虽然在sudo组,但sudoers文件里有针对该用户或该组的限制,比如默认没启用sudo组权限。Ubuntu通常带/etc/sudoers.d/文件来管理这类配置,检查一下是否有相关覆盖。最麻烦的是/etc/sudoers部分语法错误,会导致任何用户sudo都直接报错,这时候只能由root通过su或恢复模式修复,这也是我一直强调必须用visudo编辑的原因。
6.2 无法删除“正在使用”的用户
如果你执行userdel时遇到“user xxx is currently used by process xxx”,说明还有进程在以这个用户身份跑。正确的姿势是查一下是什么进程:
ps -u username -o pid,ppid,cmd确认是残留会话后,先通知相关用户下线,或者用pkill终止。只有在确认不会影响生产时,才使用kill -9。如果用户只是有延迟写日志的会话,锁定账号后等一段时间再删除,往往比强杀更省事。
6.3 密码过期与强制修改
如果要让用户下次登录必须改密码,chage -d 0是一个标准做法。但有个容易踩的坑:如果用户Shell是nologin,他在登录时无法触发密码修改流程,账号会一直显示不可用。所以这个强制改密机制主要面向普通交互用户,服务账号不要设置短有效期。
设置密码有效期时,可以用chage -M 90 username表示90天必须改一次密码。也可以在/etc/login.defs里全局设置PASS_MAX_DAYS,但全局设置会影响所有用户,包括服务账号,反而增加维护成本。我个人习惯是对交互用户逐个设置,服务账号保持永久密码。
6.4 如何通过日志精准定位用户异常
当用户反馈“无法操作”时,直接看日志是最理性的路径。常用命令:
sudo journalctl -u ssh --since today sudo grep 'sshd' /var/log/auth.log sudo grep 'sudo:' /var/log/auth.log sudo lastlog举一个实战例子:某用户突然无法登录,auth.log里显示Failed password for invalid user,这说明用户尝试登录的账号在系统里不存在。这种“invalid user”报错,通常是因为用户名拼写错误、账号已被删除,或有人尝试攻击时恰好报了那个名字。有了日志做依据,就不用凭感觉改这改那了。
最后分享一个我自己的习惯:每次在Ubuntu上做用户管理操作之前,我都会先跑一遍w和last,看看这个账号当前还有没有活动会话。别嫌这一步麻烦,我曾经就是没看,直接userdel -r删掉了一个还挂着交互页面的开发账号,虽说数据最后恢复了,但那一通折腾足够让人长记性。用户管理的基础确实是一堆简单命令,真正体现功力的,反而是操作前的确认和操作后的验证,这套习惯能帮你避开大多数事故。