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?


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!

comments powered by Disqus