Nov 29, 2017 3:00 AM

Chef InSpec: Where security and compliance meet devops

How to use InSpec, Chef's open source audit and test framework, to define and enforce security standards

Thinkstock

As our applications become more complex and the number of systems we manage grows, it’s only natural to worry that the risks to our environments are also increasing. This worry is profoundly felt in industries that must adhere to regulatory compliance standards.

Regulatory frameworks like PCI, HIPAA, FedRAMP, and the forthcoming GDPR mandate rigid security requirements for computing environments, but they introduce a new concern: that they will slow the pace of development for organizations that aren’t equipped to rapidly and effectively validate the compliance of their environments. Even in organizations that don’t need to adhere to any specific regulatory standard, the ability to reliably validate security is no less important, as frequent headlines about vulnerabilities and security breaches are keen to remind us.

To evaluate our readiness to adapt to these challenges, there are a few questions we must ask. Can we accurately determine which servers, in a fleet of thousands, are in need of software patching? Can we validate that the new feature developed adheres to our organization’s security requirements? Can we ensure our environments comply with regulatory standards, even when not actively under audit?

InSpec is an open source testing framework that can help you answer “yes” to all of those questions by providing an easy to understand, yet deeply customizable, framework to define expectations for the systems managed and detect deviations from those expectations wherever they arise.

InSpec provides a great amount of flexibility in how to go about that detection process. For ad hoc point-in-time scans, the InSpec command line utility allows users to evaluate any system reachable over SSH or WinRM. For testing infrastructure changes in development, InSpec can be used natively in Test Kitchen to validate that Chef Cookbooks behave as expected and that the resulting system remains compliant with existing policies. Finally, for continuous compliance evaluation, InSpec can be invoked as part of a Chef Client run via the audit cookbook, which can be configured to send data to a Chef Automate server, providing an aggregated, searchable, and filterable view into the health of the environment.

Below we will walk through a step-by-step guide to using InSpec to define and enforce security standards.

InSpec controls

InSpec code is made up of “controls” that define a single expectation, or group of expectations, for your systems. Here is a simple example:

control ‘sample-1.0’ do
  impact 1.0
  title ‘Hello, World’
  desc ‘You have to start somewhere’
  describe command(‘echo “Hello, World”‘) do
    its(‘exit_status’) { should eq 0 }
    its(‘stdout’) { should cmp /Hello, World/ }
  end
end

Let’s take a look at the “sample-1.0” control and see what makes it tick.

impact 1.0

Every control is rated on its impact, a weighted value from 0 to 1 describing its criticality. Most InSpec profiles will consist of many controls, which allow users to categorize results as minor (0.0 - 0.3), major (0.4 - 0.6), or critical (0.7 - 1.0).

title ‘Hello, World’
desc ‘You have to start somewhere’

The title and desc parameters allow users to define a human-readable title and description for a control. When using the Chef Automate UI, these will be the first elements to show up when selecting a control, with a click-through to the source code in full. This provides an excellent way for different teams to get the context they need about systems, whether they need only high-level information about controls or to dig into the methodologies to craft a remediation.

Chef Software
  describe command(‘echo “Hello, World”‘) do
    its(‘exit_status’) { should eq 0 }
    its(‘stdout’) { should cmp /Hello, World/ }
  end

Now we get to the meat of the control. The InSpec DSL is made up of “resources” that provide a variety of built-in validations for common infrastructure components. Above you see the “command” resource, which tells InSpec to run the command specified—in this case “echo ‘Hello, World’”—and evaluate what it returns.

Each resource contains one or more “matchers,” the definition of an expected result when evaluating the resource. In this case, two expectations are set:

  1. The command’s exit status should be 0
  2. The command’s stdout should contain “Hello, World”

The InSpec executable

Now that we’ve covered the makeup of InSpec controls, how can we start using them? The Chef development kit includes the InSpec command line utility, which will allow us to evaluate our example control. If we take the full text of the control in the previous section and save it to a file called sample_control.rb, we can run it on our local machine with this command:

inspec exec sample_control.rb

Our output should look something like this:

Profile: tests from sample_control.rb
Version: (not specified)
Target:  local://
  ✔  sample-1.0: Hello, World
     ✔  Command echo “Hello, World” exit_status should eq 0
     ✔  Command echo “Hello, World” stdout should eq “Hello, World\n”

Profile Summary: 1 successful, 0 failures, 0 skipped
Test Summary: 2 successful, 0 failures, 0 skipped

The InSpec executable can also scan remote systems, provided they’re available via SSH or WinRM. The syntax for doing so with the above example would look like so:

inspec exec sample_control.rb -t ssh://USER@HOSTNAME -i /path/to/identityfile

or

inspec exec sample_control.rb -t winrm://USER@HOSTNAME -p ‘PASSWORD’

It is worth noting that this example should work on most servers, regardless of operating system. InSpec’s execution doesn’t change from platform to platform. As long as the OS is supported, if the system being scanned behaves as described in the control, the tests will pass and the system will “echo” the expected output, regardless of whether that system is Linux, Windows, MacOS, etc.

In circumstances where differences between servers would necessitate different testing, the InSpec DSL allows users to define conditional execution for controls or resources. To illustrate this, we created a new file, called user_test.rb, containing the following code:

control ‘Test Windows Super User’ do
  impact 0.5
  title ‘Superuser Test’
  desc ‘Make sure the Administrator user exists.’

  only_if do
    os.windows?
  end

  describe user(‘Administrator’) do
    it { should exist }
  end
end

control ‘Test *nix Super User’ do
  impact 0.5
  title ‘Superuser Test’
  desc ‘Make sure the root user exists.’

  only_if do
    os.redhat? || os.debian? || os.linux? || os.darwin? || os.bsd?
  end

  describe user(‘root’) do
    it { should exist }
  end
end

Here, two controls are defined and in each we see a block like this:

only_if do
  os.windows?
end

We’re specifying that this particular control should only be run if the system being evaluated is running Windows. To illustrate, let’s try it out first locally on a Windows server, then remotely on a Linux counterpart. Note that the test for the Linux superuser is skipped on the Windows machine, and vice versa:

Chef Software

InSpec profiles

Thus a single file can be used to run controls. However, before committing that file to version control, or uploading it to a Chef Automate server, we’ll need to add it to an InSpec profile. InSpec profiles allow users to organize controls to support versioning and dependency management. To demonstrate, we’ll create an InSpec profile using the same CLI as before:

inspec init profile example_profile

This should return output similar to:

Create new profile at /Users/myuser/example_profile
* Create directory controls
* Create file controls/example.rb
* Create file inspec.yml
* Create directory libraries
* Create file README.md
* Create file libraries/.gitkeep

Creating a profile creates a number of files and directories, but for now, turn your attention to the controls directory.

The example.rb file that our command created provides a few more syntax examples for using InSpec controls, with some helpful comments. However, since we have controls of our own to implement, we can delete example.rb and move our sample_control.rb and user_test.rb files into the controls directory instead:

$ rm example_profile/controls/example.rb
$ mv sample_control.rb example_profile/controls/
$ mv user_test.rb example_profile/controls/
$ ls example_profile/controls/

sample_control.rb    user_test.rb

Now that our controls are in place, we can run InSpec exec again, this time pointed at the path to the profile we created, and it will execute all of the contained profiles:

$ inspec exec example_profile

Profile: InSpec Profile (example_profile)
Version: 0.1.0
Target:  local://

  ✔  sample-1.0: Hello, World
     ✔  Command echo “Hello, World” exit_status should eq 0
     ✔  Command echo “Hello, World” stdout should cmp == /Hello, World/
  ↺  Test Windows Super User: Superuser Test
     ↺  Skipped control due to only_if condition.
  ✔  Test *nix Super User: Superuser Test
     ✔  User root should exist

Profile Summary: 2 successful, 0 failures, 1 skipped
Test Summary: 3 successful, 0 failures, 1 skipped

If we’re making use of a Chef Automate server, we can also run the inspec archive command to compress the profile into a tarball that can be uploaded to our server:

$ inspec archive example_profile
I, [2017-08-29T20:28:44.999748 #81989]  INFO—: Checking profile in example_profile
I, [2017-08-29T20:28:45.000973 #81989]  INFO—: Metadata OK.
I, [2017-08-29T20:28:45.006901 #81989]  INFO—: Found 3 controls.
I, [2017-08-29T20:28:45.007360 #81989]  INFO—: Control definitions OK.
I, [2017-08-29T20:28:45.007868 #81989]  INFO—: Generate archive
Users/myuser/example_profile-0.1.0.tar.gz.
I, [2017-08-29T20:28:45.019701 #81989]  INFO—: Finished archive generation.

Note that when we run inspec archive, InSpec will first check that the metadata and controls in the profile are syntactically valid, then generate a tarball that can be invoked directly with InSpec exec, as above, or uploaded to a Chef Automate server. In either case, InSpec runs controls from all contained files, so we’re seeing both of our tests from the previous section run together in all of these examples.

InSpec baseline profiles

So far, we’ve written InSpec code, created a profile, and seen examples of ways to scan our systems. But we’ve yet to ask our system anything particularly meaningful. Thankfully, there is a library of prewritten InSpec profiles available on the Chef Supermarket to get baseline security information quickly.

In particular, the Security Baseline profile, which is available for both Linux and Windows, gives a more thorough accounting of how well the systems are configured. Download the compliance profiles from the above links and apply them the same way we did with our example profile. We can also run a remote profile directly by providing a URL instead of a path to our InSpec exec command:

inspec exec https://github.com/dev-sec/linux-baseline/archive/master.tar.gz

That said, InSpec profiles have a feature that provides extra flexibility in handling whatever issues we find: profile dependencies.

Let’s revisit our example_profile, and this time take a look at inspec.yml:

name: example_profile
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0

The inspec.yml file is where the project metadata is defined. It’s where we can set a profile title, maintainer and license information, and the version that got appended to our tarball when we ran the InSpec archive command. This is also where we can specify dependencies on other profiles. Depending on the system being scanned, we can add either of the profiles called out previously. I’ll use the Linux baseline in this example:

name: example_profile
title: Simple Baseline Security Test
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.2.0
depends:
- name: linux-baseline
  url: https://github.com/dev-sec/linux-baseline/archive/master.tar.gz

The depends items can now be loaded and referenced in controls. To do so, let’s create another new file in our controls directory, baseline.rb, with the following content:

include_controls ‘linux-baseline’

Now if we run our updated profile against a Linux server—or Windows if using the other profile—we’ll see a lot more is going on. I ran it on one of my base images, and as I suspected, I may have some work ahead of me!

Test Summary: 66 successful, 55 failures, 2 skipped

Note that some controls in the baseline profiles will require superuser privileges to execute properly. If you encounter issues, you can use the —sudo flag with InSpec to invoke commands as root without needing to open up direct access for that user.

Using InSpec profile dependencies

Unless we have already been applying stringent security standards to our servers, we’ll likely see at least a few failures after running this profile. It can be scary seeing a big wall of failed tests, but this is a positive thing! We now have a roadmap of the issues we need to evaluate, and each control’s “impact” score can help us determine which issues to address first. Each of our failures becomes a decision point—when a control fails, generally speaking, we’ll want to fix it. We can do so with a tool like Chef, and in the case of the security baseline profiles, there are corresponding “hardening” Chef recipes available for Linux and Windows that can give us a leg up.

That said, when using a third-party profile, we may encounter situations where a failure isn’t actually one we care to fix—every organization is unique, and will have different requirements. We can always just fork an open source profile and modify it directly, but in so doing, we might lose access to future improvements made to that profile. Instead, we can use the dependency behavior we’ve already covered to specify which controls we do or don’t want to include from a profile, without needing to recreate it entirely.

Let’s take a look at an example from the Linux security baseline:

control ‘package-01’ do
  impact 1.0
  title ‘Do not run deprecated inetd or xinetd’
  desc ‘http://www.nsa.gov/ia/_files/os/redhat/rhel5-guide-i731.pdf, Chapter 3.2.1’
  describe package(‘inetd’) do
    it { should_not be_installed }
  end
  describe package(‘xinetd’) do
    it { should_not be_installed }
  end
end

This control, package-01, specifies that the packages inetd and xinetd not be installed on our systems, per the guidelines specified in the NSA document provided in the description. Indeed, this control is failing on my test instance:

  ×  package-01: Do not run deprecated inetd or xinetd (1 failed)
     ✔  System Package inetd should not be installed
     ×  System Package xinetd should not be installed
     expected System Package xinetd not to be installed

Let’s say, for the sake of argument, that our server makes use of xinetd in some fashion, and we don’t wish to uninstall it. Rather than rewrite the Linux security baseline profile outright, we can modify our baseline.rb to selectively omit it like so:

include_controls ‘linux-baseline’ do
  skip_control ‘package-01’
end

Then we can update the version in inspec.yml and re-run our scans. This time, when I run the command against my server, I can see that whereas previously I had 55 failures, now I have 54. If we scroll through the evaluated rules, we can see that our scans skipped right from os-10 to package-02, without us needing to make any other changes!

Test Summary: 65 successful, 54 failures, 2 skipped


×  os-10: CIS: Disable unused filesystems (8 failed)
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install cramfs /bin/true”
     expected nil to match “install cramfs /bin/true”
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install freevxfs /bin/true”
     expected nil to match “install freevxfs /bin/true”
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install jffs2 /bin/true”
     expected nil to match “install jffs2 /bin/true”
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install hfs /bin/true”
     expected nil to match “install hfs /bin/true”
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install hfsplus /bin/true”
     expected nil to match “install hfsplus /bin/true”
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install squashfs /bin/true”
     expected nil to match “install squashfs /bin/true”
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install udf /bin/true”
     expected nil to match “install udf /bin/true”
     ×  File /etc/modprobe.d/dev-sec.conf content should match “install vfat /bin/true”
     expected nil to match “install vfat /bin/true”
  ✔  package-02: Do not install Telnet server
     ✔  System Package telnetd should not be installed

Just as we can selectively omit a control from a profile, we can selectively include a control while skipping the rest. Let’s say that we do want to enforce the package-01 control (i.e., we do not want xinetd installed), but the rest of the rules in the baseline profile don’t apply to our systems. We can selectively include controls by using require_controls instead of include_controls. Let’s update baseline.rb so that our content now reads:

require_controls ‘linux-baseline’ do
  control ‘package-01’
end

Once again, after we update our version, and re-run our profile, we’ll see a very different set of results:

$ inspec exec example_profile-0.2.2.tar.gz -t ssh://USER@192.168.123.45 -i ~/.ssh/id_rsa —sudo

Profile: Simple Baseline Security Test (example_profile)
Version: 0.2.2
Target:  ssh://USER@192.168.123.45:22

  ✔  sample-1.0: Hello, World
     ✔  Command echo “Hello, World” exit_status should eq 0
     ✔  Command echo “Hello, World” stdout should cmp == /Hello, World/
  ↺  Test Windows Super User: Superuser Test
     ↺  Skipped control due to only_if condition.
  ✔  Test *nix Super User: Superuser Test
     ✔  User root should exist

Profile: DevSec Linux Security Baseline (linux-baseline)
Version: 2.1.1
Target:  ssh://USER@192.168.123.45:22

  ×  package-01: Do not run deprecated inetd or xinetd (1 failed)
     ✔  System Package inetd should not be installed
     ×  System Package xinetd should not be installed
     expected System Package xinetd not to be installed

Profile Summary: 2 successful, 1 failures, 1 skipped
Test Summary: 4 successful, 1 failures, 1 skipped

Now we have only the controls we care about. Success! 

InSpec next steps

Although we’ve covered a lot here, we have only glimpsed the power and flexibility InSpec can bring to environments. To go further with InSpec, you will want to check out the full reference documentation, tutorials, and interactive demonstrations at https://inspec.io. You will likely find these resources helpful as well:  

  • Compliance as Code – learning modules that expand on the skills covered in this article and introduce more of InSpec’s capabilities
  • Local Development and Testing – a step-by-step guide to using InSpec to validate cookbook code in Test Kitchen.
  • Integrated Compliance – a guide to using Chef Automate with InSpec to organize compliance scans into auditable reports in the Automate UI

In this article, we’ve limited our demonstration to the InSpec CLI for simplicity’s sake. If you would like to get hands-on with some of the other ways you can work with InSpec, you’ll find a ton of great resources at Learn Chef Rally.

Nick Rycar is technical product marketing manager at Chef Software

New Tech Forum provides a venue to explore and discuss emerging enterprise technology in unprecedented depth and breadth. The selection is subjective, based on our pick of the technologies we believe to be important and of greatest interest to InfoWorld readers. InfoWorld does not accept marketing collateral for publication and reserves the right to edit all contributed content. Send all inquiries to newtechforum@infoworld.com.