我第一次接触 Restricted Shell 是在一个给外包人员开放服务器查询权限的需求里。当时的想法很朴素:给一个不能 cd、不能改 PATH 的 shell,他总该老实了吧。结果不到半天,对方就把脚本写到 /tmp 下直接跑了起来——当然这不怪他,怪我没读懂 Bash 手册里那句被无数人忽略的话:A restricted shell does not provide a security boundary。
后来我花了很长时间重新梳理这个功能,才明白一个道理:受限 Shell 不是用来防恶意的,它是用来防手滑的。它的价值在于降低误操作半径,而不是构建一个攻不破的牢笼。这篇文章就围绕 Bash 的 Restricted Shell 展开,讲清楚它到底限制了什么、怎么正确激活、哪些场景真正适合用它,以及最关键的——为什么你不能只靠它来保护一台机器。适合正在做 Linux 运维、或者想给临时账号加上操作边界的读者,也适合被"restricted shell 为什么一碰就碎"这个问题困扰过的人。
1. 什么场景下你会需要一把"锁住的终端"
1.1 受限 Shell 在真实运维中的位置
我在实践里发现,绝大多数人认识 restricted shell 都是出于类似的需求:给客户开一个只读查询账号、给外包团队一个只能执行指定命令的入口、或者给无人值守的 kiosk 机器准备一个无法乱跑的交互环境。
这些需求有个共同点:用户本身不是恶意的,他们只是可能不熟悉系统、可能会误操作、可能会在好奇心的驱使下敲出几个危险命令。受限 Shell 解决的就是这个问题——它像给剪刀加了一个儿童锁,不是让人撬不开,而是让正常使用过程中的意外大幅度减少。
Linux 世界里能做到类似事的工具不少,比如 rssh、scponly,甚至可以把用户的登录 shell 直接指向某个脚本。但 Bash 自带的 restricted mode 和这些方案不太一样:它不需要额外安装任何软件,只需要一个参数或者一个链接名就能激活,天然集成在每一份 Bash 里,所以非常适合作为第一道防线。
1.2 受限 Shell 和普通 Bash 的本质区别
很多人以为 restricted shell 是一套独立的程序,其实不是。它就是 Bash 本身,只是启动时带上了 -r 选项,或者通过名为 rbash 的符号链接被调用,从而进入受限模式。
进入受限模式后,Bash 依然执行启动文件(比如 /etc/profile、~/.bashrc 等),依然会读取环境变量,但有一批内置命令和 shell 行为会被显式禁止。简单的理解是:普通 Bash 里的"改环境、换目录、重定向、替换进程"等能力,在受限模式下被大面积收紧了。
有个特别容易忽略的点是:受限模式不仅影响交互命令,也会影响启动文件。如果启动文件里含有限制不允许的操作,Bash 执行到那一步时会直接报错,但不会因此拒绝启动——这和我们后面要讲的 bashrc permission denied 问题有很强的关联,后面展开说。
| 能力 | 普通 Bash | Restricted Shell |
|---|---|---|
cd 切换目录 |
允许 | 禁止 |
修改 PATH、SHELL、ENV 等变量 |
允许 | 禁止或忽略 |
执行带 / 的绝对/相对路径命令 |
允许 | 禁止 |
使用 > 等操作符重定向到文件 |
允许 | 禁止 |
用 exec 替换当前 shell 进程 |
允许 | 禁止 |
用 enable -f 加载外部内置命令 |
允许 | 禁止 |
| 关闭受限模式 | 视情况 | 禁止 |
这张表基本概括了受限模式的骨架。下面我们逐个拆开,看每一条限制背后到底在防什么。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 受限 Shell 到底把哪些门焊死了
2.1 被禁用的命令与行为清单
我们来完整过一遍 Bash 手册里列出的受限模式限制项,每一步我都补上"为什么要有这条"的解释。
第一,cd 被禁止。含义非常直接:你无法改变当前工作目录。这个限制防止用户在登录后逛到别的目录里,比如 /etc、/root 或者其他用户的 home。实际操作中,cd 一旦被禁,用户所有操作都只能围绕当前所在的登录目录展开,行为半径一下子收缩了很多。
第二,SHELL、PATH、ENV、BASH_ENV 这几个变量不能被设置或取消。这里的关键不是"这几个变量很重要",而是这几个变量几乎控制了 shell 的一切行为来源。PATH 决定了你能执行什么程序,ENV 和 BASH_ENV 决定了启动 shell 时会加载哪些额外配置。把它们锁死,等于把环境入口焊住了。受限模式下尝试给这些变量赋值会被静默忽略,尝试 unset 会被直接拒绝。
第三,不能执行任何包含 / 的命令名。这条非常反直觉,但很有效。普通模式下你可以敲 /bin/ls 或 ./script.sh,受限模式下这种写法直接报错。这意味着用户无法通过绝对路径去调用任何程序,只能依赖 PATH 里已经配置好的命令。而 PATH 又被锁死了,所以用户最终能执行的,只有系统管理员提前放进白名单目录里的那些命令。
第四,. 内建命令(source)不能加载含 / 的文件名。这条和上面的思路一致,防止用户通过 source 执行任意路径下的脚本。如果你写 . /tmp/evil.sh,会直接失败。
第五,重定向到文件被禁止,包括 >、>|、<>、>&、>> 这些操作符。用户无法把命令输出写入文件。这既防止了篡改文件内容,也防止了通过重定向创建新的脚本文件。
第六,exec 被禁止。这个限制非常要命。在普通 shell 里 exec bash 可以用一个全新的非受限 bash 替换当前进程,既然 restricted shell 不想让你换个解释器,那就直接禁掉 exec。
第七,enable -f 和 enable -d 被禁止,不能动态加载或删除内建命令。这意味着用户无法通过加载一个外部模块来扩展 shell 能力。
第八,command -p 不能使用。command -p 的作用是用默认 PATH 来查找和执行命令,在受限模式下这条路径也被堵死了。
第九,启动时不能从环境中导入函数定义,SHELLOPTS 环境变量也不会被解析。这条是为了防止有人通过环境变量把恶意函数塞进受限 shell。值得留意的是,BASH_ENV 与 ENV 在启动时虽然受限制,但启动文件本身还是会被读取,所以对启动文件的权限控制非常关键。
第十,不能通过 set +r 或 shopt -u restricted_shell 关闭受限模式。这个设定让受限模式"只能进、不能退",在当前 shell 生命周期内始终有效。
2.2 为什么这些限制都指向同一个核心变量
把上面这些条目放在一起看,你会发现它们并不是随机的拼凑,而是围绕一个核心目标:阻止用户改变 shell 的执行环境。
- 不让
cd,是为了把你钉在某个固定的目录里。 - 不让改
PATH,是为了把你钉在一组固定的命令集合里。 - 不让带
/执行命令,是为了防止你绕过PATH找其他程序。 - 不让重定向和
exec,是为了防止你留下文件或换掉进程。 - 不让
unset关键变量,是为了防止你拆掉现有约束。
这套约束在"防止普通用户乱跑"这个层面上是自洽的。但请注意,它的自洽范围仅限于 shell 本身的操作,并没有继续向上约束「用户可以通过某些外部命令间接做更多事情」。这正是它和真正的沙箱之间最本质的差别。
2.3 用一段实操演示感受受限模式的"窒息感"
纸上谈兵没什么意思,我们直接进终端感受一下。假设你已经有了一个普通用户的登录权限,执行:
bash复制bash -r
此时你的提示符应该没变,但你的操作空间已经完全不同了:
bash复制pwd
# /home/limited
cd /tmp
# bash: cd: restricted: 不能改变工作目录
PATH=/usr/bin:/bin
# bash: PATH: restricted: 该变量不能被修改
/bin/ls
# bash: /bin/ls: restricted: 不能指定包含/的命令名
echo "hello" > /tmp/test.txt
# bash: /tmp/test.txt: restricted: 不能使用重定向
每一条报错都在提醒你:这个 shell 里你的大多数"正常操作"都失效了。第一次接触这种模式的人会非常不适应,但这正是它的意义——把所有可能造成影响的命令全部拦在门外。
不过你也应该注意到:上面这些命令中,如果你试一下 ls 本身(不带路径),在 PATH 里没有 /usr/bin 的情况下,很可能提示 command not found。这引出一个非常关键但又常被忽略的问题:受限 Shell 默认的 PATH 可能根本不能满足正常使用。后面讲激活方式时,我会专门说怎么处理 PATH。
3. 激活受限 Shell 的正确姿势
3.1 bash -r 与 rbash 的细微差别
激活受限模式有两种等价的方式:
第一种是显式参数:bash -r 或者 bash --restricted。这种方式适合临时测试,比如你想看看当前用户的命令在受限模式下会有什么表现,直接敲一下马上就有感觉。
第二种是通过符号链接 rbash。当 Bash 发现自己的调用名是 rbash 时,它会自动进入受限模式,不需要额外传参。实际操作中我们会把 /bin/rbash 链接到 /bin/bash:
bash复制ln -sf /bin/bash /bin/rbash
然后通过 usermod -s /bin/rbash username 把某个用户的登录 shell 指向 rbash。这样用户一登录就直接进入受限模式,比手动执行 bash -r 可靠得多。因为如果依赖用户手动执行,他完全可以不执行,也就谈不上限制了。
需要强调一个细节:rbash 只是一个链接名,它并不是一个独立的二进制。你可以把 /bin/rbash 理解为"带默认参数 -r 的 bash"。如果系统中没有这个链接,你需要自己创建,很多精简安装的系统默认没有 /bin/rbash。
3.2 在 profile 里锁定 SHELL 变量
除了修改用户 shell,还有一种常见做法是在启动文件里把 SHELL 变量改为 /bin/rbash。例如在 /etc/profile 或用户的 ~/.bashrc 中加入:
bash复制export SHELL=/bin/rbash
但这里有个非常大的坑:如果用户的登录 shell 仍然是最普通的 /bin/bash,而只是在启动文件里设置了 SHELL=/bin/rbash,那么用户在交互提示符下依然拥有完整能力。因为 SHELL 变量只是一个环境变量,它不会限制当前已经运行的 bash 实例。
真正正确的做法是两者配合:
- 用
usermod -s /bin/rbash username把用户的登录 shell 改为受限模式。 - 在
/etc/profile.d/limited.sh这样的系统级启动文件里设置好受限用户需要的PATH和别名。
为什么要把 PATH 的设置放在系统级启动文件里?因为受限模式下,用户自己的启动文件可能无法被修改(如果权限控制得当),而系统级的配置可以由 root 统一维护,保证用户一登录就拿到一个合理的白名单命令目录。
3.3 配合 sshd 限制远程登录的典型配置
在真实运维中,受限 Shell 最常见的落地方式是通过 SSH 的公钥授权文件来控制。你可以在 authorized_keys 中使用 command= 前缀,强制用户在连接到服务器时只执行指定的命令,而不是拿到一个完整交互 shell:
code复制command="bash -r",no-port-forwarding,no-agent-forwarding ssh-rsa AAAAB3NzaC1yc2E... user@host
这段配置的含义是:当这个密钥发起 SSH 连接时,服务器会执行 bash -r,并且禁止端口转发等操作。这样即使用户的账号存在,他也只能进入受限 shell,无法直接获得普通交互环境。
如果你希望所有使用某个账号的 SSH 登录都进入受限模式,也可以在 /etc/ssh/sshd_config 里针对该用户设置:
code复制Match User limited
ForceCommand bash -r
AllowTcpForwarding no
这个方案的优点是不需要修改系统全局 shell 设置,只对特定用户生效。缺点是需要格外注意 PATH 和用户权限,因为 ForceCommand 只是把用户"塞进"了受限模式,但用户能够执行哪些外部命令,依然取决于他的用户权限和 PATH 中的内容。
3.4 git bash/Windows 环境下的特殊情况
很多读者是在 Windows 的 Git Bash 里第一次遇见 bash 的,所以这里也多说一句:Git Bash 使用的是 MSYS2 提供的 bash,bash -r 本身是可用的,但因为 Windows 下路径规则与 Linux 不同,受限模式的某些限制表现会出乎意料。
比如热词里出现的 bash: /c/users/14301/appdata/roaming/npm/claude: cannot exec 这类报错,在实际排查中很多时候不是受限模式引起的,而是 Windows 下 npm 全局安装的脚本带有了错误的换行符或没有可执行权限。如果你在 Git Bash 里执行某个命令时遇到 cannot exec,优先检查三点:文件首行是否有正确的 shebang(比如 #!/bin/sh)、文件是否含有 CRLF 换行、文件的执行权限位是否正确。
受限模式在 Git Bash 下还有一个常见坑:路径转换。MSYS2 层会把 /c/Users/... 自动转换成 C:\Users\...,但受限模式里对含 / 命令名的禁止规则依然生效,所以你会发现 C:/Program Files/... 之类的路径很难直接执行。在这种环境下,我更建议把真正需要管控的场景放到 Linux 服务器或 WSL 里面做,不要在 Git Bash 上死磕受限模式。
4. 受限 Shell 不是安全牢笼:理解限制的边界
4.1 为什么 Bash 官方文档反复强调"不是安全功能"
Bash 手册里有一句话写得很直白:A restricted shell does not provide a security boundary。这句话的份量,很多人一开始是感受不到的,直到被现实教育过一次才懂。
为什么它不是安全边界?因为受限模式只能约束 shell 内置行为,但它约束不了外部命令的行为。如果用户的 PATH 里有 vi、less、find、python、perl 这类功能强大的程序,这些程序完全可以进一步执行其他命令,甚至启动一个新的不受限 shell。
一个非常经典的例子:如果用户可以执行 vi,那么进入 vi 后输入 :!bash,就会得到一个不受限的 bash。如果你把 less 开放给用户,在 less 的提示符里输入 !bash,同样可以逃逸。如果你把 python 开放给用户,那更不需要绕圈子,直接调用 subprocess 模块就能执行任意命令。
我在这里把这个行为演示出来,不是为了教大家破坏限制,而是想帮所有人建立一个认知:受限 Shell 覆盖范围只到"shell 自身操作"这一层,它管不到外部程序之间的组合攻击。你设计权限方案时,如果只封 shell 而放开了 vi 和 python,那这个方案就等于裸奔。
4.2 常见的"越过限制"路径与防御对策
虽然没有必要把所有逃逸手法罗列一遍,但有几个典型的路径值得防御者了解,因为它们能帮你判断自己在配置里是否开了后门。
第一条路径是通过编辑器逃逸。你限制了用户不能执行 /bin/bash,但用户如果能在 vi、less、more 这类程序里执行子 shell,限制就会落空。应对办法很简单:受限模式下,白名单目录里绝对不要放这类能产生子进程的交互程序。如果业务确实需要查看文件,优先考虑 cat、grep 这类不提供子 shell 功能的命令,并且要注意 cat 配合 less 之类的分页器仍然可能引发问题,所以最稳妥的做法是连分页器都不放进去。
第二条路径是用系统自带脚本解释器。python、perl、ruby、awk 这些解释器几乎都能执行系统命令。哪怕你只给了 find,某些版本的 find 也支持 -exec 参数直接调用命令。所以在受限用户的 PATH 白名单里,这些解释器一条都不能出现。别觉得"只给一个 python 应该没关系",实际上给了解释器就等于把整个系统都给了对方。
第三条路径是利用可写目录和已有命令结合。假设受限用户可以在自己的 home 目录里写文件,而系统又在某个目录放了一个辅助脚本,用户完全可以把脚本复制出来改掉再执行。这时候你需要确保用户对这些文件没有写权限,或者你的白名单目录不是用户可写的。
防御的核心思路是:不要只看 shell 限制了什么,要看"受限用户能碰到的所有程序组合起来还能做什么"。做一次完整的权限审计,把可疑程序全部从白名单目录里移走,才能把逃逸面降到最低。
4.3 何时该用真正的 sandbox/容器
如果说受限 Shell 适合防误操作,那么真正的高安全隔离场景应该交给更底层的机制:
| 方案 | 隔离强度 | 开销 | 适用场景 |
|---|---|---|---|
| Restricted Shell | 低,仅约束 shell 操作 | 极低 | 防误操作、给临时账号限定命令集合 |
| chroot 环境 | 中,限制文件系统可见范围 | 低 | 需要锁定用户只能看到一个目录树 |
| 容器(如 Docker) | 中高,进程与文件系统隔离 | 中 | 运行不信任的应用或脚本 |
| 虚拟机 | 高,硬件级隔离 | 高 | 面对强对抗场景,需要完全隔离客户系统 |
从我的经验看,如果你的需求是"给一个已知且非恶意的用户缩小操作范围",Restricted Shell 完全够用。如果你的需求是"运行来自互联网的不可信脚本,且不允许它访问主机数据",那至少要用容器把进程装起来。如果对方可能主动发起攻击,那直接上虚拟机都比在 shell 配置上抠细节靠谱。
记住这个判断逻辑:受限 Shell 解决的是"边界内的秩序",而不是"边界上的防御"。想要边界防御,去选边界工具。
5. 实操中的高频坑:环境文件权限与 PATH 管理
5.1 遇到 bashrc permission denied 时你在看什么
热词里有一条非常典型:bash: /home/xtest/.bashrc: Permission denied。很多人在给用户配置受限环境时都会撞上这个问题,但它和受限模式本身不一定有直接关系,它说明的是启动文件权限或目录访问权出了问题。
排查这一类问题,我建议按照下面的顺序走:
bash复制ls -l /home/xtest/.bashrc
# 确认文件的属主和权限
namei -l /home/xtest/.bashrc
# 确认从根目录到文件之间每一层目录的访问权限
sudo -u xtest bash -c 'echo ok'
# 以该用户身份测试能否正常读取
最常见的元凶有三个:一是 .bashrc 属主不是该用户,导致用户只有读权限甚至没有读权限;二是 /home/xtest 目录权限被设置为 700 而属主是 root,用户根本无法进入自己的家目录;三是 SELinux 或挂载选项开启后,文件上下文与预期不符。
这个问题在受限 Shell 场景下更值得重视,因为受限模式启动时要读取启动文件,如果启动文件不可读,用户可能就会掉进一个"连提示符都奇怪"的环境,或者系统配置的 PATH 压根没生效。解决起来也简单:先确认目录链路的可进入性,再确认 .bashrc 的属主为对应用户,最后用 setenforce 0 或正确的 SELinux 上下文排除策略层干扰。
5.2 环境变量在受限模式下为什么"不生效"
受限模式下有一个让人非常困惑的现象:我在远程登录时明明通过 SSH 传了环境变量,为什么进到 shell 里 echo $PATH 还是老样子?
答案在受限模式的第二条限制里:PATH、ENV、BASH_ENV 等变量不能由用户在受限 shell 内修改,同样也会阻止来自启动阶段之外的环境注入。如果你执行:
bash复制ssh limited@host "export PATH=/custom/bin:$PATH; mycmd"
在受限模式下,这条命令大概率不会按你的预期执行。export 操作会被忽略,PATH 保持不变,而 mycmd 如果不在原 PATH 中,就会提示 command not found。
解决这个问题只有一个可靠姿势:由 root 提前在系统级启动文件(比如 /etc/profile.d/limited.sh)里设置好受限用户的 PATH。例如:
bash复制# /etc/profile.d/limited.sh
if [[ "$(basename "$SHELL")" == "rbash" ]]; then
export PATH=/home/limited/bin
fi
这样用户一登录,PATH 就被锁定到白名单目录,而且用户在受限模式下自己改不了。这个做法我推荐给所有需要部署受限环境的人,能省掉很多排查环境变量问题的痛苦。
5.3 给受限用户准备一个可用的家目录环境
前面说了这么多理论,最后给出一套可以直接落地的配置流程。假设我们要创建一个用户 limited,只允许他执行 ls、pwd、echo、grep 和 cat 这几条命令:
bash复制# 1. 创建受限 shell 链接
ln -sf /bin/bash /bin/rbash
# 2. 创建用户并设置受限登录 shell
useradd -m -s /bin/rbash limited
# 3. 创建白名单目录
mkdir -p /home/limited/bin
# 4. 把允许的命令链接进白名单目录
ln -s /bin/ls /home/limited/bin/ls
ln -s /bin/pwd /home/limited/bin/pwd
ln -s /bin/echo /home/limited/bin/echo
ln -s /bin/grep /home/limited/bin/grep
ln -s /bin/cat /home/limited/bin/cat
# 5. 设置系统级受限环境
cat > /etc/profile.d/limited.sh <<'EOF'
if [[ "$(basename "$SHELL")" == "rbash" ]]; then
PATH=/home/limited/bin
export PATH
fi
EOF
# 6. 收紧家目录权限,防止用户改自己的配置
chown -R root:limited /home/limited/bin
chmod 755 /home/limited/bin
chmod 750 /home/limited/.bashrc
chown root:limited /home/limited/.bashrc
这套配置完成后,用 limited 用户登录,你会发现自己被锁在一个非常小的命令集合里,cd 不能用,ls /etc 这个带路径的命令也不能执行。如果想查看 /etc/passwd,可以用 grep 加相对路径吗?其实也不行,因为 /etc/passwd 含 /,grep 的参数里带 / 是允许的,重定向不行,但读取文件内容本身不受含 / 参数的限制。所以 grep root /etc/passwd 是可以执行的,cat /etc/passwd 也是可以执行的。这说明一个现实:受限模式限制的是"如何执行命令"和"往哪儿写文件",并不阻止用户读取他们有权读取的文件。
这也是为什么最后还要配合系统权限、文件权限一起做,而不是靠一个 rbash 走天下。另外提醒一句:不要把 vi、less、more、python 这些命令链接到白名单目录,否则受限模式形同虚设。
我在多次配置中得到的体会是:受限 Shell 最尴尬的地方,恰恰也是它最有用的地方——它给不了你绝对安全,但它能非常便宜地帮你把"误操作"和"路径探索"挡在门外。想清楚你防的是谁,再决定用哪把锁,这把锁才能真正发挥价值。
