开头直接用从业者口吻,结合自身经历引入主题,点出核心场景:运维巡检、日志查看、给外部同事开一个“只读账号”,同时点名关键技术useradd、sudo、mount只读挂载。然后自然过渡到正文。
只读用户,不只是chmod 444那么简单
做运维这些年,开过最多的账号大概就是“只读用户”。需求基本都长一个样:某人需要上服务器看看日志、查查进程、确认一下配置,但我们不想让他动任何东西,最好连删个临时文件的能力都没有。
这类账号最典型的使用场景是:给审计同事一个只读账号去机房巡检、给研发同事开个账号只查日志不碰服务、给外部合作伙伴临时开个只读账号让他们自己排查网络问题。听起来很简单对不对?不就是创建一个普通用户、不给他sudo权限嘛。真做过一遍你就会发现,Linux里的“只读”是个很暧昧的词,系统层面根本没有“全局只读”这个开关,你得从账号权限、文件系统挂载、命令执行策略三个层面一起动手,才能真正让用户“看得见但摸不着”。
这篇文章就围绕“linux创建普通只读用户”这件事,把我平时常用的组合方案一步步拆开讲透。包含useradd参数选型、密码锁定策略、sudo白名单怎么写才不踩坑、目录只读挂载的持久化配置,以及最后那些“看起来没问题但一登录就暴露”的坑。内容不算很深,但都是我实际敲过的命令,适合刚接触Linux账号管理的同学,也适合想规范服务器账号体系的人翻出来参考。
1. 需求拆解:什么场景下才需要“只读用户”
创建只读用户前,先搞清楚一件事:你到底是防“手滑”,还是防“恶意”,这是两条完全不同的技术路线。
如果是防手滑——比如自己或同事在正式环境里误删了配置文件、误改了服务参数,那我们要做的就是一个“尽量少权限”的账号。它本质上是靠权限管理降低人为失误的概率,属于运维规范层面的东西。这种需求不用做得太绝,只要把写权限、执行权限收干净,配合sudo白名单就能覆盖大部分场景。
如果是防恶意——比如怀疑有人拿到账号后会故意搞破坏、传文件、扫内网,那情况就复杂得多。普通用户权限下的“只读”根本不是真正的安全边界。你可以用chmod去掉写权限,但对方完全可以下载一个不依赖文件写权限的工具来反弹shell;你可以限制sudo,但对方如果利用系统漏洞获取root,那你所有只读限制都是形同虚设。所以做安全加固时,只读账号只是基础,还要配合网络隔离、审计日志、命令堡垒机等一整套机制。
我自己的经验是,绝大多数业务方要的其实都是第一种“防手滑”。所以下文会先把基础方案讲透,再给出一个实践中最稳妥的组合推荐,最后单独强调哪些场景下“只读用户”这个方案压根不够用,别指望它当安全防御用。
我们先看第一种需求下最简单的实现层次。所谓“只读”,落到具体动作上无非是三类:
- 不能新增、删除、重命名文件;
- 不能修改已存在文件的内容;
- 不能启动会改变系统状态的命令。
对应到Linux技术栈,就是账号权限、文件系统挂载策略、sudo命令白名单三个方面的事。熟练之后,三件事分开来做,条理非常清晰。
2. 基础账号创建:useradd参数选型与密码锁定策略
创建用户本身很简单,一句话:useradd -M -s /usr/sbin/nologin readonly_user。但真正生产环境里,我几乎不会用默认useradd参数直接建号,因为默认行为里藏着好几个隐患。
默认的useradd会同时创建用户家目录、会使用系统默认shell(通常是bash),还会按/etc/login.defs里的配置给用户设置密码时效。这些默认值对“只读用户”来说都不够理想。我给这类账号用的命令是这样的:
useradd -M -s /bin/bash -U readonly_user先解释几个参数:
-M:不自动创建家目录。为什么不建家目录?因为家目录默认是普通用户自己的读写区域,如果创建了,用户登录进去就能在自己的home里随便建文件。虽说不影响系统全局,但很多“只读”需求连这个也不想留。而且很多程序启动时会往HOME目录写缓存,不设家目录反而能避免一部分“运行时偷偷改东西”的场景。-s /bin/bash:给一个可用的shell。很多人会想,既然是只读用户,直接给它/usr/sbin/nologin,让他登录不了不就完了?这个想法在“纯只看文件”的场景是对的,但现实需求往往是“我需要他能登录上来敲几条查看命令”。nologin意味着SSH登录直接被拒,不符合需求。所以这里先用bash,后面通过sudo规则和rbash来限制,这个取舍在第三节细说。-U:同时创建同名用户组。后续给权限时可以用组为单位管理,比单个用户操作更灵活。
建完账号后,密码策略根据使用方式有两种选择:
第一种,如果这个账号只给一个人用,且对方通过SSH密钥登录,那我倾向于直接锁定密码:
passwd -l readonly_user锁定密码后,该账号无法通过密码登录,只能靠预置的SSH公钥认证。好处很明显:不存在密码泄露的问题,也不存在弱口令爆破的风险。
第二种,如果对方确实需要密码登录(比如对方在自己电脑上、我们没法控制他的客户端配置),那就用强密码,同时设置短时效:
passwd readonly_user chage -M 30 readonly_userchage -M 30表示30天后必须改密码。对这种低频使用的只读账号,密码长期不换就是个定时炸弹,建议至少给个有效期。另外把空密码策略关掉:
passwd -l root 的相反方向需要注意这里提醒一个坑:passwd -l在CentOS 6和早期系统上是向/etc/shadow密码字段前面加!!,效果可靠。但在某些较新的系统上,passwd -l其实只锁定了密码登录,不影响SSH密钥登录和其他PAM认证方式。这个不一定是坏事,只是要心里有数——锁定密码不等同于“锁死账号”,只是禁用了密码认证这一种方式。
账号建好之后,检查一下端口:确认真实shell是bash(而不是被nologin挡掉)、确认家目录没有自动生成。如果useradd版本默认配置与预期不一致,可以用grep -E 'CREATE_HOME|SHELL' /etc/login.defs查看系统默认值,明白默认行为再决定加不加参数。
3. 权限收敛:文件系统层面的“只读”
账号建好后,最核心的问题是:这个用户登录进去到底能碰哪些目录。
默认情况下,普通用户对/etc、/usr、/opt这些系统目录只有读权限,写不了什么。真正需要处理的是三类地方:用户自己的家目录、/tmp这类全局可写目录、以及你要开放给他查看但又不希望他改的业务数据目录。
3.1 家目录与全局可写目录的处理
如果按上一节操作加了-M,用户没有家目录,登录时shell会报“Could not chdir to home directory”,然后以/作为当前目录。这个报错不影响命令执行,但每次登录都提示,体验不好,而且某些程序依赖HOME环境变量,设置为不存在的路径可能引发额外问题。
我一般建议给只读用户建一个家目录,但把目录权限做成只读:
mkdir -p /home/readonly_user chown readonly_user:readonly_user /home/readonly_user chmod 555 /home/readonly_userchmod 555就是r-xr-xr-x,用户自己也没写权限。但你可能会问:“他自己的家目录都写不了,那shell启动时要写.bash_history怎么办?”答案很简单——它写不了就写不了,顶多不记录历史命令。如果非要历史记录,就单独指定一个可写的小目录,但我会明确建议:只读用户不需要历史记录,抹掉反而更干净。
相比之下,/tmp是个真正需要注意的洞。/tmp对所有人开放写权限,任何用户都能在里面创建文件。如果你严格追求“不能被写入”,那就得用只读挂载把/tmp变成只读,但这个动作很可能会影响系统正常运行——很多服务、临时文件、socket都在/tmp下。这就回到了“防手滑”和“防恶意”的取舍。对防手滑场景,我不建议动/tmp,因为改动成本太高;对防恶意场景,只读挂载/tmp也没多大用,攻击者可以换一个可写目录。后面第6节还会专门讲这个矛盾。
3.2 关键业务目录的只读挂载
如果要开放的目录本身需要被用户写(比如业务程序要写日志目录、要写临时文件),但又不想让只读用户去改历史数据,那就要用到Linux的bind mount配合remount只读。
它的核心思路是:把系统里的某个目录重新挂载一份到另一个挂载点,并附加ro属性。这条命令是:
mount --bind /data/logs /data/logs_ro mount -o remount,ro,bind /data/logs_ro第一行把/data/logs绑定到/data/logs_ro,两个路径指向同一份数据;第二行把绑定后的挂载点重新挂载成只读。之后/data/logs_ro看到的内容和/data/logs完全一致,但任何写入尝试都会被拒绝。
为什么需要bind mount而不是直接mount -o remount,ro /data/logs?因为后者会直接把原始目录变成只读,影响业务写入。bind mount把同一个文件系统“映射”出一个只读视角,原始目录的访问方式完全不受影响。这个技巧在给只读用户开日志目录时非常实用。
生产环境里,这种绑定挂载一般是写在/etc/fstab里的,否则重启即失效:
/data/logs /data/logs_ro none bind 0 0 /data/logs_ro /data/logs_ro none bind,ro 0 0fstab里写两行的原因在于:单独一行bind不会带上ro属性,需要把绑定后的挂载点再用带ro的bind规则覆盖一次。如果你用systemd比较熟,也可以写成mount unit,效果更可控,但fstab两行式最通用,不依赖systemd版本。
需要注意,只读挂载挡得住普通用户,挡不住root。root对任何只读挂载点都有强制写的能力(可以remount成rw),所以这个机制本质上是约束普通操作,不是安全边界。另外,业务程序本身如果还在写/data/logs,那你通过/data/logs_ro看到的最新日志会有几秒到几分钟的延迟,这取决于写缓存和挂载传播,属于正常现象,不用奇怪。
3.3 文件权限和不可变位:更细粒度的写保护
除了挂载只读,更细的权限控制也有必要。比如某个配置文件,你想让只读用户能看、能执行某些脚本,但绝对不能改,可以用chmod把写权限收掉:
chmod 444 /etc/nginx/nginx.conf chattr +i /etc/nginx/nginx.confchattr +i给文件加了immutable位。加了这层保护后,即使root用户也无法直接修改该文件,得先chattr -i解除。这比普通chmod更强,适合锁定核心配置文件。但有几点要注意:chattr +i只对ext4、xfs等部分文件系统有效,对tmpfs、网络文件系统可能不生效;另外有些程序重装或升级时会因为无法改写配置而报错,所以这个操作只建议在“确定不需要改动”的文件上用。
我遇到过一个案例,某同事给整个/etc目录递归加了chattr +i,结果第二天系统更新时一堆包管理器因为无法覆盖配置文件而失败。所以不可变位这个手段,要用在点子上,千万别冲动到对目录整体操作。给单个配置文件加没问题,批量加之前先想想系统更新怎么办。
4. sudo白名单:让用户只能执行“查看类”命令
文件层面的“只读”解决的是“能不能修改文件”的问题,但用户登录后能执行什么命令,是另一套独立体系。默认情况下,普通用户能执行大部分“非特权”命令,包括passwd(改自己密码)、mount(查挂载信息)、su(尝试切用户)等。这里面有些命令是必须禁掉的,有些则要单独放行。
控制这个的入口就是sudo配置。我强烈建议不要直接编辑/etc/sudoers主文件,而是到/etc/sudoers.d/下新建一个独立文件,这样既方便管理,也不容易改坏主配置。推荐做法:
visudo -f /etc/sudoers.d/readonly_user为什么用visudo?因为它是官方推荐的编辑器,保存时会做语法校验,能避免因为sudoers语法错误导致所有sudo操作不可用的极端情况。我见过有人直接vim编辑sudoers然后写错语法,结果整台服务器的sudo都失效,还好他开着另一个root会话才救回来。用visudo等于买了个“写错不生效”的保险。
下面是一套我自己常用的只读用户sudo规则:
readonly_user ALL=(root) NOPASSWD: /usr/bin/cat, /usr/bin/less, /usr/bin/tail, /usr/bin/head, /usr/bin/grep, /usr/bin/ps, /usr/bin/top -b -n 1, /usr/bin/df, /usr/bin/free, /usr/bin/du, /usr/bin/ls, /usr/bin/stat, /usr/bin/mount, /usr/bin/nice, /usr/bin/iostat, /usr/bin/vmstat, /usr/bin/ss, /usr/bin/ip addr show, /usr/bin/curl -s, /usr/bin/wget -O -详细解释几个关键点:
第一,所有允许执行的命令必须写绝对路径。sudo在匹配命令时是精确到路径的,只写cat会被判定为无权限。这是初学者最容易踩的坑。怎么查一个命令的绝对路径?用which cat、type -a top先查出来。
第二,top这类交互命令要特别注意。top本身是交互式的,直接放行等于给用户一个交互shell的跳板(虽然跳板功能有限,但终归不干净)。所以我把top限制成top -b -n 1这种批处理单次执行模式,即“一次性输出后退出”。这样用户可以用它看实时信息,但不会进入交互界面。
第三,su命令是绝对要禁掉的。默认情况下,如果只读用户知道root密码,他完全可以用su -切到root,绕过所有限制。同样要禁的还有sudo su、sudo bash、sudo vi这类命令。vi尤其危险,因为vim里可以执行:!sh直接起shell,到那时你所有权限方案都白搭了。
禁命令的写法:
readonly_user ALL=(root) !/usr/bin/su, !/usr/bin/sudo, !/usr/bin/bash, !/usr/bin/sh, !/usr/bin/vi, !/usr/bin/vim, !/usr/bin/nano有人可能问:“前面允许了cat,后面又禁bash,优先级怎么算?”sudo的规则是后者覆盖前者,而且同一行内先匹配到哪条生效哪条。为了清晰,我会把禁命令放在允许命令前面或单独一行。实际测试中我建议把禁用的交互shell命令放到规则最前面,避免因为规则顺序导致意外的放行。
不过这里有一个绕不过去的现实:一旦允许了cat、less这种查看命令,用户理论上可以通过这些工具读取系统里大量的敏感文件,包括/etc/shadow(如果root权限运行cat的话)、其他用户的私钥、数据库配置里的密码等。所以只读用户这个方案,天生就是基于信任的——信任对方不会做越界的事。如果你连这个信任都没有,那需要的根本不是“只读用户”,而是“跳板机+审批流”。
再补一个重要细节:NOPASSWD:表示执行这些命令不需要再次输入密码。对只读用户我一般会加,因为这类账号通常是半自动巡检或者快速查问题用的,每次都要输密码很麻烦。但如果你追求更高的审计完整性,可以去掉NOPASSWD:,让sudo记录里多一次密码验证事件,审计日志更完整。
5. shell层面的限制与加固
账号建了、sudo白名单配了、目录挂载也做了只读,但别急着收工。普通用户登录后拿到的是一个完整bash shell,这个shell本身有能力做不少事:可以cd到任何目录、可以设置环境变量、可以执行PATH下的所有未受sudo限制的命令。这些能力里藏着几个容易被忽略的绕过路径。
5.1 rbash:比bash更“自闭”的受限shell
Linux自带的rbash(restricted bash)是bash的一种受限模式,它限制用户在登录后:
- 不能
cd切换目录,只能待在当前目录及其子目录; - 不能把
PATH环境变量改成自定义值; - 不能重定向输出到任意文件;
- 不能执行包含
/的绝对路径命令,只能执行PATH目录下的命令; - 不能
unset内置变量。
创建时这样改用户的shell:
usermod -s /bin/rbash readonly_userrbash的思路不错,但它并不能完全锁住用户,绕过的案例一大堆。比如通过su readonly_user切另一个shell、或者利用ssh命令执行远端命令、或者在允许的命令里加上空格参数来隐式调用别的程序。所以rbash适合条件有限、权限要求不太严格的场合,但对掌握Linux基本攻击手法的人来说,形同虚设。
严格防护的做法是用命令白名单配合特定入口,比如系统本来就提供的rssh或者加固后的scp、sftp限制。不过我个人在实际项目中很少给只读用户上rbash,因为它在降低风险的同时也严重影响了正常使用体验。
5.2 环境变量与真实路径防护
环境变量才是最容易被忽视的“旁路”。比如用户在自己的~/.bashrc里写export PATH=/tmp:$PATH,然后放一个假的ls脚本到/tmp,之后他执行ls时就会跑到/tmp下的恶意脚本。这个脚本虽然以他自己的权限运行,但在某些提权漏洞利用中,这种“可控代码执行”是起点。
怎么防?第一步是给账号设置一个只读的、受控的PATH。在用户自己的~/.bash_profile里加:
export PATH=/usr/local/bin:/usr/bin:/bin umask 027这里umask 027确保新建文件默认不给其他用户写权限,降低“写文件后影响其他用户”的概率。
第二步是把.bashrc的属主改成root,并且设置为只读:
chown root:root /home/readonly_user/.bashrc chmod 644 /home/readonly_user/.bashrc这样用户无法修改自己的shell配置文件,也就无法往里面植入恶意环境变量。但要注意:.bashrc的属主如果改成root,用户登录时bash读取该文件会把它当作“不可写”,有些bash版本会忽略不可写的启动文件,导致配置不加载。解决方法是仍然保留用户可读,但使用readonly标记变量,或者干脆把要保护的变量写死在/etc/profile.d/下的全局配置里,并设置只读属性。这招更稳:
cat > /etc/profile.d/readonly_env.sh <<'EOF' export PATH=/usr/local/bin:/usr/bin:/bin export HISTFILE=/dev/null export HISTSIZE=0 EOF chmod 755 /etc/profile.d/readonly_env.sh设置HISTFILE=/dev/null的意思是让所有历史记录写入空设备,也就是不落地。对只读用户,我很推荐这个配置——既保证不留痕迹,也规避了~/.bash_history写失败的一堆报错。
5.3 需要清醒认知:shell限制的边界在哪里
讲到这一步,必须泼盆冷水。rbash也好、环境变量收口也好、sudo白名单也好,都属于“用户态限制”,不是“内核级隔离”。一个真正的攻击者,只要能执行任意代码,有无数种方法绕过这些限制。最常见的路径就是利用系统服务漏洞提权到root,一旦拿到root,你再多的只读取设置都可以被绕过。换句话说,这套方案对“防手滑”是优秀的,对“防攻击”是不够格的。
安全界有个说法叫“least privilege”,最小权限原则,只读用户本质上就是最小权限原则在账号管理上的落地。它降低的是“误操作”和“无意识越权”的概率,而不是“有意识攻击”的风险。
6. 常见问题与排查实录
讲了这么多理论,下面把实际运维中我反复踩过的坑和排查过程整理成一个速查表。说实话,这些坑每个都是拿真实线上环境换来的经验,值得收藏。
6.1 用户登录后显示“Could not chdir to home directory”
这个最常见。用户认证成功,但shell无法进入家目录,提示Could not chdir to home directory: No such file or directory。
原因通常是创建用户时用了-M,而用户登进系统后shell尝试切到/home/readonly_user失败。解决方案在上文提过:手动创建家目录并设置只读权限,或者修改/etc/passwd里该用户的家目录指向一个已存在的目录如/。我个人建议前者,因为家目录还是得有,否则一些工具(如mysql客户端)的默认行为会异常。
6.2 只读挂载不生效,目录依然可写
有次配置了fstab里bind+ro,重启后居然还是可写的。排查过程是这样的:
先执行mount | grep readonly_ro查看挂载状态,发现确实显示ro,但试写入还是成功。后来查日志发现是overlayfs和内核版本兼容问题——某些旧内核在处理bind remount后,文件系统表面显示ro但实际写入路径穿透到底层可写的overlay。解决办法是换用mount -o remount,bind,ro /data/logs_ro之后再用touch /data/logs_ro/test验证一次。这个“验证”步骤不能省,别只看/proc/mounts的输出就放心了。
6.3 sudo配置无效:明明放行了cat却提示没有权限
这个问题十有八九是路径没写对。用户用sudo cat /var/log/nginx/access.log被拒绝,第一反应是查是否在sudoers里写了cat。用which cat查看cat的真实路径,发现是/usr/bin/cat还是/bin/cat,这在不同发行版上不一样。Debian系和CentOS 6的cat路径可能不同,旧系统往往是/bin/cat。sudo规则必须和实际路径一致,权限判断是字符串精确匹配,不是靠PATH解析。
另一个原因是规则的顺序问题。如果规则里同时写了/usr/bin/cat和!/bin/cat,而该命令的路径在系统里被链接到/bin/cat,那条禁命令规则反而会生效。所以检查时把sudo -l -U readonly_user的输出和which cat的结果对照一遍,很快就能定位。
6.4 只读用户能改自己的密码,要禁吗?
默认情况下普通用户可以执行passwd修改自己密码。对只读用户来说,这个能力的风险在于:如果这个账号是给特定人用的,修改密码意味着别人可能被挤掉或进不来。但反过来,如果禁止改密码,一旦密码过期又没法自助找回。
我的建议是,在账号创建时就决定好:如果是为了配合密钥登录,用passwd -l锁掉密码,那么改密码的问题自然不存在;如果必须保留密码登录,那passwd还是得留,因为允许改密码的运维成本远低于“密码过期后重置”的成本。
6.5 chattr +i带来的坑:删不掉也改不了
某个文件设了chattr +i,后来发现程序需要更新它,结果root也写不了。解除方法很简单:
chattr -i /path/to/file但这个坑的本质是:你给文件加了immutable位后,之后任何自动化任务、备份工具、软件升级都可能会因无法写入而报错。如果文件很重要,建议设完之后建立一套“变更记录”:什么时间、为什么加只读位、由谁负责解除。排查问题时能省很多时间。
6.6 “只读用户”其实还有局限:root权限的程序不受控
最后一个要牢记的认知是:只读用户的所有限制,都只作用于这个普通用户身份本身。如果系统上有其他root权限的进程(比如Nginx worker、数据库服务、监控Agent),这些进程对文件系统拥有完全的控制权。只读用户虽然权限很低,但可以通过某些系统调用间接影响这些进程的运行,比如短暂占满CPU、发送信号等。从严格安全角度看,这不算一个真正的隔离环境。
如果业务确实需要一个能被外部使用的“几乎完全隔离只读环境”,我自己会考虑用systemd-nspawn或轻量级容器方案,把用户限制在一个临时或只读的rootfs里。那是一条比普通用户+只读挂载更强的路,但它涉及镜像管理、网络配置和更复杂的权限映射,一般仅在面向不可信对象的场景下才值得做,适合以后单独开一篇来写。
7. 写到最后:只读账号的运维实战建议
回到开头那个问题——“linux创建普通只读用户”到底怎么做才叫合格?
我个人最常用的一套组合如下:用useradd -M -s /bin/bash创建无家目录用户,密码锁定并用SSH密钥登录;sudo白名单只放查看类命令并禁掉交互shell工具;对要开放的业务目录做bind+ro挂载;再加一层/etc/profile.d/全局环境变量收口。整套下来,配置时间在十分钟以内,效果却相当直观:用户能看日志、查进程、确认端口,但任何rm、vi、su、reboot都会被系统无情拒绝。
这套方案的典型画像就是“给一个可信但容易手滑的人一份最小权限”。它能把运维操作面收得很窄,也能在出事时让审计日志清楚地告诉你“这个用户做了什么”。但请记住,它不是安全边界。如果对方恶意,光靠这套是拦不住的。真要说我踩过最深的坑,就是曾经把“只读用户”当作“安全隔离”来用,结果对方利用一个业务服务的历史漏洞拿到了root,所有只读机制瞬间化为乌有。从那以后我给自己定了一条规矩:只读账号解决误操作,安全隔离必须靠真正的隔离手段(虚拟化、容器、网络策略),两者永远不要混为一谈。
如果你的需求就是给一个同事或审计人员开一个“看不见摸得着改不了”的账号,照着本文这套思路操作,应该足够应对绝大多数场景。最后再分享一个小习惯:每次配置完只读用户,我都会用这个账号重新登录一次,跑一遍touch /tmp/test、rm -rf /home/readonly_user、sudo bash之类的动作,确认全部被拒之后才算验收通过。这个测试过程只需要几分钟,但能帮你提前发现那些配置时没注意的漏洞。别嫌麻烦,线上出问题时再补,成本就完全不一样了。