In the name of practising-what-you-preach, I like to be in the habit of configuring my Linux workstations with Puppet in the same way that I do for servers. Though I confess I have fallen out of habit recently and accumulated a bunch of tools that haven’t made it back to the manifests. Having just trashed my laptop, in an unrelated incident, I fancied the opportunity to check out chef-solo for bringing a fresh install of Mint Linux up to speed.
The Chef package currently available from Ubuntu’s own package repositories is version 0.8, but I wanted to use the latest and greatest 0.10. That’s simple to solve with my new-found love for rbenv and Bundler. Except one hitch - both
chef-solo and it’s cousin
puppet apply need to be run as root in order to manage system resources. As it turns out rbenv wasn’t designed to be run through
If we attempt to run
chef-solo then it unsurprisingly fails to find the binary:
If we attempt to tell it where the binary shim is then we lose the magic of Bundler:
If we attempt to pass it through the Bundler shim then we get this rather obtuse error caused by it using the system install of Ruby:
The reason this does not work out-of-the-box is that most operating systems distribute sudo compiled with the
secure_path option enabled. This throws away the caller’s
$PATH environment variable and replaces it with a predefined list of search paths that are to be considered safe.
If you search around a little you’ll find a handful of people complaining that this is a “bug” or “annoying feature” of some OSs like Ubuntu. On the contrary, it is there to protect you. Those predefined paths are considered safe because only the root user can drop new binaries into those locations. If your normal PATH variable has questionable contents like
./, intentionally or otherwise, then they could supersede normal system binaries and be unwittingly executed as a privileged user.
Do not, whatever you do, disable this feature with
!secure_path. It’s enabled by default for a good reason. You can subvert it by resetting the variable for a given command with
env. This is what the rvmsudo function provided by RVM does to pass a variety of environment variables over regardless of
env_reset. It’s not ideal though.
Since we only want
rbenv to work in the sudo session let’s concentrate on that. We only need to pass on rbenv’s binary and shim directories. This is effectively what rbenv’s own init/setup does.
What I had originally hoped to do was prepend these to the value of
secure_path. This is possible, but it’s complicated somewhat by shell quoting:
That works for simple cases. But pre-quoted arguments get stripped of their quotes:
We can mitigate this slightly by identifying arguments that contain whitespace and were thus previously quoted, then re-quoting them:
Which gets us close, but not perfect:
We could preserve those with some more hackery but at this point I decided it probably wasn’t worth it. How about using the same
eval command trick that
rvmsudo does. Sure eval is pretty evil and it in-turn means that we can’t evaluate the
$PATH within the sudo session. However a predefined list seems like the next best thing:
Now this works as expected:
rbenv provides us with two niceties to wrap this up with. The first is plugin support which means that instead of using a Bash function we can create a sub-command called
rbenv sudo simply by dropping a shell script into the right place. Furthermore all rbenv plugins have access to an
RBENV_ROOT variable which saves us from calling out to
$(rbenv root). So we’re left with the following code:
I’ve made plugin this available on Github as dcarley/rbenv-sudo. Any comments or pull requests are welcomed.