Restricted shells promise to let someone log in to a server but only do a few things, and in practice that promise breaks the moment any allowed program can start another program. Administrators still reach for rbash, rssh, custom menus and sudo rules on editors to give contractors, support staff, backup jobs and file-transfer partners "limited" access, and penetration testers still walk out of them routinely. The GNU Bash manual itself calls restricted mode only one component of a restricted environment. This guide explains what rbash actually restricts, why shell escapes keep working, and which mechanisms hold up better: SSH forced commands with restrict, internal-sftp with ChrootDirectory, purpose-built shells such as git-shell, sudo NOEXEC and INTERCEPT, and isolation through containers or jump hosts. It is written for Linux and Unix administrators, platform engineers and the people who review their access designs.
Key takeaways
- rbash is a convenience, not a boundary. It blocks
cd, slashes in command names, output redirection and changes to PATH, but any permitted program with a shell escape undoes all of it. - The allowed command list is the real attack surface. Editors, pagers, interpreters,
find,scpandrsynccan all run other programs; GTFOBins catalogues hundreds of such binaries. - Forced commands are stronger, if you close the side doors. OpenSSH
ForceCommandandcommand=ignore what the client asks for, but do not stop port, agent or X11 forwarding unless you addrestrictorDisableForwarding. - Prefer a mechanism built for the job.
internal-sftpin a chroot for file transfer, git-shell for Git, a single-purpose script for automation, a container or jump host for anything interactive. - Assume escape and limit the blast radius. The restricted account should have nothing worth stealing: no sudo, no readable secrets, no writable paths in other users' trust chains, and full session logging.
What rbash actually restricts
Bash enters restricted mode when it is started as rbash or with -r or --restricted. The Bash 5.3 manual page lists what is disallowed or not performed:
| Restriction | What it stops | What it does not stop |
|---|---|---|
No cd | Changing directory in the shell | Programs that open files by path on their own |
| Cannot set or unset SHELL, PATH, HISTFILE, ENV or BASH_ENV | Pointing PATH at a directory of the user's choosing | Variables not on the list, such as VISUAL, EDITOR or PAGER, which change what allowed programs launch |
| No command names containing a slash | Running /bin/sh directly | Allowed programs that run a shell internally |
No slash in filenames for ., history and hash -p; no -p for . or command | Sourcing or hashing arbitrary paths | Programs that read configuration from the home directory |
No output redirection (>, >>, >|, <>, >&, &>) | Writing files from the shell | Allowed programs that write files (editors, tee, scp) |
No exec, no enable changes, no importing functions or BASHOPTS and SHELLOPTS from the environment | Replacing the shell or loading builtins | Anything outside the shell process |
Cannot turn off restricted mode (set +r, shopt -u restricted_shell) | Simple self-release | A new, unrestricted shell started by another program |
Two details catch people out. Restrictions apply only after startup files are read, so a user who can write ~/.bashrc or ~/.bash_profile controls the session. And when rbash runs a command that turns out to be a shell script, the manual says restrictions are turned off in the shell spawned to run it. The manual's own advice is to set PATH to a small set of verified commands, move the user to a non-writable directory other than home, block shell scripts, clear variables such as VISUAL and PAGER, and use jails, zones or containers where stronger separation is needed.
Why shell escapes keep working
A shell escape is any feature of an allowed program that starts another program, writes a file that will later be executed, or reads a file the user should not see. These are ordinary features, not bugs, which is why they survive patching. GTFOBins, the community catalogue of Unix binaries that can bypass local restrictions on misconfigured systems, classifies them into functions including Shell, Command, File read, File write, Upload, Download and Library load, and contexts including Unprivileged, Sudo, SUID and Capabilities. It states it is not a list of exploits but of legitimate functions.
| Class of allowed program | Why it escapes | Safer alternative |
|---|---|---|
| Editors (vi, vim, nano, emacs) | Run external commands and open any readable file | sudoedit scoped to named files, or a web or API workflow |
| Pagers (less, more, man) | Run commands and open an editor (VISUAL, EDITOR) | Output to the client without a pager; set PAGER to a fixed safe value |
| Interpreters (python, perl, awk, lua) | Full programming language with process and file APIs | Never allow; wrap the needed task in a fixed script |
| File tools (find, tar, zip, rsync, scp) | Options that run commands or load code | internal-sftp in a chroot; fixed rsync command with validated arguments |
| Network tools (ssh, curl, wget) | Load configuration and proxy commands; move files in and out | Do not allow on restricted accounts |
| Custom menus and wrapper scripts | Unquoted input passed to a shell, or calls to the programs above | Fixed command set, no user-controlled arguments, reviewed like any privileged code |
Purpose-built restricted shells fail the same way. rssh, which limited SSH users to scp, sftp, rsync and similar, had CVE-2019-1000018 (command execution through its allowscp permission) and CVE-2019-3463 and CVE-2019-3464 (rsync argument and environment variable bypasses). When the ESnet security team reported the scp issue, the author replied that rssh is no longer maintained. Any system still using it should migrate. For the defensive view of these techniques see the Linux Privilege Escalation Techniques Guide.
Mechanisms compared
| Mechanism | Good for | Strength | Main caveat |
|---|---|---|---|
| rbash with a curated PATH | Reducing accidents by trusted users | Low | Any escape in an allowed binary defeats it |
SSH ForceCommand (sshd_config, in a Match block) | Accounts or groups that should run one thing | Medium to high | Runs through the user's login shell with -c; forwarding must be disabled separately |
command= with restrict in authorized_keys | Automation keys: backups, deploy hooks, monitoring | High for that key | The wrapper script must validate SSH_ORIGINAL_COMMAND safely |
internal-sftp plus ChrootDirectory | File transfer partners and uploads | High | Chroot path must be root-owned and not writable by group or others |
| git-shell | Git-only SSH access | High | A git-shell-commands directory enables custom and interactive commands |
| sudo with NOEXEC, INTERCEPT or sudoedit | Delegated admin tasks | Medium to high | NOEXEC only covers dynamically linked programs on supporting systems |
| Container, VM or jump host session | Interactive support access | High | More to build and run; still needs identity, logging and least privilege |
SSH forced commands done properly
- Behaviour: sshd_config(5) says
ForceCommandignores any command supplied by the client and~/.ssh/rc, and applies to shell, command and subsystem requests. The original request is available to the wrapper inSSH_ORIGINAL_COMMAND. - Close the side channels: the same manual warns that
ForceCommanddoes not limit TCP, agent, socket or X11 forwarding. UseDisableForwarding yesandPermitTTY noin theMatchblock. For per-key restrictions in authorized_keys, sshd(8) definesrestrict, which disables port, agent and X11 forwarding, PTY allocation and~/.ssh/rc, and will include future restrictions. - Why it matters: OpenSSH's 2016 X11 forwarding advisory (CVE-2016-3115, fixed in 7.2p2) showed that injected xauth commands could get around ForceCommand, authorized_keys command restrictions and restricted shells when X11 forwarding was enabled. Its recommended mitigation was
X11Forwarding noandrestrictorno-x11-forwardingon restricted keys. - Wrapper scripts: treat
SSH_ORIGINAL_COMMANDas untrusted input. Match it against an exact allow list and run fixed commands; never pass it toevalor an unquoted shell expansion. See the Secure Bash and Shell Scripting Guide. - Login shell: because the forced command runs through the account's login shell, set that shell to a plain, root-owned shell and make the home directory and its dotfiles non-writable by the user.
sudo controls for allowed programs
The sudoers manual describes two tags for programs that can start other programs. NOEXEC prevents a dynamically linked program from running further commands, where sudo and the operating system support it; the manual's example applies it to more and vi. INTERCEPT instead validates every child command against the sudoers policy and logs it, at some performance cost and with limits on set-user-ID programs. For editing system files, sudoedit scoped to named files avoids running an editor as root at all. See the sudo and sudoers Security Guide.
How to implement
- Inventory restricted access: find accounts whose login shell is rbash, rssh, a menu script or similar, every
Matchblock withForceCommand, and every authorized_keys entry withcommand=. See How to Find ssh authorized keys across a server fleet. - Write down the purpose of each: file transfer, Git, a single automated task, or interactive support. The purpose decides the mechanism from the table above.
- Replace the weakest first: rssh and rbash accounts used by external parties, then menu scripts, then forced commands missing
restrictorDisableForwarding. - Pilot on one host: deploy the new configuration with
sshd -tto validate syntax, keep an existing root session open, and test both the intended task and a set of denied actions (interactive shell, port forward, agent forward, file outside the chroot). - Roll out by group: use
Match Groupblocks so you can move accounts between old and new configurations by group membership, which also makes rollback a group change rather than an edit under pressure. - Test escapes, not just function: include restricted accounts in authorised penetration tests and ask testers to report every escape route, not just the first.
Where identity fits
Most restricted SSH access today belongs to non-human identities: backup agents, deployment pipelines, monitoring collectors and partner file transfers, each with a key in an authorized_keys file. Those keys rarely expire and are often copied between systems. A forced command with restrict and a from= source address limit is the minimum for each one; short-lived SSH certificates issued per job are better. Each key needs an owner, a purpose and a review date. See the SSH Key Management Guide and the Linux Service Accounts and Keytab Management Guide.
Detection and measurement
- Process lineage: alert when a restricted account's session produces a shell or interpreter process (
sh,bash,python,perl) whose parent is an editor, pager or file tool. Linux auditdexecverules or an EDR agent provide the data. See the Linux Logging and auditd for Defenders Guide. - SSH channel requests: log and alert on refused forwarding or PTY requests from restricted accounts, which often show probing.
- Configuration drift: monitor sshd_config, authorized_keys and dotfiles in restricted homes with file integrity monitoring.
- Metrics: number of accounts on rbash or rssh (target zero for external users); percentage of
command=keys that also carryrestrictandfrom=; percentage ofForceCommandblocks with forwarding disabled; escapes found per penetration test; restricted accounts with sudo rights (target zero).
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | AC-3, AC-6 | Access enforcement and least privilege for limited accounts |
| NIST SP 800-53 Rev. 5 | CM-7, CM-7(2) | Least functionality and preventing program execution beyond what is needed |
| NIST SP 800-53 Rev. 5 | AC-17, AU-12 | Remote access controls and audit record generation for sessions |
| NIST CSF 2.0 | PR.AA-05, PR.PS-01, DE.CM-03 | Least-privilege permissions, configuration management, monitoring of personnel activity |
| ISO/IEC 27001:2022 Annex A | A.8.2, A.8.18 | Privileged access rights; use of privileged utility programs |
| CIS Controls v8.1 | Control 5, Control 6 | Account management and access control management |
| MITRE ATT&CK v20 | T1059.004 Unix Shell, T1548.003 Sudo and Sudo Caching | What a successful escape produces, and abuse of sudo rules on escape-capable programs |
Common failure patterns
- Allowing one "harmless" editor or pager: it turns rbash back into a full shell. If users need to edit, use
sudoeditor a managed workflow. - Writable home and dotfiles: the user edits
~/.bashrcor adds files to a PATH directory, and restrictions never apply. - Forced command without
restrict: the key cannot open a shell but can still forward ports into the internal network or use an agent, which is the gap CVE-2016-3115 and the sshd_config warning describe. - Still running rssh: an unmaintained tool with published bypasses guards external file transfer accounts.
- Restricted account with secrets: SSH keys, cloud credentials or database passwords readable by the restricted user mean any escape becomes lateral movement. A jump host that holds such secrets is a high-value target; see If your jump host and bastion is compromised.
- Testing only the happy path: the task works, so the configuration ships, without anyone trying the denied actions.
Practitioner checklist
- Inventory accounts on rbash, rssh or menu shells,
ForceCommandblocks andcommand=keys. - Migrate file transfer accounts to
internal-sftpwith a root-ownedChrootDirectory. - Add
restrictandfrom=to every automation key withcommand=. - Set
DisableForwarding yesandPermitTTY noin everyMatchblock that usesForceCommand. - Validate
SSH_ORIGINAL_COMMANDagainst an exact allow list in wrapper scripts. - Remove editors, pagers, interpreters and file tools from restricted PATHs; use
sudoedit, NOEXEC or INTERCEPT where delegation is required. - Make restricted home directories and dotfiles non-writable by the user.
- Log process creation for restricted sessions and alert on shells spawned by allowed programs.
- Retire rssh and replace custom menu scripts with single-purpose commands.
- Include restricted accounts in penetration test scope with escape testing explicitly requested.
Standards and references
- GNU: Bash Reference Manual, The Restricted Shell
- OpenSSH: sshd_config(5)
- OpenSSH: sshd(8), AUTHORIZED_KEYS FILE FORMAT
- OpenSSH: X11 forwarding input validation advisory
- Sudo Project: sudoers manual
- Git: git-shell documentation
- GTFOBins: Unix binaries that bypass local security restrictions
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: SSH Key Management Guide · sudo and sudoers Security Guide · Linux Server Hardening Guide · Linux Privilege Escalation Techniques Guide · Terminal Escape Sequence Injection Guide