Restricting CI Accounts With Rush
Motivation: locking down CI accounts
This blog is written in Hugo. The source is hosted on Gitlab, and deployed automatically by a Gitlab Pipeline when I push a new post. This is a fairly standard setup, involving running Hugo and then rsync-ing the build to my server.
There was still one aspect that I wanted to improve, and couldn’t find much assistance on: best-practice on locking down the account used by rsync, in case the ssh key is ever compromised. It should obviously be a dedicated account, with no unecessary privileges and only used for that purpose, but that still leaves our hypothetical scenario of escalation following a regular ssh entry.
I have previously used OpenSSH’s
internal-sftp
to set up an SFTP
server that can’t be used for general shell access, so I was asking:
what is the equivalent for rsync?
Rush
The answer seems to be: GNU Rush! (For “Restricted user shell”, which gives you an idea of its purpose)
I initially found the examples a bit hard to follow, and furthermore the syntax in the online manual is a bit different from the version included with the Ubuntu 20.04 LTS system I was using it on.
So, two useful points to get started:
- The manual you want (on similar systems) is this one; and
- The first line in your config should probably be
debug 3
, because any errors are otherwise hard to spot. Output will be in/var/log/auth.log
by default.
How it works
Firstly, set the CI user’s shell to /usr/sbin/rush
.
The configuration file (/etc/rush.rc
) contains a number of rules,
which consist of conditions defining the commands to match on, and
transformations to undertake.
Conditions can include regexp matches over the whole command line, or
individual parts. By default, only one rule matches unless you
specify fall-through
. The default configuration uses a fall-through
rule to set some ulimits and a restricted environment-variable
profile.
Transformations can include forcing a particular working directory (possibly involving chroot), and manipulating paths.
Rsync is a common use-case, and the examples provided should get you started.
Warning: the default configuration probably will not work out of the
box! This is because it refers to paths such as /srv/rush
that you
probably don’t have on your system. Increasing the debug verbosity as
recommended above will help you track this down, but it is still a lot
to wade through.
A simple example
We can see this in action with a very simple local example. We will
create a user with the Rush shell, and only allow them to run the “ls”
command, and furthermore only in the /tmp
directory.
apt install rush
adduser --shell /usr/sbin/rush --disabled-password rushdemo
Simplify /etc/rush.rc
to the following:
debug 3 # change back to 1 when you're happy
rule ls-demo
command ^ls
set /bin/ls
chdir /tmp
Now we can try it out:
$ sudo -u rushdemo -i ls
... /tmp listing elided...
Success! Now try another directory:
$ sudo -u rushdemo -i ls /etc
... /tmp listing elided...
Also success! Or is that really what we wanted? We can do better; it is more likely that you want to block attempts to list any other directory. We’ll do this with 2 rules; the first to match legal commands, and a second to reject anything else.
rule ls-demo
command ^ls$
set /bin/ls
chdir /tmp
rule ls-trap
command ^ls
argc > 1
exit fatal: arguments not allowed
Note that we’ve changed the first rule to match only on “ls
”. The
second will match anything else that starts with ls
, and has more
than one component (the argc
) line. If this rule matches we exit
with an error:
$ sudo -u rushdemo -i ls
... /tmp listing elided...
$ sudo -u rushdemo -i ls /etc
fatal: arguments not allowed
Now we’re in business!