## What is the Curl Pipe Bash Problem?
You have probably seen documentation on commands to use to install chef-client that look something like:
[bash]
curl -L https://www.chef.io/chef/install.sh | bash -s — -v 12
[/bash]
Or just:
[bash]
curl -L https://chef.sh | bash
[/bash]
Or for ChefDK:
[bash]
curl -L https://www.chef.io/chef/install.sh | bash -s — -v 12 -P chefdk
[/bash]
What these commands do is talk to a service we have called omnitruck and that downloads a shortish 600-line shell script which does the logic of figuring out what kind of distribution you are running and then downloads the correct s3 package artifacts for you (with some handling of version numbers, and nightlies and command line options to install chefdk, etc).
These commands all follow the pattern of downloading an installer script off the internet and then running it locally on the box through a bash shell. This practice has come under fire for numerous reasons.
I’ll start by laying out what I think is the level of actual concern that you should have over this practice and the real risk, and then will describe some alternatives, because absolutely nobody is telling that you that YOU MUST be using curl pipe bash to install Chef. At the same time there are good reasons that we have for having this install method and we will likely continue to put this install method first in front of new users.
So, here are five different things you can do about curl pipe bash.
## Stop Caring
Yeah, so the first way to deal with install.sh really is to stop caring. I’m going to talk down to you a little. Skip forwards one or two sections if you don’t care to hear my arguments.
Initially I wrote out a lot of rationale about why you should stop caring because most people who make the argument that curl-pipe-bash is bad make some pretty bad arguments. Suffice it to say that we’ve got the best architected curl-pipe-bash that we can build. It uses SSL, we checksum all the binaries, keep the checksums separate from the artifacts, on accounts with different credentials, we have the sources to all the scripts checked into git with histories and access so you can submit PRs against it. If you’re concerned about an unreviewed 600 line install.sh script you should also be much more concerned with the 10,000s of lines of ruby code in the chef-client itself (and please test our RCs more before upgrading, we *know* that nobody does that remotely often enough).
There is one unqiue risk which install.sh does open you up to, however, which is the case where the omnitruck servers themselves get hacked, and where install.sh is tampered with. That file will then be shipped to your servers and you will run it as root. If you use install.sh as part of an automated provisioning process or via an auto-update script which hits install.sh then you will be open to that attacker injecting root-running code into your servers for the amount of time where we have not detected the tampering. An RPM or Deb repo solves this problem since the root-running code is the yum or apt-get binary on your distro, and the package that is downloaded is signed and presumably you have turned on validation of the binaries and so tampered packages would be detected (although you still have to trust that the public signing key you downloaded was not tampered with and you still are exposed to someone stealing the private key that we use to sign our binaries).
I will freely admit that this is a weakness of our install.sh and where installing via a package manager is better. However, I will mention that the people who worry about this and worry about our servers getting hacked are probably not worrying enough about our signing key also getting compromised, which would also lead to a compromise of a package manager distribution route. It is, however, true that it is more likely that the omnitruck ruby app could be broken into and that we do have additional safeguards around our private key which is not stored on directly internet-facing servers.
## Cache the install.sh Script Somewhere
The first simplest solution to the install.sh issue is to cache the script. If you cache it then nobody hacking our servers can tamper with your version of the script. Then it is no longer a “random script” you download off the internet any more than ‘apt-get’ is a “random binary” that you run on your distro. You’re exposed to roughly the same level of attack that someone hacking one of the distribution mirror sites could accomplish against you. The script, may of course get out of date, but its been fairly stable for a long time and the API has been stable, and you can always peruse the github history and determine when and if you need to pull down bugfixes.
Since the script will checksum the artifacts that it pulls down and since the artifacts and the checksums are stored in different repositories with different credentials internally, and its all done over SSL that will up front mitigate most ways that the information that has been downloaded has been compromised. And that is on top of the signing of the artifact that is done (MacOSX and Windows are being fixed) so that if you have package validation turned on in your package manager it will also rejected tampered packages just like if you were using a package repository and had imported our public key.
## Use the packagecloud.io Repositories
Okay if you really just don’t like install.sh then the next solution that you’ll probably be very pleased to read about is that we have RPM and deb file repositories at packagecloud.io. There is some documentation on how to install the packagecloud repos on the packagecloud site itself. Note that you’ll land in the default “Bash Scripts” tab there which is (LOL!) probably not where you want to go by default, so you’ll be wanting to hit the ‘Chef’ tab. The instructions there are pretty short and sweet and point you at the packagecloud chef cookbook to do most of the work.
In the metadata.rb of a Chef recipe:
[ruby]
depends “packagecloud”
[/ruby]
And then I’d actually suggest a slightly more complicated recipe that borrows a little from the omnibus_updater cookbook:
[ruby]
packagecloud_repo “chef/stable” do
type “deb” # or “rpm”
end
package “chef” do
version node[‘myorg’][‘chef-client-version’]
action :install
notifies :run, ‘ruby_block[omnibus chef killer]’, :immediately
end
# Continuing with a chef client run after the client has been upgraded will give undetermined results
ruby_block ‘omnibus chef killer’ do
block do
raise ‘New omnibus chef version installed. Killing Chef run!’
end
action :nothing
end
[/ruby]
This requires Chef being running to install Chef, however, so it doesn’t solve the initial bootstrapping problem. So you’ll need to use this in combination with one of the two methods below. I do believe that code is a drop-in replacement for the most common best-practice use-case of the omnibus_updater cookbook (assuming you are all deb/rpm).
## Custom Knife Bootstrap Template
This lets you do literally whatever you want by dropping in replacement shell script code for the code that knife boostrap runs on the node being bootstrapped. The template that chef uses by default is called ‘chef-full’ (short for ‘full stack omnibus installer’) and can be found in in the Chef source code. The block of code (as of 12.4.1) that handles installing the chef-client can be found here and can be changed to whatever you like:
[ruby]
<% if knife_config[:bootstrap_install_command] %>
<%= knife_config[:bootstrap_install_command] %>
<% else %>
install_sh=”<%= knife_config[:bootstrap_url] ? knife_config[:bootstrap_url] : “https://www.opscode.com/chef/install.sh” %>”
if test -f /usr/bin/chef-client; then
echo “—–> Existing Chef installation detected”
else
echo “—–> Installing Chef Omnibus (<%= latest_current_chef_version_string %>)”
do_download ${install_sh} $tmp_dir/install.sh
sh $tmp_dir/install.sh -P chef <%= latest_current_chef_version_string %>
fi
<% end %>
[/ruby]
You can edit that code, drop in shellcode which sets up the packagecloud repos, or upload a specific chef version to an S3 bucket and download it directly from your own private URL, etc, etc. Once you are happy with it or want to test your version you can place it in `~/.chef/bootstrap/my-chef-full.erb` and you can use it by passing a `-t my-chef-full` argument to `knife bootstrap`. I don’t know what a good example for you would be, but it could be as simple as replacing that code with this:
[ruby]
rpm -ivh https://myawsbucket.s3.amazonaws.com/chef-12.4.1-1.el5.x86_64.rpm
[/ruby]
## Custom Bootstrap Install Command
For the simple case above the astute reader may have noticed that there’s another way to do it. The problem with forking your own template is that then you buy the cost of importing new bugfixes into your fork whenever we fix or enhance something. So users with forked templates ran into difficulties using the new validatorless bootstrap feature which requires template changes — which were made to chef-full but which users with custom templates cannot use until they fix their templates. If all you want to do is replace the install.sh script with some other command we added the ability to tweak that on the knife bootstrap command line (or in the knife.rb file) back in 11.14.0. The flag to add to knife bootstrap to replace install.sh and replicate the previous example would be `–bootstrap-install-command=’rpm -ivh https://myawsbucket.s3.amazonaws.com/chef-12.4.1-1.el5.x86_64.rpm’` Note carefully the single quotes around the command argument because the command itself contains spaces, and if you start getting too complicated here you many run into other shell expansion issues (and Windows users will need to experiment).
If all you want to do is drop in another command in place of curl|bash that will be the easiest solution, and most future-proof. In order of recommended practice this is better than forking a custom template, but introducing the bootstrap templates first in this article made more expository sense.
## Summary and Historical Notes
Hopefully this presents useful alternatives for people who are frustrated about the curl|bash problem. And it probably is not a comprehensive list of all the different alternatives, but should provide useful jumping off points.
I would also like to flatly dispel the notion that because we put curl|bash in front of people initially that we think its necessarily the recommended solution for everyone. We wrote install.sh in order to solve a hard problem that knife bootstrap needed a way to install omnibus artifacts across platforms when the workstation knife was issued on didn’t necessarily ‘know’ what the host it was going to bootstrap was, and that host did not have chef or ohai installed. We also were facing a problem of proliferating bootstrap templates and the requirements of every bootstrap command to contain the distribution that was going to be bootstrapped (turning the person typing on the keyboard into ohai and making knife bootstrap commands non-portable when you were bootstrapping different targets). From the perspective of a Chef user who manages only Ubuntu boxes this is an overly complicated problem to solve. From the perspective of a Chef developer who has Customers that manage a mishmash of different Linux distros with Solaris and AIX sometimes tossed into the mix it was the problem that we needed to solve up front. The packagecloud repository solution is useful for Ubuntu and RHEL users but utterly useless for any Customer who has an AIX or Solaris problem.
The combination of `knife bootstrap` and `install.sh` is the solution which covers all the bases but which is not necessarily the best optimized for any individual one. So, that is why its there and why its still around and why its likely to be around for quite some time to come. Please use the other options that are available, however, and provide feedback or just thumbs up on them.
## More Stuff Coming Soon
Take a peek at mixlib-install to sort out all of the differences between test-kitchen, vagrant-omnibus, chef-provisioning, and all the other code which has invented its own ways to install omnibus artifacts.